Lesson 5 · Build, deploy & trunk-based delivery

Building & pushing images

Turning source into the one image everything deploys — tagged by the release, built at most once.

Your win: trace how CI builds and pushes the container image — the tool, the registry, the tag scheme — and the neat "don't rebuild what already exists" trick.

One binary, one image, one tag

Recall from Course 1: the whole monorepo compiles to a single build/server binary, and your three services (conversationmgmt, notification, spike) all ride inside it. CI mirrors that — it builds one backend image that every service's Helm subchart deploys. The build is a composite action (Lesson 4).

Anchor — .github/actions/tbd.build-backend/action.yml The image is built with docker buildx via docker/build-push-action@v6 (:102) — not skaffold build (Skaffold is only used at deploy time, Lesson 8). Details:

The "build at most once" trick

Idempotent builds via docker manifest inspect Before building, the action runs docker manifest inspect <image:tag> (tbd.build-backend/action.yml:74-76). If the tag already exists in the registry, the build is skipped. Why it matters: a release tag is immutable, and the staging cron (Lesson 1) may pick the same tag more than once — without this check you'd rebuild an identical image repeatedly. "Is this artifact already built? Then don't rebuild it" is a hallmark of an efficient pipeline.

Build isn't the end — scan, then sign

Anchor — the post-build chain After the image is pushed, two more steps run in the same action: a vulnerability scan (security-scan-image, :121-127) and a binary-authorization attestation (binauthz-sign, :129-135) — a cryptographic stamp that "this image was built by the trusted pipeline." That attestation is the supply-chain story of Lesson 10. So the build stage's real output is: a pushed image + a scan result + a signed attestation.
Why the tag-as-version is elegant Because the image tag equals the release tag, and Helm stamps that tag into the manifests at deploy (global.image.tag, Course 3 L8), there's one version identifier flowing end-to-end: git tag → image tag → deployed pods. To know "what's running," you read one number. To roll back, you deploy an older tag.
Read this next

docker/build-push-action + Skaffold CI/CD

How the build-push action drives buildx in CI, plus Skaffold's build/deploy split.

github.com — docker/build-push-action
skaffold.dev — CI/CD

Check yourself (from memory)

Q1. The backend image is built with…

buildx via docker/build-push-action@v6 with release.Dockerfile. Skaffold is used only at deploy time.

Q2. The image tag is…

be_release_tag — so git tag = image tag = deployed version, one identifier end-to-end.

Q3. docker manifest inspect before building is used to…

If the immutable release tag is already in the registry, don't rebuild it — idempotent builds.
How CI builds & pushes the image — tool, tag, and the skip trick.
recall, then click to reveal
ONE monorepo binary → ONE backend image (all 3 services ride in it) → every service's Helm subchart deploys it. BUILD: tbd.build-backend/action.yml — DOCKER BUILDX via docker/build-push-action@v6 (NOT skaffold build), Dockerfile release.Dockerfile target runner, registry asia.gcr.io/student-coach-e1e95/backend, TAG = the git RELEASE TAG (be_release_tag) → git tag = image tag = deployed version. Cache type=gha. IDEMPOTENT: docker manifest inspect first → SKIP build if the tag already exists. Post-build: vulnerability SCAN → binauthz SIGN (attestation, Lesson 10). Rollback = deploy an older tag.
Want to see release.Dockerfile's stages, or how the GHA layer cache speeds rebuilds? Ask me.

1. In-repo: .github/actions/tbd.build-backend/action.yml; docker/build-push-action.