GitHub
Draft — being built (deploy-pipeline P1)

The build-image.yml workflow now exists in internal-portal/.github/workflows/, but hasn't run green end-to-end yet (it needs the terraform from Terraform bring-up applied first). Follow it and report mismatches. Status in .claude/features/deploy-pipeline/tasks.md § P1.

build-image.yml is the only sanctioned path to a production image — once it's green, just publish from a laptop is retired. It's also the org template: every future app repo copies this workflow nearly verbatim.

Prerequisites

Done once, from Terraform bring-up:

  1. The internal/portal ECR repo and the gha-internal-portal IAM role exist (terraform apply in live/prod).

  2. On ShyftSolutions/internal-portal, set a repository variable (not a secret — it's not sensitive) named AWS_GHA_ROLE_ARN to the portal_gha_role_arn terraform output:

    Settings → Secrets and variables → Actions → Variables → New variable
    AWS_GHA_ROLE_ARN = arn:aws:iam::973309142234:role/gha-internal-portal
    

No AWS access keys are stored anywhere — auth is OIDC (below).

What the workflow does

Two jobs. The gate runs the standardized just recipes that every app shares; build-and-push does the CI-specific image plumbing.

push to main / v* tag
        │
        ▼
   gate: just install → just typecheck → just lint
        │  (smoke is deferred — needs a Postgres service; see below)
        ▼
   build-and-push:
     strip dev-bypass route   (Containerfile refuses to build with it present)
     OIDC → assume AWS_GHA_ROLE_ARN
     ECR login
     compute tags (metadata-action)
     buildx build + push  →  linux/amd64 + linux/arm64

OIDC, not keys

permissions: id-token: write lets the job mint a short-lived OIDC token; aws-actions/configure-aws-credentials trades it for temporary STS credentials by assuming AWS_GHA_ROLE_ARN. The role's trust policy (in the gha-oidc-role module) only accepts that exchange from ShyftSolutions/internal-portal on main or a v* tag — a PR from a fork can't assume it.

Why buildx here, not just image

just image / just publish are laptop recipes: single-arch, docker daemon, SSO profile. CI needs multi-arch (amd64 + arm64) and OIDC push, so the workflow uses docker/build-push-action directly. The app-logic gate still goes through just so it's identical to what you run locally.

Tag strategy

Computed by docker/metadata-action. Which tags land depends on the trigger (this is decision D2):

TagWhenMutable?Purpose
sha-<short>every buildno (by discipline)Immutable provenance — exact commit
v<semver>push of a v* tagnoThe release
stablepush of a v* tagyes"Current release" pointer
mainpush to mainyes"Latest main" pointer
On immutability

ECR tag-immutability is repo-wide, and we need :stable to move — so the repo is MUTABLE and immutability of :v*/:sha- is enforced by CI never re-pushing them. The ecr-repo module header notes the IMMUTABLE_WITH_EXCLUSION upgrade path if we want it in metal.

Prod pins an immutable tag (:v<semver> or :sha-<short>); never :stable/:main for a real rollout.

Testing it

# 1. Trigger a main build — push any no-op commit to main (or use the
#    workflow_dispatch button in the Actions tab).
# 2. Watch: Actions tab → build-image → both jobs green.
# 3. Confirm the tags landed:
aws ecr list-images --repository-name internal/portal \
  --region us-east-2 --profile internal \
  --query 'imageIds[].imageTag' --output table
#    expect: sha-<short>, main

# 4. Cut a release tag and confirm the release tags:
git tag v0.3.0 && git push origin v0.3.0
#    expect additionally: v0.3.0, stable

Failure modes

SymptomLikely cause
Not authorized to perform sts:AssumeRoleWithWebIdentityAWS_GHA_ROLE_ARN unset/wrong, or the push ref isn't main/v* (trust policy rejects it)
Error: failed to solve: ... dev-bypassThe dev-bypass strip step didn't run / route re-added — Containerfile refuses prod builds with it present
ECR push deniedRole's push policy doesn't cover this repo ARN, or the ECR repo doesn't exist yet (run terraform)
arm64 build slow/OOMQEMU emulation of arm64 on an amd64 runner is slow; acceptable for now, native arm64 runners are a later optimization

Deferred

The gate job runs typecheck + lint but not just smoke — Playwright needs a Postgres service container and a browser install, which would make the first pipeline flaky. It's a follow-up: add a services: postgres job once the happy path is proven.