Merge pull request #473 from trakrf/feat/tra-956-scan-point-antenna-m… #917
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: Docker Build and Push | |
| on: | |
| pull_request: | |
| push: | |
| # `preview` is the PR-stacked composition built by sync-preview.yml. It is | |
| # force-rewritten on every PR open/sync/close, so expect frequent builds — | |
| # intended. Each push publishes two tags: | |
| # - `ghcr.io/trakrf/backend:sha-<short>` — immutable, for traceability | |
| # - `ghcr.io/trakrf/backend:preview` — floating, for the GKE | |
| # auto-track (TRA-483, ArgoCD Image Updater w/ digest strategy) | |
| # The `latest` tag stays gated on the default branch; the floating | |
| # `preview` tag is gated on `refs/heads/preview`. Prod uses semver tags | |
| # — no path for `preview` to promote into a stable ref. | |
| branches: [main, preview] | |
| concurrency: | |
| group: docker-build-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| permissions: | |
| contents: read | |
| packages: write | |
| env: | |
| REGISTRY_IMAGE: ghcr.io/trakrf/backend | |
| # Multi-arch strategy (TRA-909): fork into native per-arch builds, then merge a | |
| # manifest — NO per-arch tags. The `build` matrix builds linux/amd64 on a | |
| # standard amd64 runner and linux/arm64 on the ARM runner in parallel, each | |
| # pushing by digest (no tag). The `merge` job stitches the digests into one | |
| # multi-arch tag with `docker buildx imagetools create`. Wall clock is | |
| # ~max(amd64, arm64), not the sum, and we avoid QEMU emulation entirely (the | |
| # ~20-min cycle-time hit that originally drove the arm64-only switch). Native | |
| # builds also keep CGO a non-issue — though the Dockerfile already pins | |
| # CGO_ENABLED=0, so the server binary is fully static pure-Go regardless. | |
| # | |
| # Downstream tags stay byte-correct for free: promote-prod.yml and | |
| # sync-preview.yml both manipulate the manifest (imagetools create / branch | |
| # composition), never rebuild, so the multi-arch index propagates to :prod and | |
| # :preview without changes. | |
| jobs: | |
| build: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: linux/amd64 | |
| runner: ubuntu-24.04 | |
| arch: amd64 | |
| - platform: linux/arm64 | |
| runner: ubuntu-24.04-arm | |
| arch: arm64 | |
| runs-on: ${{ matrix.runner }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| # Full history + tags so `git describe` can resolve the platform | |
| # version (TRA-485). Shallow clones drop tag refs and ancestor | |
| # commits, leaving describe with nothing to report. | |
| fetch-depth: 0 | |
| - name: Compute platform version | |
| id: version | |
| run: | | |
| # --match 'v*' so only release tags (v1.1.0, v1.1.1, ...) anchor the | |
| # version. Ad-hoc snapshot tags like `pre-tra-720` would otherwise win | |
| # by recency and produce a misleading version string (TRA-851). | |
| version=$(git describe --tags --match 'v*' --always --dirty) | |
| # On preview-branch builds, append the list of PR numbers currently | |
| # merged into preview, sourced from the .preview-manifest file written | |
| # by sync-preview.yml. Result: `v1.1.1-N-g<sha>-preview+419+420` so a | |
| # quick refresh of the preview env tells you which PRs are in it. | |
| if [[ "${GITHUB_REF}" == "refs/heads/preview" && -s .preview-manifest ]]; then | |
| prs=$(tr '\n' '+' < .preview-manifest | sed 's/+$//') | |
| if [[ -n "${prs}" ]]; then | |
| version="${version}-preview+${prs}" | |
| fi | |
| fi | |
| echo "version=${version}" >> "$GITHUB_OUTPUT" | |
| echo "Platform version: ${version}" | |
| - name: Set up Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to GHCR | |
| if: github.event_name != 'pull_request' | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract metadata | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ env.REGISTRY_IMAGE }} | |
| # `preview` is given priority=50 (below `sha`'s default 100) so the | |
| # immutable `sha-<short>` remains the primary version output — that | |
| # keeps BUILD_TAG/VITE_BUILD_TAG pinned to a real commit instead of | |
| # the floating label, so /health and /version.json stay honest. | |
| tags: | | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| type=raw,value=preview,enable=${{ github.ref == 'refs/heads/preview' }},priority=50 | |
| type=sha,prefix=sha-,format=short | |
| - name: Build and push by digest | |
| id: build | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: ./Dockerfile | |
| target: production | |
| platforms: ${{ matrix.platform }} | |
| # Push by digest only — no tag here. The merge job applies the tags | |
| # to the assembled manifest list. `push=false` on pull_request keeps | |
| # PRs build-only (validates both arches compile, nothing published). | |
| outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }} | |
| # Plain single-platform manifests merge cleanly. Provenance/SBOM | |
| # attestations would wrap each digest in an image index, which | |
| # imagetools create then nests — yielding bogus unknown/unknown | |
| # platform entries in the final tag. Disable so each digest is a | |
| # single OCI manifest. | |
| provenance: false | |
| labels: ${{ steps.meta.outputs.labels }} | |
| # Per-arch cache scopes — the two matrix legs run concurrently and | |
| # would clobber a shared gha cache otherwise. | |
| cache-from: type=gha,scope=backend-${{ matrix.arch }} | |
| cache-to: type=gha,mode=max,scope=backend-${{ matrix.arch }} | |
| # Inject build metadata so /health and frontend /version.json report | |
| # the deployed commit. BUILD_TAG uses the primary ref tag produced | |
| # by metadata-action (sha-xxxx on commits, latest on default branch). | |
| # Dockerfile propagates these to the frontend stage as VITE_* env. | |
| # The environment banner is NOT baked here — it's runtime-driven from | |
| # the backend's ENVIRONMENT_LABEL env var (TRA-853), so one image | |
| # serves every environment. | |
| build-args: | | |
| COMMIT_SHA=${{ github.sha }} | |
| BUILD_TAG=${{ steps.meta.outputs.version }} | |
| APP_VERSION=${{ steps.version.outputs.version }} | |
| - name: Export digest | |
| if: github.event_name != 'pull_request' | |
| run: | | |
| mkdir -p /tmp/digests | |
| digest="${{ steps.build.outputs.digest }}" | |
| # The artifact only needs the digest in its name; the file is an | |
| # empty marker the merge job globs to reconstruct the @sha256 refs. | |
| touch "/tmp/digests/${digest#sha256:}" | |
| - name: Upload digest | |
| if: github.event_name != 'pull_request' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| # Distinct per-arch names — upload-artifact@v4 names are immutable, so | |
| # the two legs cannot share one artifact. merge job globs `digest-*`. | |
| name: digest-${{ matrix.arch }} | |
| path: /tmp/digests/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| # Required-status-check shim. The branch ruleset requires a context named | |
| # exactly `build`, but the matrix above emits `build (linux/amd64, ...)` / | |
| # `build (linux/arm64, ...)` — the bare `build` context matches neither, so | |
| # without this every PR would be permanently BLOCKED. This re-exports a single | |
| # `build` check that's green only when every matrix leg succeeded. It's | |
| # independent of the matrix dimensions, so changing runner labels or arches | |
| # never forces a ruleset edit. | |
| build-complete: | |
| name: build | |
| needs: build | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Verify all arch builds succeeded | |
| run: | | |
| if [ "${{ needs.build.result }}" != "success" ]; then | |
| echo "::error::One or more arch builds did not succeed (result: ${{ needs.build.result }})" | |
| exit 1 | |
| fi | |
| merge: | |
| # No manifest to merge on PRs — the build job already validated both arches. | |
| if: github.event_name != 'pull_request' | |
| runs-on: ubuntu-latest | |
| needs: build | |
| steps: | |
| - name: Download digests | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: /tmp/digests | |
| pattern: digest-* | |
| merge-multiple: true | |
| - name: Set up Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract metadata | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ env.REGISTRY_IMAGE }} | |
| # Same tag rules as the build job — metadata-action derives them from | |
| # the github context (ref, sha), so no checkout is needed here. | |
| tags: | | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| type=raw,value=preview,enable=${{ github.ref == 'refs/heads/preview' }},priority=50 | |
| type=sha,prefix=sha-,format=short | |
| - name: Create manifest list and push | |
| working-directory: /tmp/digests | |
| env: | |
| DOCKER_METADATA_OUTPUT_JSON: ${{ steps.meta.outputs.json }} | |
| run: | | |
| # Stitch the per-arch digests into one multi-arch tag set. The `-t` | |
| # flags come from metadata-action's tag list; the trailing args are | |
| # the digest refs (one empty marker file per arch in this dir). | |
| # SC2046: the word-splitting on both $(...) is deliberate — each | |
| # expands to multiple separate CLI args. | |
| # shellcheck disable=SC2046 | |
| docker buildx imagetools create \ | |
| $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ | |
| $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) | |
| - name: Inspect image | |
| run: | | |
| docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }} |