From 3d68ee82320ed0ad0c8e415f0856d6ba12acdcda Mon Sep 17 00:00:00 2001 From: Zohar Zilberman Date: Thu, 28 May 2026 21:48:15 +0300 Subject: [PATCH 1/3] Split CI into test.yml, build.yml, and release.yml orchestrator Extracts fmt + clippy + test into a reusable test.yml and the multi-arch image build into a reusable build.yml. Both run directly on pull_request (build.yml without pushing). release.yml now triggers only on tag push, calls test.yml and build.yml via workflow_call (build with push:true), and gates merge + GitHub release on both completing. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/build.yml | 101 ++++++++++++++++++++++++ .github/workflows/release.yml | 141 +++------------------------------- .github/workflows/test.yml | 55 +++++++++++++ 3 files changed, 165 insertions(+), 132 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..7147cfd --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,101 @@ +name: ๐Ÿณ Build + +on: + pull_request: + branches: + - main + workflow_call: + inputs: + push: + description: Push images by digest after building + type: boolean + default: false + +env: + DOCKER_REPO: platzio/backend + +permissions: + contents: read + +# One image build per architecture, on a native runner. When called with +# push:true we push by digest; the caller is responsible for assembling +# the multi-arch manifest list. On pull_request we build only, to validate +# the Dockerfile without publishing. +# +# Native arm64 runners avoid QEMU emulation, which dominated build time. +jobs: + image: + name: ๐Ÿณ Build (${{ matrix.platform }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + - platform: linux/arm64 + runner: ubuntu-24.04-arm + steps: + - name: ๐Ÿงท Platform pair + run: | + platform="${{ matrix.platform }}" + echo "PLATFORM_PAIR=${platform//\//-}" >> "${GITHUB_ENV}" + + - name: ๐Ÿ›Ž๏ธ Checkout + uses: actions/checkout@v5 + + - name: ๐Ÿ—ฝ Free disk space + uses: ShubhamTatvamasi/free-disk-space-action@master + + - name: ๐Ÿณ Login to DockerHub + if: inputs.push + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: ๐Ÿ› ๏ธ Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # PR validation: build the image but don't push. Cache scoped per arch so + # the two matrix entries don't clobber each other's GHA cache. + - name: ๐Ÿณ Build (PR validation) + if: ${{ !inputs.push }} + uses: docker/build-push-action@v6 + with: + context: . + build-args: BASE_IMAGE=platzio/base:v8 + platforms: ${{ matrix.platform }} + push: false + cache-from: type=gha,scope=build-${{ env.PLATFORM_PAIR }} + cache-to: type=gha,mode=max,scope=build-${{ env.PLATFORM_PAIR }} + + # Release path: build and push by digest. The caller assembles the final + # tagged manifest from the uploaded digest artifacts. + - name: ๐Ÿณ Build & push by digest + if: inputs.push + id: build + uses: docker/build-push-action@v6 + with: + context: . + build-args: BASE_IMAGE=platzio/base:v8 + platforms: ${{ matrix.platform }} + outputs: type=image,name=${{ env.DOCKER_REPO }},push-by-digest=true,name-canonical=true,push=true + cache-from: type=gha,scope=build-${{ env.PLATFORM_PAIR }} + cache-to: type=gha,mode=max,scope=build-${{ env.PLATFORM_PAIR }} + + - name: ๐Ÿ“ค Export digest + if: inputs.push + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: ๐Ÿ“ฆ Upload digest + if: inputs.push + uses: actions/upload-artifact@v4 + with: + name: digests-${{ env.PLATFORM_PAIR }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3ca3778..b4a5e89 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,6 @@ name: โŽˆ Release Version on: - pull_request: - branches: - - main push: tags: - v** @@ -15,141 +12,22 @@ permissions: contents: read jobs: - fmt: - name: ๐ŸŽจ Check formatting - runs-on: ubuntu-latest - steps: - - name: ๐Ÿ›Ž๏ธ Checkout - uses: actions/checkout@v5 - - - name: ๐Ÿงฐ Install rustfmt - run: rustup component add rustfmt - - - name: ๐ŸŽจ Cargo fmt - run: cargo fmt --all -- --check - test: - name: ๐Ÿงช Run Tests - runs-on: ubuntu-latest - steps: - - name: ๐Ÿ›Ž๏ธ Checkout - uses: actions/checkout@v5 - - - name: ๐Ÿ“ฆ Cargo version - id: cargo - run: | - echo "rustc=$(rustc --version)" >> "${GITHUB_OUTPUT}" - - - name: ๐Ÿชฃ Cache cargo registry - uses: actions/cache@v4 - with: - key: ${{ steps.cargo.outputs.rustc }}-${{ hashFiles('Cargo.lock') }} - restore-keys: | - ${{ steps.cargo.outputs.rustc }} - path: | - ~/.cargo/registry - ~/.cargo/git - target - - - name: ๐Ÿ“Ž Cargo clippy - run: | - cargo clippy --release -- -D warnings - - - name: ๐Ÿ”จ Cargo test - run: | - cargo test --release - - # One image build per architecture, on a native runner. We push by digest; - # a final `merge` job stitches the two digests into a multi-arch manifest list. - # Native arm64 runners avoid QEMU emulation, which dominated build time. - # - # This job does NOT depend on `test`: the test job compiles for the host gnu - # target while these build for musl, so they share no artifacts and gating - # would only serialize clippy+test ahead of the build for no reuse. Tests run - # concurrently instead; the `merge` job below re-gates on `test` so a tagged - # multi-arch image is never published when linting or tests fail. - image: - name: ๐Ÿณ Build (${{ matrix.platform }}) - runs-on: ${{ matrix.runner }} - strategy: - fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - steps: - - name: ๐Ÿงท Platform pair - run: | - platform="${{ matrix.platform }}" - echo "PLATFORM_PAIR=${platform//\//-}" >> "${GITHUB_ENV}" + name: ๐Ÿงช Test + uses: ./.github/workflows/test.yml - - name: ๐Ÿ›Ž๏ธ Checkout - uses: actions/checkout@v5 - - - name: ๐Ÿ—ฝ Free disk space - uses: ShubhamTatvamasi/free-disk-space-action@master - - - name: ๐Ÿณ Login to DockerHub - if: github.event_name == 'push' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: ๐Ÿ› ๏ธ Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - # PR validation: build the image but don't push. Cache scoped per arch so - # the two matrix entries don't clobber each other's GHA cache. - - name: ๐Ÿณ Build (PR validation) - if: github.event_name != 'push' - uses: docker/build-push-action@v6 - with: - context: . - build-args: BASE_IMAGE=platzio/base:v8 - platforms: ${{ matrix.platform }} - push: false - cache-from: type=gha,scope=build-${{ env.PLATFORM_PAIR }} - cache-to: type=gha,mode=max,scope=build-${{ env.PLATFORM_PAIR }} - - # Tag push: build and push by digest. The merge job below assembles the - # final tagged manifest. - - name: ๐Ÿณ Build & push by digest - if: github.event_name == 'push' - id: build - uses: docker/build-push-action@v6 - with: - context: . - build-args: BASE_IMAGE=platzio/base:v8 - platforms: ${{ matrix.platform }} - outputs: type=image,name=${{ env.DOCKER_REPO }},push-by-digest=true,name-canonical=true,push=true - cache-from: type=gha,scope=build-${{ env.PLATFORM_PAIR }} - cache-to: type=gha,mode=max,scope=build-${{ env.PLATFORM_PAIR }} - - - name: ๐Ÿ“ค Export digest - if: github.event_name == 'push' - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - - name: ๐Ÿ“ฆ Upload digest - if: github.event_name == 'push' - uses: actions/upload-artifact@v4 - with: - name: digests-${{ env.PLATFORM_PAIR }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 + build: + name: ๐Ÿณ Build + uses: ./.github/workflows/build.yml + with: + push: true + secrets: inherit merge: name: ๐Ÿงฌ Assemble manifest list - if: github.event_name == 'push' needs: - - image - test + - build runs-on: ubuntu-latest outputs: tag: ${{ steps.tag.outputs.tag }} @@ -188,7 +66,6 @@ jobs: release: name: ๐Ÿš€ Create Release - if: github.event_name == 'push' runs-on: ubuntu-latest needs: - merge diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..09acadf --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,55 @@ +name: ๐Ÿงช Test + +on: + pull_request: + branches: + - main + workflow_call: + +permissions: + contents: read + +jobs: + fmt: + name: ๐ŸŽจ Check formatting + runs-on: ubuntu-latest + steps: + - name: ๐Ÿ›Ž๏ธ Checkout + uses: actions/checkout@v5 + + - name: ๐Ÿงฐ Install rustfmt + run: rustup component add rustfmt + + - name: ๐ŸŽจ Cargo fmt + run: cargo fmt --all -- --check + + test: + name: ๐Ÿงช Run Tests + runs-on: ubuntu-latest + steps: + - name: ๐Ÿ›Ž๏ธ Checkout + uses: actions/checkout@v5 + + - name: ๐Ÿ“ฆ Cargo version + id: cargo + run: | + echo "rustc=$(rustc --version)" >> "${GITHUB_OUTPUT}" + + - name: ๐Ÿชฃ Cache cargo registry + uses: actions/cache@v4 + with: + key: ${{ steps.cargo.outputs.rustc }}-${{ hashFiles('Cargo.lock') }} + restore-keys: | + ${{ steps.cargo.outputs.rustc }} + path: | + ~/.cargo/registry + ~/.cargo/git + target + + - name: ๐Ÿ“Ž Cargo clippy + run: | + cargo clippy --release -- -D warnings + + - name: ๐Ÿ”จ Cargo test + run: | + cargo test --release From 56f943c017001352b98937069b791029b94f8342 Mon Sep 17 00:00:00 2001 From: Zohar Zilberman Date: Fri, 29 May 2026 00:05:31 +0300 Subject: [PATCH 2/3] Unify build step, lift BASE_IMAGE to env, bump action versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - build.yml: collapse the PR-validation / push-by-digest pair into a single Build step with a conditional `outputs:` expression (type=cacheonly on PR, push-by-digest on release) - build.yml: lift the base image tag to a workflow-level BASE_IMAGE env var so bumps live in one place - release.yml: replace the deprecated actions/create-release + actions/upload-release-asset + StuYarrow/publish-release trio with a single softprops/action-gh-release@v3 step - Bump action versions across all three files: actions/checkout @v5โ†’@v6, actions/cache @v4โ†’@v5, actions/upload-artifact @v4โ†’@v7, actions/download-artifact @v4โ†’@v8, docker/login-action @v3โ†’@v4, docker/setup-buildx-action @v3โ†’@v4, docker/build-push-action @v6โ†’@v7 Co-Authored-By: Claude Opus 4.7 --- .github/workflows/build.yml | 37 ++++++++++++----------------------- .github/workflows/release.yml | 34 +++++++------------------------- .github/workflows/test.yml | 6 +++--- 3 files changed, 23 insertions(+), 54 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7147cfd..ec375bf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,6 +13,7 @@ on: env: DOCKER_REPO: platzio/backend + BASE_IMAGE: platzio/base:v8 permissions: contents: read @@ -42,45 +43,33 @@ jobs: echo "PLATFORM_PAIR=${platform//\//-}" >> "${GITHUB_ENV}" - name: ๐Ÿ›Ž๏ธ Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: ๐Ÿ—ฝ Free disk space uses: ShubhamTatvamasi/free-disk-space-action@master - name: ๐Ÿณ Login to DockerHub if: inputs.push - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: ๐Ÿ› ๏ธ Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - # PR validation: build the image but don't push. Cache scoped per arch so - # the two matrix entries don't clobber each other's GHA cache. - - name: ๐Ÿณ Build (PR validation) - if: ${{ !inputs.push }} - uses: docker/build-push-action@v6 - with: - context: . - build-args: BASE_IMAGE=platzio/base:v8 - platforms: ${{ matrix.platform }} - push: false - cache-from: type=gha,scope=build-${{ env.PLATFORM_PAIR }} - cache-to: type=gha,mode=max,scope=build-${{ env.PLATFORM_PAIR }} - - # Release path: build and push by digest. The caller assembles the final - # tagged manifest from the uploaded digest artifacts. - - name: ๐Ÿณ Build & push by digest - if: inputs.push + # On push:true we push by digest and the caller assembles the multi-arch + # manifest from the uploaded digests. On PR (push:false) we discard the + # output (type=cacheonly) โ€” the build itself still validates the + # Dockerfile and warms the per-arch GHA cache. + - name: ๐Ÿณ Build id: build - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . - build-args: BASE_IMAGE=platzio/base:v8 + build-args: BASE_IMAGE=${{ env.BASE_IMAGE }} platforms: ${{ matrix.platform }} - outputs: type=image,name=${{ env.DOCKER_REPO }},push-by-digest=true,name-canonical=true,push=true + outputs: ${{ inputs.push && format('type=image,name={0},push-by-digest=true,name-canonical=true,push=true', env.DOCKER_REPO) || 'type=cacheonly' }} cache-from: type=gha,scope=build-${{ env.PLATFORM_PAIR }} cache-to: type=gha,mode=max,scope=build-${{ env.PLATFORM_PAIR }} @@ -93,7 +82,7 @@ jobs: - name: ๐Ÿ“ฆ Upload digest if: inputs.push - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: digests-${{ env.PLATFORM_PAIR }} path: /tmp/digests/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b4a5e89..ed91f7f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,20 +38,20 @@ jobs: echo "tag=${GITHUB_REF#refs/tags/}" >> "${GITHUB_OUTPUT}" - name: ๐Ÿ“ฅ Download digests - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: /tmp/digests pattern: digests-* merge-multiple: true - name: ๐Ÿณ Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: ๐Ÿ› ๏ธ Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: ๐Ÿงฌ Create manifest list and push working-directory: /tmp/digests @@ -80,29 +80,9 @@ jobs: /root/platz-api openapi schema \ > openapi.yaml - - name: โœจ Create release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - draft: true - tag_name: ${{ needs.merge.outputs.tag }} - release_name: ${{ needs.merge.outputs.tag }} - - - name: ๐Ÿ“ฆ Upload OpenAPI schema - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: openapi.yaml - asset_name: openapi.yaml - asset_content_type: application/vnd.oai.openapi - - name: ๐Ÿš€ Publish release - uses: StuYarrow/publish-release@v1.1.2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: softprops/action-gh-release@v3 with: - id: ${{ steps.create_release.outputs.id }} + tag_name: ${{ needs.merge.outputs.tag }} + name: ${{ needs.merge.outputs.tag }} + files: openapi.yaml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 09acadf..e2b6163 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: ๐Ÿ›Ž๏ธ Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: ๐Ÿงฐ Install rustfmt run: rustup component add rustfmt @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - name: ๐Ÿ›Ž๏ธ Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: ๐Ÿ“ฆ Cargo version id: cargo @@ -36,7 +36,7 @@ jobs: echo "rustc=$(rustc --version)" >> "${GITHUB_OUTPUT}" - name: ๐Ÿชฃ Cache cargo registry - uses: actions/cache@v4 + uses: actions/cache@v5 with: key: ${{ steps.cargo.outputs.rustc }}-${{ hashFiles('Cargo.lock') }} restore-keys: | From 75d226f8c7b4f8758f6d1a88f3b86fd938272d1b Mon Sep 17 00:00:00 2001 From: Zohar Zilberman Date: Fri, 29 May 2026 00:07:07 +0300 Subject: [PATCH 3/3] Drop stale QEMU / native-runner comment from build.yml Co-Authored-By: Claude Opus 4.7 --- .github/workflows/build.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ec375bf..70fc93d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,12 +18,6 @@ env: permissions: contents: read -# One image build per architecture, on a native runner. When called with -# push:true we push by digest; the caller is responsible for assembling -# the multi-arch manifest list. On pull_request we build only, to validate -# the Dockerfile without publishing. -# -# Native arm64 runners avoid QEMU emulation, which dominated build time. jobs: image: name: ๐Ÿณ Build (${{ matrix.platform }})