# CI / CD Pipeline > **NOTE**: image registry path reflects the post-rename state. The pipeline pushes `${REGISTRY_HOST}/azaion/missions:${BRANCH}-arm` (B10 done — AZ-549). ## Source `./.woodpecker/build-arm.yml` (single CI file). One job: build + push the container image. ```yaml when: event: [push, manual] branch: [dev, stage, main] labels: platform: arm64 steps: - name: build-push image: docker environment: REGISTRY_HOST: { from_secret: registry_host } REGISTRY_USER: { from_secret: registry_user } REGISTRY_TOKEN: { from_secret: registry_token } commands: - echo "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" -u "$REGISTRY_USER" --password-stdin - export TAG=${CI_COMMIT_BRANCH}-arm - export BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) - | docker build -f Dockerfile \ --build-arg CI_COMMIT_SHA=$CI_COMMIT_SHA \ --label org.opencontainers.image.revision=$CI_COMMIT_SHA \ --label org.opencontainers.image.created=$BUILD_DATE \ --label org.opencontainers.image.source=$CI_REPO_URL \ -t $REGISTRY_HOST/azaion/missions:$TAG . # post-B10 - docker push $REGISTRY_HOST/azaion/missions:$TAG # post-B10 volumes: - /var/run/docker.sock:/var/run/docker.sock ``` ## Triggers | Trigger | Branch filter | Outcome | |---------|---------------|---------| | `push` | `dev`, `stage`, `main` | Build + push the corresponding `${BRANCH}-arm` image tag | | `manual` | any of `dev`, `stage`, `main` | Same as `push` — used for rebuilding without a code change (e.g., after a base-image security patch) | | `pull_request`, other branches | — | **Not built today.** Feature branches do not produce images | ## Runner / platform - **`labels: platform: arm64`** — the pipeline runs on an ARM64-labeled Woodpecker runner. The build is therefore **native** for the ARM64 image variant (no QEMU). The Dockerfile's `--platform=$BUILDPLATFORM` for the build stage ensures the SDK image matches the runner's architecture. - For the (currently absent) AMD64 variant a second pipeline file (`build-amd.yml`) on an AMD64-labeled runner would be the natural pattern. ## Secrets | Secret | Source | Purpose | |--------|--------|---------| | `registry_host` | Woodpecker secret store | Hostname of the suite's container registry (Caddy-fronted Gitea Container Registry per `../../../suite/_docs/00_top_level_architecture.md`) | | `registry_user` | Woodpecker secret store | Service account that has push rights to `azaion/missions` | | `registry_token` | Woodpecker secret store | Personal access token for the service account | `docker login` is piped via stdin (`--password-stdin`) — token never lands on the command line or in process listings. ## OCI labels emitted Three standard OCI labels are baked into every published image: | Label | Value | Source | |-------|-------|--------| | `org.opencontainers.image.revision` | `$CI_COMMIT_SHA` | Git commit driving this build | | `org.opencontainers.image.created` | `$BUILD_DATE` (ISO 8601 UTC) | `date -u +%Y-%m-%dT%H:%M:%SZ` at build time | | `org.opencontainers.image.source` | `$CI_REPO_URL` | Suite Gitea repo URL | These let `docker inspect` answer "which commit and when?" without consulting the registry's metadata. ## What the pipeline does **NOT** do (carry-forward improvements) - **No `dotnet test` step** — there is no test project in the repo today. Tracked in `../../../suite/_docs/_process_leftovers/2026-04-22_ci-unit-test-lane-missing-projects.md`. When a `tests/Azaion.Missions.Tests/` sibling lands (autodev existing-code Steps 5–7), the natural insert is a `name: test` step that runs `dotnet test --collect "XPlat Code Coverage"` against the build before the docker step. - **No security scan** — neither container scanning (Trivy / Grype on the published image) nor source scanning (CodeQL / Semgrep) is wired. Suite-level concern; out of this Epic. - **No migration check** — the migrator runs at app startup (Flow F6). A CI-time "does the migrator's DDL parse cleanly against an empty PG" smoke test is the next-cheapest safety net once tests exist. - **No SBOM** — `docker buildx --sbom` would surface base-image CVEs at registry-push time. Carry-forward. - **No image-signing** — Notary v2 / cosign is not wired. Carry-forward; suite-wide concern. - **No multi-arch matrix** — only `arm64` builds today (see `containerization.md` § Multi-arch limitation). ## Pipeline run-time characteristics - Single step, single image. Typical wall-clock time: 2–5 minutes on the suite's ARM64 runner (most of it `dotnet publish` + the runtime image layer pull). - No caching layer between builds today. `dotnet restore` re-downloads NuGet packages on every run. A persistent `~/.nuget` volume on the runner would shave ~30 seconds per build. ## Failure modes | Failure | Symptom in Woodpecker | Recovery | |---------|------------------------|----------| | `dotnet publish` fails | Build step fails with compilation errors | Standard — fix source, push again | | Registry login fails (rotated token) | `docker login` exits non-zero | Rotate `registry_token` in Woodpecker secrets | | Registry push rate-limited | `docker push` 429 | Retry the manual run; rare in practice with Gitea-hosted registry | | ARM64 runner offline | Pipeline waits in queue | Bring the runner back online; pipeline auto-resumes |