Skip to content

Merge pull request #473 from trakrf/feat/tra-956-scan-point-antenna-m… #917

Merge pull request #473 from trakrf/feat/tra-956-scan-point-antenna-m…

Merge pull request #473 from trakrf/feat/tra-956-scan-point-antenna-m… #917

Workflow file for this run

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 }}