diff --git a/.github/workflows/app-build-image.yml b/.github/workflows/app-build-image.yml new file mode 100644 index 00000000..710d31c9 --- /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: Rocket-Quack/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/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