diff --git a/.github/scripts/get_latest_tags.py b/.github/scripts/get_latest_tags.py index 2eb62fb7..526716ce 100644 --- a/.github/scripts/get_latest_tags.py +++ b/.github/scripts/get_latest_tags.py @@ -49,6 +49,13 @@ def update_env(file_name: str, frappe_tag: str, erpnext_tag: str | None = None): f.write(text) +def update_output(file_name: str, frappe_tag: str, erpnext_tag: str | None = None): + with open(file_name, "a", encoding="utf-8") as f: + f.write(f"frappe_version={frappe_tag}\n") + if erpnext_tag: + f.write(f"erpnext_version={erpnext_tag}\n") + + def _print_resp(frappe_tag: str, erpnext_tag: str | None = None): print(json.dumps({"frappe": frappe_tag, "erpnext": erpnext_tag})) @@ -70,6 +77,9 @@ def main(_args: list[str]) -> int: file_name = os.getenv("GITHUB_ENV") if file_name: update_env(file_name, frappe_tag, erpnext_tag) + file_name = os.getenv("GITHUB_OUTPUT") + if file_name: + update_output(file_name, frappe_tag, erpnext_tag) _print_resp(frappe_tag, erpnext_tag) return 0 diff --git a/.github/workflows/app-build-image.yml b/.github/workflows/app-build-image.yml new file mode 100644 index 00000000..74a01048 --- /dev/null +++ b/.github/workflows/app-build-image.yml @@ -0,0 +1,189 @@ +name: App / Build Image + +on: + workflow_call: + inputs: + app_name: + required: true + type: string + description: "App module and image name, for example 'crm'" + app_repo: + required: true + type: string + description: "Git URL or GitHub slug for the app repository" + app_ref: + required: true + type: string + description: "Git branch or tag to install for the app" + frappe_ref: + required: true + type: string + description: "Tag of the existing frappe/base and frappe/build images, for example version-16" + frappe_image_prefix: + required: false + type: string + default: frappe + description: "Image prefix for existing base and build images, for example 'frappe' or 'ghcr.io/frappe'" + image_name: + required: true + type: string + description: "Full image name, for example ghcr.io/frappe/crm" + image_tag: + required: true + type: string + description: "Image tag, for example develop or v16.0.0" + push: + required: true + type: boolean + registry: + required: false + type: string + default: docker.io + frappe_repo: + required: false + type: string + default: https://github.com/frappe/frappe + description: "Git URL for the Frappe framework repository" + builder_repository: + required: false + type: string + default: frappe/frappe_docker + description: "Repository that contains the Containerfile and helper scripts" + builder_ref: + required: false + type: string + default: main + description: "Ref to checkout from the builder repository" + platforms: + required: false + type: string + default: linux/amd64 + description: "Docker platforms for the final build" + secrets: + REGISTRY_USERNAME: + required: false + REGISTRY_PASSWORD: + required: false + +permissions: + contents: read + packages: write + +concurrency: + group: app-image-${{ github.repository }}-${{ inputs.app_name }}-${{ inputs.app_ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + BUILDER_DIR: builder + APPS_JSON_PATH: builder/.github/tmp/apps.json + CACHE_SCOPE: app-image-${{ inputs.app_name }}-${{ inputs.frappe_ref }} + TEST_IMAGE: local/${{ inputs.app_name }}:${{ github.run_id }}-${{ github.run_attempt }} + FINAL_IMAGE: ${{ inputs.image_name }}:${{ inputs.image_tag }} + + steps: + - name: Checkout builder repository + uses: actions/checkout@v6 + with: + repository: ${{ inputs.builder_repository }} + ref: ${{ inputs.builder_ref }} + path: ${{ env.BUILDER_DIR }} + + - name: Setup QEMU + uses: docker/setup-qemu-action@v4 + with: + image: tonistiigi/binfmt:latest + platforms: all + + - name: Setup Buildx + uses: docker/setup-buildx-action@v4 + + - name: Create apps.json + env: + APP_REPO: ${{ inputs.app_repo }} + APP_REF: ${{ inputs.app_ref }} + APPS_JSON_PATH: ${{ env.APPS_JSON_PATH }} + run: | + mkdir -p "$(dirname "$APPS_JSON_PATH")" + python3 - <<'PY' + import json + import os + from pathlib import Path + + repo = os.environ["APP_REPO"].strip() + ref = os.environ["APP_REF"].strip() + + if repo.count("/") == 1 and not repo.startswith(("https://", "http://")): + repo = f"https://github.com/{repo}" + + for prefix in ("refs/heads/", "refs/tags/"): + if ref.startswith(prefix): + ref = ref.removeprefix(prefix) + + Path(os.environ["APPS_JSON_PATH"]).write_text( + json.dumps([{"url": repo, "branch": ref}], indent=2) + "\n", + encoding="utf-8", + ) + PY + + - name: Build smoke-test image + uses: docker/build-push-action@v6 + with: + context: ${{ env.BUILDER_DIR }} + file: ${{ env.BUILDER_DIR }}/images/layered/Containerfile + build-args: | + FRAPPE_IMAGE_PREFIX=${{ inputs.frappe_image_prefix }} + FRAPPE_PATH=${{ inputs.frappe_repo }} + FRAPPE_BRANCH=${{ inputs.frappe_ref }} + cache-from: type=gha,scope=${{ env.CACHE_SCOPE }} + cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }} + load: true + platforms: linux/amd64 + secrets: | + id=apps_json,src=${{ env.APPS_JSON_PATH }} + tags: ${{ env.TEST_IMAGE }} + + - name: Smoke test image contents + env: + APP_NAME: ${{ inputs.app_name }} + TEST_IMAGE: ${{ env.TEST_IMAGE }} + run: | + docker run --rm --entrypoint bash "$TEST_IMAGE" -lc \ + "test -d /home/frappe/frappe-bench/apps/frappe && test -d /home/frappe/frappe-bench/apps/${APP_NAME}" + + - name: Login to GHCR + if: ${{ inputs.push && inputs.registry == 'ghcr.io' }} + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Login to target registry + if: ${{ inputs.push && inputs.registry != 'ghcr.io' }} + uses: docker/login-action@v4 + with: + registry: ${{ inputs.registry }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: Push multi-arch image + if: ${{ inputs.push }} + uses: docker/build-push-action@v6 + with: + context: ${{ env.BUILDER_DIR }} + file: ${{ env.BUILDER_DIR }}/images/layered/Containerfile + build-args: | + FRAPPE_IMAGE_PREFIX=${{ inputs.frappe_image_prefix }} + FRAPPE_PATH=${{ inputs.frappe_repo }} + FRAPPE_BRANCH=${{ inputs.frappe_ref }} + cache-from: type=gha,scope=${{ env.CACHE_SCOPE }} + cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }} + platforms: ${{ inputs.platforms }} + push: true + secrets: | + id=apps_json,src=${{ env.APPS_JSON_PATH }} + tags: ${{ env.FINAL_IMAGE }} diff --git a/.github/workflows/build_develop.yml b/.github/workflows/build_develop.yml index 8ec061ed..dfb0ee69 100644 --- a/.github/workflows/build_develop.yml +++ b/.github/workflows/build_develop.yml @@ -1,33 +1,12 @@ -name: Develop build +name: Legacy / Build Develop on: - pull_request: - branches: - - main - paths: - - images/production/** - - overrides/** - - tests/** - - compose.yaml - - docker-bake.hcl - - example.env - - .github/workflows/build_develop.yml - - schedule: - # Every day at 12:00 pm - - cron: 0 0 * * * - workflow_dispatch: jobs: - build: - uses: ./.github/workflows/docker-build-push.yml - with: - repo: erpnext - version: develop - push: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} - python_version: 3.14.2 - node_version: 24.12.0 - secrets: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + delegate: + uses: ./.github/workflows/core-build-develop.yml + permissions: + contents: read + packages: write + secrets: inherit diff --git a/.github/workflows/build_stable.yml b/.github/workflows/build_stable.yml index 174516bc..15438f00 100644 --- a/.github/workflows/build_stable.yml +++ b/.github/workflows/build_stable.yml @@ -1,116 +1,12 @@ -name: Stable build +name: Legacy / Build Stable on: - pull_request: - branches: - - main - paths: - - images/production/** - - overrides/** - - tests/** - - compose.yaml - - docker-bake.hcl - - example.env - - .github/workflows/build_stable.yml - - push: - branches: - - main - paths: - - images/production/** - - overrides/** - - tests/** - - compose.yaml - - docker-bake.hcl - - example.env - - # Triggered from frappe/frappe and frappe/erpnext on releases - repository_dispatch: - workflow_dispatch: jobs: - v15: - uses: ./.github/workflows/docker-build-push.yml - with: - repo: erpnext - version: "15" - push: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} - python_version: 3.11.6 - node_version: 20.19.2 - secrets: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} - - v16: - uses: ./.github/workflows/docker-build-push.yml - with: - repo: erpnext - version: "16" - push: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} - python_version: 3.14.2 - node_version: 24.12.0 - secrets: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} - - update_versions: - name: Update example.env and pwd.yml - runs-on: ubuntu-latest - if: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} - needs: v16 - - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup Python - uses: actions/setup-python@v6 - with: - python-version: "3.14.2" - - - name: Get latest versions - run: python3 ./.github/scripts/get_latest_tags.py --repo erpnext --version 16 - - - name: Update - run: | - python3 ./.github/scripts/update_example_env.py - python3 ./.github/scripts/update_pwd.py - - - name: Push - run: | - git config --global user.name github-actions - git config --global user.email github-actions@github.com - git add example.env pwd.yml - if [ -z "$(git status --porcelain)" ]; then - echo "versions did not change, exiting." - exit 0 - else - echo "version changed, pushing changes..." - git commit -m "chore: Update example.env" - git pull --rebase - git push origin main - fi - - release_helm: - name: Release Helm - runs-on: ubuntu-latest - if: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} - needs: v16 - - steps: - - name: Setup deploy key - uses: webfactory/ssh-agent@v0.10.0 - with: - ssh-private-key: ${{ secrets.HELM_DEPLOY_KEY }} - - - name: Setup Git Credentials - run: | - git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - - - name: Release - run: | - git clone git@github.com:frappe/helm.git && cd helm - pip install -r release_wizard/requirements.txt - ./release_wizard/wizard 16 patch --remote origin --ci + delegate: + uses: ./.github/workflows/core-build-stable.yml + permissions: + contents: read + packages: write + secrets: inherit diff --git a/.github/workflows/build_bench.yml b/.github/workflows/core-build-bench.yml similarity index 95% rename from .github/workflows/build_bench.yml rename to .github/workflows/core-build-bench.yml index 1bc47018..ef0ecbd0 100644 --- a/.github/workflows/build_bench.yml +++ b/.github/workflows/core-build-bench.yml @@ -1,4 +1,4 @@ -name: Bench +name: Core / Build Bench on: pull_request: @@ -7,7 +7,7 @@ on: paths: - images/bench/** - docker-bake.hcl - - .github/workflows/build_bench.yml + - .github/workflows/core-build-bench.yml schedule: # Every day at 12:00 pm diff --git a/.github/workflows/core-build-develop.yml b/.github/workflows/core-build-develop.yml new file mode 100644 index 00000000..64800241 --- /dev/null +++ b/.github/workflows/core-build-develop.yml @@ -0,0 +1,49 @@ +name: Core / Build Develop + +permissions: + contents: read + packages: write + +on: + pull_request: + branches: + - main + paths: + - images/production/** + - overrides/** + - tests/** + - compose.yaml + - docker-bake.hcl + - example.env + - .github/workflows/core-build-develop.yml + - .github/workflows/core-build-test-images.yml + - .github/workflows/core-publish-images.yml + + schedule: + # Every day at 12:00 pm + - cron: 0 0 * * * + + workflow_dispatch: + +jobs: + test: + uses: ./.github/workflows/core-build-test-images.yml + with: + repo: erpnext + version: develop + python_version: 3.14.2 + node_version: 24.12.0 + + publish: + if: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} + needs: test + uses: ./.github/workflows/core-publish-images.yml + with: + repo: erpnext + frappe_version: ${{ needs.test.outputs.frappe_version }} + erpnext_version: ${{ needs.test.outputs.erpnext_version }} + python_version: 3.14.2 + node_version: 24.12.0 + secrets: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/core-build-stable.yml b/.github/workflows/core-build-stable.yml new file mode 100644 index 00000000..6c453adb --- /dev/null +++ b/.github/workflows/core-build-stable.yml @@ -0,0 +1,144 @@ +name: Core / Build Stable + +permissions: + contents: read + packages: write + +on: + pull_request: + branches: + - main + paths: + - images/production/** + - overrides/** + - tests/** + - compose.yaml + - docker-bake.hcl + - example.env + - .github/workflows/core-build-stable.yml + - .github/workflows/core-build-test-images.yml + - .github/workflows/core-publish-images.yml + + push: + branches: + - main + paths: + - images/production/** + - overrides/** + - tests/** + - compose.yaml + - docker-bake.hcl + - example.env + - .github/workflows/core-build-stable.yml + - .github/workflows/core-build-test-images.yml + - .github/workflows/core-publish-images.yml + + # Triggered from frappe/frappe and frappe/erpnext on releases + repository_dispatch: + + workflow_dispatch: + +jobs: + v15_test: + uses: ./.github/workflows/core-build-test-images.yml + with: + repo: erpnext + version: "15" + python_version: 3.11.6 + node_version: 20.19.2 + + v15_publish: + if: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} + needs: v15_test + uses: ./.github/workflows/core-publish-images.yml + with: + repo: erpnext + frappe_version: ${{ needs.v15_test.outputs.frappe_version }} + erpnext_version: ${{ needs.v15_test.outputs.erpnext_version }} + python_version: 3.11.6 + node_version: 20.19.2 + secrets: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + + v16_test: + uses: ./.github/workflows/core-build-test-images.yml + with: + repo: erpnext + version: "16" + python_version: 3.14.2 + node_version: 24.12.0 + v16_publish: + if: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} + needs: v16_test + uses: ./.github/workflows/core-publish-images.yml + with: + repo: erpnext + frappe_version: ${{ needs.v16_test.outputs.frappe_version }} + erpnext_version: ${{ needs.v16_test.outputs.erpnext_version }} + python_version: 3.14.2 + node_version: 24.12.0 + secrets: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + + update_versions: + name: Update example.env and pwd.yml + runs-on: ubuntu-latest + if: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} + needs: v16_publish + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: "3.14.2" + + - name: Get latest versions + run: python3 ./.github/scripts/get_latest_tags.py --repo erpnext --version 16 + + - name: Update + run: | + python3 ./.github/scripts/update_example_env.py + python3 ./.github/scripts/update_pwd.py + + - name: Push + run: | + git config --global user.name github-actions + git config --global user.email github-actions@github.com + git add example.env pwd.yml + if [ -z "$(git status --porcelain)" ]; then + echo "versions did not change, exiting." + exit 0 + else + echo "version changed, pushing changes..." + git commit -m "chore: Update example.env" + git pull --rebase + git push origin main + fi + + release_helm: + name: Release Helm + runs-on: ubuntu-latest + if: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} + needs: v16_publish + + steps: + - name: Setup deploy key + uses: webfactory/ssh-agent@v0.10.0 + with: + ssh-private-key: ${{ secrets.HELM_DEPLOY_KEY }} + + - name: Setup Git Credentials + run: | + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + + - name: Release + run: | + git clone git@github.com:frappe/helm.git && cd helm + pip install -r release_wizard/requirements.txt + ./release_wizard/wizard 16 patch --remote origin --ci diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/core-build-test-images.yml similarity index 64% rename from .github/workflows/docker-build-push.yml rename to .github/workflows/core-build-test-images.yml index 9fee1782..7c118c93 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/core-build-test-images.yml @@ -1,4 +1,4 @@ -name: Build +name: Core / Build and Test Images on: workflow_call: @@ -11,9 +11,6 @@ on: required: true type: string description: "Major version, git tags should match 'v{version}.*'; or 'develop'" - push: - required: true - type: boolean python_version: required: true type: string @@ -22,16 +19,36 @@ on: required: true type: string description: NodeJS Version - secrets: - DOCKERHUB_USERNAME: - required: true - DOCKERHUB_TOKEN: - required: true + outputs: + frappe_version: + description: "Resolved frappe image tag" + value: ${{ jobs.resolve.outputs.frappe_version }} + erpnext_version: + description: "Resolved erpnext image tag" + value: ${{ jobs.resolve.outputs.erpnext_version }} + +permissions: + contents: read jobs: + resolve: + name: Resolve Versions + runs-on: ubuntu-latest + outputs: + frappe_version: ${{ steps.resolve.outputs.frappe_version }} + erpnext_version: ${{ steps.resolve.outputs.erpnext_version }} + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Resolve image versions + id: resolve + run: python3 ./.github/scripts/get_latest_tags.py --repo ${{ inputs.repo }} --version ${{ inputs.version }} + build: name: Build runs-on: ubuntu-latest + needs: resolve services: registry: image: docker.io/registry:2 @@ -57,8 +74,12 @@ jobs: driver-opts: network=host platforms: linux/${{ matrix.arch }} - - name: Get latest versions - run: python3 ./.github/scripts/get_latest_tags.py --repo ${{ inputs.repo }} --version ${{ inputs.version }} + - name: Set resolved versions + run: | + echo "FRAPPE_VERSION=${{ needs.resolve.outputs.frappe_version }}" >> "$GITHUB_ENV" + if [ -n "${{ needs.resolve.outputs.erpnext_version }}" ]; then + echo "ERPNEXT_VERSION=${{ needs.resolve.outputs.erpnext_version }}" >> "$GITHUB_ENV" + fi - name: Set build args run: | @@ -85,17 +106,3 @@ jobs: - name: Test run: venv/bin/pytest --color=yes - - - name: Login - if: ${{ inputs.push }} - uses: docker/login-action@v4 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Push - if: ${{ inputs.push }} - uses: docker/bake-action@v7.1.0 - with: - push: true - set: "*.platform=linux/amd64,linux/arm64" diff --git a/.github/workflows/core-publish-images.yml b/.github/workflows/core-publish-images.yml new file mode 100644 index 00000000..4da6bfee --- /dev/null +++ b/.github/workflows/core-publish-images.yml @@ -0,0 +1,92 @@ +name: Core / Publish Images + +on: + workflow_call: + inputs: + repo: + required: true + type: string + description: "'erpnext' or 'frappe'" + frappe_version: + required: true + type: string + description: "Resolved frappe image tag" + erpnext_version: + required: false + type: string + description: "Resolved erpnext image tag" + python_version: + required: true + type: string + description: Python Version + node_version: + required: true + type: string + description: NodeJS Version + secrets: + DOCKERHUB_USERNAME: + required: true + DOCKERHUB_TOKEN: + required: true + +permissions: + contents: read + packages: write + +jobs: + publish: + name: Publish + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup QEMU + uses: docker/setup-qemu-action@v4 + with: + image: tonistiigi/binfmt:latest + platforms: all + + - name: Setup Buildx + uses: docker/setup-buildx-action@v4 + + - name: Set resolved versions + run: | + echo "FRAPPE_VERSION=${{ inputs.frappe_version }}" >> "$GITHUB_ENV" + if [ -n "${{ inputs.erpnext_version }}" ]; then + echo "ERPNEXT_VERSION=${{ inputs.erpnext_version }}" >> "$GITHUB_ENV" + fi + + - name: Set build args + run: | + echo "PYTHON_VERSION=${{ inputs.python_version }}" >> "$GITHUB_ENV" + echo "NODE_VERSION=${{ inputs.node_version }}" >> "$GITHUB_ENV" + + - name: Login to Docker Hub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Push Docker Hub images + uses: docker/bake-action@v7.1.0 + with: + push: true + set: "*.platform=linux/amd64,linux/arm64" + + - name: Login to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Push GHCR base images + uses: docker/bake-action@v7.1.0 + with: + targets: base-images + push: true + set: "*.platform=linux/amd64,linux/arm64" + env: + REGISTRY_USER: ghcr.io/${{ github.repository_owner }} diff --git a/.github/workflows/publish_docs.yml b/.github/workflows/docs-publish-site.yml similarity index 93% rename from .github/workflows/publish_docs.yml rename to .github/workflows/docs-publish-site.yml index ffecb82c..32a9fb48 100644 --- a/.github/workflows/publish_docs.yml +++ b/.github/workflows/docs-publish-site.yml @@ -1,11 +1,11 @@ -name: Deploy Frappe Docker Docs to GitHub Pages +name: Docs / Publish Site on: push: branches: [main] paths: - "docs/**" - - ".github/workflows/publish_docs.yml" + - ".github/workflows/docs-publish-site.yml" workflow_dispatch: permissions: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 47be9ebb..1b1db5c4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -143,4 +143,4 @@ In case of new release of Debian. e.g. bullseye to bookworm. Change following fi Change following files on release of ERPNext -- `.github/workflows/build_stable.yml`: Add the new release step under `jobs` and remove the unmaintained one. e.g. In case v12, v13 available, v14 will be added and v12 will be removed on release of v14. Also change the `needs:` for later steps to `v14` from `v13`. +- `.github/workflows/core-build-stable.yml`: Add the new release step under `jobs` and remove the unmaintained one. e.g. In case v12, v13 available, v14 will be added and v12 will be removed on release of v14. Also change the `needs:` for later steps to `v14` from `v13`. diff --git a/README.md b/README.md index 6619e4d2..ccf213ab 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Frappe Docker -[![Build Stable](https://github.com/frappe/frappe_docker/actions/workflows/build_stable.yml/badge.svg)](https://github.com/frappe/frappe_docker/actions/workflows/build_stable.yml) -[![Build Develop](https://github.com/frappe/frappe_docker/actions/workflows/build_develop.yml/badge.svg)](https://github.com/frappe/frappe_docker/actions/workflows/build_develop.yml) +[![Build Stable](https://github.com/frappe/frappe_docker/actions/workflows/core-build-stable.yml/badge.svg)](https://github.com/frappe/frappe_docker/actions/workflows/core-build-stable.yml) +[![Build Develop](https://github.com/frappe/frappe_docker/actions/workflows/core-build-develop.yml/badge.svg)](https://github.com/frappe/frappe_docker/actions/workflows/core-build-develop.yml) Docker images and orchestration for Frappe applications. diff --git a/docker-bake.hcl b/docker-bake.hcl index baf7f3b3..cb59f835 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -62,6 +62,10 @@ group "default" { targets = ["erpnext", "base", "build"] } +group "base-images" { + targets = ["base", "build"] +} + function "tag" { params = [repo, version] result = [ diff --git a/docs/08-reference/06-github-actions-image-workflows.md b/docs/08-reference/06-github-actions-image-workflows.md new file mode 100644 index 00000000..85805b64 --- /dev/null +++ b/docs/08-reference/06-github-actions-image-workflows.md @@ -0,0 +1,312 @@ +--- +title: GitHub Actions Image Workflows +--- + +This document describes the current workflow setup for shared core images and reusable downstream app images. + +# Workflow roles + +The current workflow layout is: + +- `.github/workflows/core-build-develop.yml` +- `.github/workflows/core-build-stable.yml` +- `.github/workflows/core-build-test-images.yml` +- `.github/workflows/core-publish-images.yml` +- `.github/workflows/app-build-image.yml` + +`core-build-develop.yml` and `core-build-stable.yml` are orchestration workflows. +They decide when the core image pipeline runs. + +`core-build-test-images.yml` is the reusable workflow that: + +- resolves the image versions for the requested release line +- builds the shared core images into a local registry +- runs the test suite against those images + +`core-publish-images.yml` is the reusable workflow that: + +- publishes the tested images to Docker Hub +- publishes `base` and `build` to GHCR + +`app-build-image.yml` is the reusable workflow that downstream repositories call to: + +- create an `apps.json` file from the caller's app repository and ref +- build `images/layered/Containerfile` +- consume existing `base` and `build` images +- install the requested app into the final image +- optionally push the final app image to the caller's registry + +# Current flow + +The current structure is: + +```text +core orchestration + -> core build and test + -> core publish + +downstream app workflow + -> consume published base and build + -> install app + -> publish final app image +``` + +Current Mermaid overview: + +```mermaid +flowchart TD + subgraph Core["Core image flow"] + A[core-build-develop.yml or core-build-stable.yml] + B[core-build-test-images.yml] + C[Resolve versions] + D[Build local test images] + E[Run pytest] + F[core-publish-images.yml] + G[Push Docker Hub: erpnext, base, build] + H[Push GHCR: base, build] + + A --> B + B --> C + C --> D + D --> E + E --> F + F --> G + F --> H + end + + subgraph App["Downstream app flow"] + I[Downstream repo workflow] + J[app-build-image.yml] + K[Create apps.json] + L[Build images/layered/Containerfile] + M[Install app] + N[Push final app image] + + I --> J + J --> K + K --> L + L --> M + M --> N + end + + G --> J + H --> J +``` + +More concretely: + +```text +core-build-test-images.yml + -> resolves frappe and erpnext tags + -> builds images into a local CI registry + -> runs tests + +core-publish-images.yml + -> pushes Docker Hub: erpnext, base, build + -> pushes GHCR: base, build + +app-build-image.yml + -> pulls: + - /base: + - /build: + -> installs app from app_repo + app_ref + -> pushes final image_name:image_tag +``` + +# Naming convention + +GitHub Actions requires workflow files to stay directly inside `.github/workflows`. +Subdirectories are not supported for workflow files, so structure should come from file names and `name:` values. + +Recommended file naming convention: + +```text +--.yml +``` + +Current examples: + +- `core-build-bench.yml` +- `core-build-develop.yml` +- `core-build-stable.yml` +- `core-build-test-images.yml` +- `core-publish-images.yml` +- `app-build-image.yml` +- `docs-publish-site.yml` + +Recommended visible workflow names: + +- `Core / Build Bench` +- `Core / Build Develop` +- `Core / Build Stable` +- `Core / Build and Test Images` +- `Core / Publish Images` +- `App / Build Image` +- `Docs / Publish Site` + +# Style rules + +To keep workflows predictable, use one convention per category instead of mixing styles. + +Workflow file names should use kebab-case: + +```text +core-build-test-images.yml +app-build-image.yml +``` + +Workflow display names should use short title-style groups: + +```text +Core / Build and Test Images +App / Build Image +``` + +Workflow inputs should use snake_case: + +```yaml +app_name: +frappe_ref: +image_name: +``` + +Environment variables should use upper snake case: + +```text +FRAPPE_IMAGE_PREFIX +PYTHON_VERSION +NODE_VERSION +``` + +The recommended rule set is: + +- workflow file names: kebab-case +- workflow `name:` values: grouped title case +- workflow inputs: snake_case +- job ids and step ids: snake_case where practical +- environment variables: UPPER_SNAKE_CASE + +This means `-` is preferred for file names, while `_` remains appropriate for YAML keys, inputs, and environment variables. + +# Important inputs in `app-build-image.yml` + +The reusable app workflow is controlled mainly by these inputs: + +- `app_name` + The application directory name, for example `crm` +- `app_repo` + The Git repository to install, for example `frappe/crm` +- `app_ref` + The branch or tag to install, for example `develop` +- `frappe_ref` + The tag of the existing `base` and `build` images, for example `version-16` +- `frappe_image_prefix` + Where the shared `base` and `build` images come from, for example `frappe` or `ghcr.io/frappe` +- `image_name` + The final target image name, for example `ghcr.io/acme/crm` +- `image_tag` + The final target image tag, for example `develop` +- `registry` + The registry for the final push, for example `ghcr.io` or `docker.io` + +The key distinction is: + +```text +frappe_image_prefix = source of shared base/build images +image_name = destination of the final app image +``` + +# Example: caller repository publishes to GHCR + +This example assumes: + +- shared base images exist in `ghcr.io/frappe/base` and `ghcr.io/frappe/build` +- the caller repository wants to publish its own app image to `ghcr.io/acme/crm` + +```yaml +name: App / Build CRM Image + +on: + workflow_dispatch: + push: + branches: + - develop + +permissions: + contents: read + packages: write + +jobs: + build-image: + uses: frappe/frappe_docker/.github/workflows/app-build-image.yml@main + with: + app_name: crm + app_repo: acme/crm + app_ref: develop + frappe_ref: version-16 + frappe_image_prefix: ghcr.io/frappe + image_name: ghcr.io/acme/crm + image_tag: develop + registry: ghcr.io + push: true + platforms: linux/amd64 +``` + +What happens: + +```text +1. app-build-image.yml is called +2. apps.json is generated from acme/crm + develop +3. the workflow builds images/layered/Containerfile +4. layered uses: + - ghcr.io/frappe/build:version-16 + - ghcr.io/frappe/base:version-16 +5. CRM is installed +6. the final image is pushed to ghcr.io/acme/crm:develop +``` + +For GHCR, the caller workflow should grant: + +- `permissions: packages: write` + +The reusable workflow then logs in with the workflow token. + +# Example: caller repository publishes to Docker Hub + +This example assumes: + +- shared base images come from Docker Hub under `frappe` +- the caller repository wants to publish its app image to Docker Hub as `acme/crm` + +```yaml +name: App / Build CRM Image + +on: + workflow_dispatch: + push: + branches: + - develop + +jobs: + build-image: + uses: frappe/frappe_docker/.github/workflows/app-build-image.yml@main + with: + app_name: crm + app_repo: acme/crm + app_ref: develop + frappe_ref: version-16 + frappe_image_prefix: frappe + image_name: acme/crm + image_tag: develop + registry: docker.io + push: true + platforms: linux/amd64 + secrets: + REGISTRY_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + REGISTRY_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} +``` + +In this case: + +- shared images are pulled from `frappe/base:version-16` and `frappe/build:version-16` +- the final image is pushed to Docker Hub as `acme/crm:develop` diff --git a/images/layered/Containerfile b/images/layered/Containerfile index 02bf9a20..808f898f 100644 --- a/images/layered/Containerfile +++ b/images/layered/Containerfile @@ -1,6 +1,7 @@ ARG FRAPPE_BRANCH=version-16 +ARG FRAPPE_IMAGE_PREFIX=frappe -FROM frappe/build:${FRAPPE_BRANCH} AS builder +FROM ${FRAPPE_IMAGE_PREFIX}/build:${FRAPPE_BRANCH} AS builder ARG FRAPPE_BRANCH=version-16 ARG FRAPPE_PATH=https://github.com/frappe/frappe @@ -24,7 +25,7 @@ RUN --mount=type=secret,id=apps_json,target=/opt/frappe/apps.json,uid=1000,gid=1 echo "{}" > sites/common_site_config.json && \ find apps -mindepth 1 -path "*/.git" | xargs rm -fr -FROM frappe/base:${FRAPPE_BRANCH} AS backend +FROM ${FRAPPE_IMAGE_PREFIX}/base:${FRAPPE_BRANCH} AS backend USER frappe