ci(docker): native arm64 + multi-arch manifest #36
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| # One release at a time; do not cancel one mid-flight. | |
| concurrency: | |
| group: release | |
| cancel-in-progress: false | |
| env: | |
| # Derive the image name from the repository so it tracks a rename automatically | |
| # instead of drifting (the hardcoded name once lagged a rename and published under | |
| # the old name). `github.repository` is `owner/repo`, lowercase as GHCR requires. | |
| IMAGE: ghcr.io/${{ github.repository }} | |
| # OCI namespace for the Helm chart, shared across this owner's charts. The chart is | |
| # pushed as `<owner>/charts/git-cache-proxy`. | |
| CHART_REPO: oci://ghcr.io/${{ github.repository_owner }}/charts | |
| jobs: | |
| release: | |
| name: Release | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| outputs: | |
| released: ${{ steps.release.outputs.released }} | |
| version: ${{ steps.release.outputs.version }} | |
| steps: | |
| # The version-bump commit is pushed directly to `main`, which is protected by a | |
| # branch ruleset that requires status checks. The built-in GITHUB_TOKEN cannot be | |
| # granted a ruleset bypass, so mint a token for a GitHub App that is on the | |
| # ruleset's bypass list and push with that instead. | |
| - name: Mint a token for the release app | |
| id: app-token | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| app-id: ${{ secrets.RELEASE_APP_ID }} | |
| private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} | |
| - uses: actions/checkout@v7 | |
| with: | |
| # Full history and tags so knope can read the commits since the last release. | |
| fetch-depth: 0 | |
| # Push the release commit as the app so it bypasses the branch ruleset. | |
| token: ${{ steps.app-token.outputs.token }} | |
| - uses: knope-dev/action@v2.1.2 | |
| with: | |
| version: 0.23.0 | |
| - name: Configure the release identity | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| # knope bumps the version, updates the changelog, tags, and creates the GitHub | |
| # release from the conventional commits since the last tag. The push uses the app | |
| # token, so - unlike the built-in token - it retriggers this workflow; that rerun | |
| # finds no releasable commits and no-ops via `no_release`, so the publish job runs | |
| # only in the run that actually released. `no_release` is a normal no-op, not a | |
| # failure. | |
| - name: Release | |
| id: release | |
| env: | |
| GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} | |
| run: | | |
| set +e | |
| out="$(knope release 2>&1)" | |
| code=$? | |
| echo "$out" | |
| if [ "$code" -eq 0 ]; then | |
| # Read the version knope just bumped in the working tree. Do NOT derive it | |
| # from `git tag`: with [github] configured, knope creates the tag via the | |
| # GitHub API, so it never appears as a local tag in this checkout, and | |
| # `git tag | head` would return the *previous* release instead. | |
| version="$(grep -m1 '^version = ' Cargo.toml | sed -E 's/.*"(.*)".*/\1/')" | |
| echo "released=true" >> "$GITHUB_OUTPUT" | |
| echo "version=${version}" >> "$GITHUB_OUTPUT" | |
| echo "::notice::Released v${version}." | |
| elif echo "$out" | grep -q 'no_release'; then | |
| echo "::notice::No releasable commits since the last release; nothing to do." | |
| echo "released=false" >> "$GITHUB_OUTPUT" | |
| else | |
| exit "$code" | |
| fi | |
| # Build each arch on its own native runner - QEMU-emulated musl compiles are | |
| # painfully slow - then push tag-less, digest-only images for `publish-manifest` | |
| # to stitch into one multi-arch tag. | |
| publish: | |
| name: Build image (${{ matrix.platform }}) | |
| needs: release | |
| if: needs.release.outputs.released == 'true' | |
| runs-on: ${{ matrix.runner }} | |
| permissions: | |
| contents: read | |
| packages: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: linux/amd64 | |
| runner: ubuntu-latest | |
| - platform: linux/arm64 | |
| runner: ubuntu-24.04-arm | |
| steps: | |
| - name: Sanitize the platform for artifact naming | |
| run: | | |
| platform="${{ matrix.platform }}" | |
| echo "PLATFORM_PAIR=${platform//\//-}" >> "$GITHUB_ENV" | |
| - uses: actions/checkout@v7 | |
| with: | |
| ref: v${{ needs.release.outputs.version }} | |
| - uses: docker/setup-buildx-action@v4 | |
| - uses: docker/login-action@v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push by digest | |
| id: build | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: Dockerfile | |
| platforms: ${{ matrix.platform }} | |
| outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true | |
| cache-from: type=gha,scope=${{ matrix.platform }} | |
| cache-to: type=gha,mode=max,scope=${{ matrix.platform }} | |
| - name: Export the digest | |
| run: | | |
| mkdir -p "${{ runner.temp }}/digests" | |
| digest="${{ steps.build.outputs.digest }}" | |
| touch "${{ runner.temp }}/digests/${digest#sha256:}" | |
| - name: Upload the digest | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: digests-${{ env.PLATFORM_PAIR }} | |
| path: ${{ runner.temp }}/digests/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| publish-manifest: | |
| name: Publish multi-arch manifest | |
| needs: [release, publish] | |
| if: needs.release.outputs.released == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Download the digests | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: ${{ runner.temp }}/digests | |
| pattern: digests-* | |
| merge-multiple: true | |
| - uses: docker/setup-buildx-action@v4 | |
| - uses: docker/login-action@v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Create the manifest list and push | |
| working-directory: ${{ runner.temp }}/digests | |
| run: | | |
| docker buildx imagetools create \ | |
| -t ${{ env.IMAGE }}:${{ needs.release.outputs.version }} \ | |
| -t ${{ env.IMAGE }}:latest \ | |
| $(printf '${{ env.IMAGE }}@sha256:%s ' *) | |
| - name: Inspect the published manifest | |
| run: docker buildx imagetools inspect ${{ env.IMAGE }}:${{ needs.release.outputs.version }} | |
| # Publish the crate to crates.io (which feeds docs.rs) in the same run that cut the | |
| # release, at the tag knope just created. Uses crates.io trusted publishing: GitHub's | |
| # OIDC token is exchanged for a short-lived crates.io token, so no API token secret is | |
| # stored. One-time bootstrap before this can work: publish once manually to claim the | |
| # name (`cargo publish`), then add a GitHub Actions trusted publisher on the crate's | |
| # crates.io settings for this repo and the Release workflow. | |
| publish-crate: | |
| name: Publish crate | |
| needs: release | |
| if: needs.release.outputs.released == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| ref: v${{ needs.release.outputs.version }} | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Authenticate to crates.io | |
| id: auth | |
| uses: rust-lang/crates-io-auth-action@v1 | |
| - name: Publish to crates.io | |
| run: cargo publish --locked | |
| env: | |
| CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} | |
| # Package and push the Helm chart to GHCR as an OCI artifact, in the same run that | |
| # cut the release, at the tag knope just created. The published GHCR package starts | |
| # private; make it public once to allow anonymous `helm pull`. | |
| publish-chart: | |
| name: Publish chart | |
| needs: release | |
| if: needs.release.outputs.released == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| ref: v${{ needs.release.outputs.version }} | |
| - uses: azure/setup-helm@v5 | |
| # Version the chart in lockstep with the app: package at the released version | |
| # (overriding the static Chart.yaml version) with the matching appVersion, so a | |
| # pulled chart deploys the image it was cut with. | |
| - name: Package and push the chart | |
| run: | | |
| helm package chart -d dist \ | |
| --version "${{ needs.release.outputs.version }}" \ | |
| --app-version "${{ needs.release.outputs.version }}" | |
| echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u "${{ github.actor }}" --password-stdin | |
| helm push dist/git-cache-proxy-*.tgz "${{ env.CHART_REPO }}" |