Releases & promotion (application repositories)¶
Every other page in this repo starts at the point where an image tag already exists in GHCR. This page covers the step before that: how a change in an application repository becomes a versioned image, who has to approve it, and where responsibility passes to Flux.
It is an inventory of what is configured today, not a tutorial. The mechanics of GitHub
Actions, python-semantic-release, Docker Buildx Bake and Flux image automation are
documented by their own projects and are not restated here:
- Release Workflows — the shared org release action
- GitHub Actions
- python-semantic-release
- Docker Buildx Bake
- Flux image update automation
Scope boundary
This page ends at the moment Release Workflows pushes a semver tag to GHCR. From there,
GitOps with Flux and Kubernetes applications
take over. The release pipeline for platform-infra itself — which is the same
action with the same settings — is documented in
CI/CD & tooling.
Where release automation lives¶
There is no central release pipeline. Each application repository carries its own
.github/workflows/release.yml, and each one calls the same shared org action,
example-org/release-workflows, pinned to a full commit SHA. platform-infra runs the same
workflow for its own versioning.
The reference example is
admin-backend/.github/workflows/release.yml.
It is the most recently updated of the set and carries the fullest configuration; the other
repos are the same workflow at an earlier revision.
What the workflow is configured to do¶
Two Release Workflows steps, selected by event:
| Event | Step | mode |
Configured outcome |
|---|---|---|---|
Pull request (opened, synchronize, reopened) |
PR image build | ci |
Builds a pr-<N> image — only in repos that have a docker-bake.hcl |
Push to staging |
Release | release |
rc prerelease for the staging environment |
Push to main |
Release | release |
Stable version for the prod environment |
workflow_dispatch |
Release | release |
Manual release run from the selected branch |
The release step's inputs, identical across every app repo:
mode: release
deployment-model: bbd
branch-map: '{"staging": "staging", "main": "prod"}'
environments: '["staging", "prod"]'
prerelease-identifiers: '{"staging": "rc"}'
Three consequences of that configuration are worth stating explicitly, because they are the things people ask about:
- The environment is derived from the branch, not chosen. Under
deployment-model: bbdthere is no environment input to set, so a push or a manual dispatch fromstagingcan only ever cut anrc.prodis reachable only frommain. - A repo builds an image only if it has a
docker-bake.hcl. Release Workflows auto-detects the image name from that file. A repo without one runs versioning only — it still tags and bumps, it just publishes nothing to GHCR. - Push events ignore documentation-only changes. Every workflow sets
paths-ignorefor**/*.md,docs/**,LICENSEand.gitignore(plus the docs-site files inadmin-backendandplatform-infra), so a docs change does not cut a version.
admin-required-from: prod¶
admin-backend and public-api set admin-required-from: prod explicitly. Per the
comment in the workflow itself, this is already the Release Workflows default (@last, the last
entry in environments); it is pinned to make the intent visible. It puts the prod
environment behind the action's manual-release admin guardrail. The four repos still on the
earlier revision do not set the input and therefore inherit the same default.
admin-backend and public-api also expose a workflow_dispatch input bump
(auto | patch | minor | major) wired to the Release Workflows force-bump, with auto mapped
to an empty string so the tool decides from commits. The other repos have a bare
workflow_dispatch with no inputs.
Who approves a release¶
Merge is the approval gate. There is no separate release sign-off step.
Each app repo's .github/CODEOWNERS routes every path to one team:
This is the same team and the same single-line pattern used by platform-infra
(see CI/CD & tooling). Present in admin-backend,
ingest-driver, camera-console, status-page and platform-utils.
public-api has no CODEOWNERS file
public-api carries .github/dependabot.yml, release.yml and security.yml, but
no .github/CODEOWNERS on main. Reviews on that repo are therefore not routed to
release-approvers by code ownership. Whether a branch ruleset covers it instead is
to be confirmed — rulesets are a GitHub setting and are not visible in Git.
Not recorded here
The membership of @example-org/release-approvers, and the per-repo branch
protection / ruleset configuration that makes CODEOWNERS review required rather than
merely requested, live in GitHub settings and are not captured in either repository.
Both are to be confirmed and would be worth recording during handover.
Application repository inventory¶
| App repo | Image(s) published | docker-bake.hcl |
staging branch |
Prod branch | Flux app path |
|---|---|---|---|---|---|
admin-backend |
ghcr.io/example-org/backend |
Yes | Yes | main |
kubernetes/apps/backend |
ingest-driver |
ghcr.io/example-org/driver |
Yes | Yes | main |
kubernetes/apps/driver |
public-api |
ghcr.io/example-org/public-api |
Yes | Yes | main |
kubernetes/apps/public-api |
camera-console |
ghcr.io/example-org/camera-console |
Yes | No | main |
kubernetes/apps/camera-console |
status-page |
(none built here) | No | No | main |
kubernetes/apps/status |
platform-utils |
13 bake targets, one image per utility | Yes | Yes | main |
kubernetes/apps/utils |
platform-utils is the one multi-image repo: its bake file declares a default group of
13 targets (camera-probe-propagator, camera-image-size-report,
cluster-capacity-analysis, distance-cache-cleanup, harddisk-hoover, mysql-archival,
capture-exporter, s3-bucket-size, sonic-stragglers-report, watchlist-log-items,
mikrotik-wireguard-exporter, router-fleet-resolver, router-lifetime-reconciler), each
publishing ghcr.io/example-org/<target>. That is a superset of the sub-apps wired in
kubernetes/apps/utils — see Kubernetes applications.
Release Workflows pin per repo¶
| Pin | Repositories |
|---|---|
v2.4.4 (aaaaaaa…) |
admin-backend, public-api |
v2.3.0 (bbbbbbb…) |
ingest-driver, camera-console, status-page, platform-utils |
The app repos are not covered by the platform-infra Dependabot config, which watches only
that repo's own Actions. Whether each app repo runs its own Dependabot for Action SHAs is
to be confirmed (public-api has a dependabot.yml; the others were not checked in
detail).
Staging and prod are independent version lines¶
Under branch-based deployment, staging and main each carry their own semver history.
Staging is not "prod minus one" — the two lines advance separately and can be far apart.
mainproduces stablevX.Y.Ztags, selected by the prodImagePolicy.stagingproducesvX.Y.Z-rc.Ntags, selected by the stagingImagePolicy.
An rc on staging is not a candidate that later "becomes" the prod version; a change
reaches prod only when it is merged to main and main cuts its own stable version.
This is visible in admin-backend today: the prod overlay pins a v2.18.x stable tag while
the staging overlay pins a v2.8.x-rc.N tag — over ten minor versions apart, on two
independent lines. Check the live values rather than trusting any number written down:
git show origin/main:k8s/overlays/prod/kustomization.yaml | grep -A2 '^images:'
git show origin/staging:k8s/overlays/staging/kustomization.yaml | grep -A2 '^images:'
The staging overlay on main is not the deployed staging overlay
Each app repo has a k8s/overlays/staging/ directory on both branches. Flux reads it
from the staging branch, and the ImageUpdateAutomation writes bumps back there. The
copy on main drifts and is not what runs. In admin-backend the two carry different
pinned tags. Always read the staging overlay from origin/staging.
Handoff to Flux¶
the Release Workflows last act is publishing a semver-tagged image to GHCR. Nothing in the app repo deploys anything. The handoff is:
- Release Workflows pushes
ghcr.io/example-org/<image>:vX.Y.Z(or…-rc.N). ImageRepository(inkubernetes/apps/<app>/base/) scans that GHCR repository on a5minterval.ImagePolicy(inoverlays/<env>/) selects the newest tag matching the environment's pattern — stable for prod,-rc.Nfor staging.ImageUpdateAutomation(5minterval) rewrites the# {"$imagepolicy": …}setter marker in the app repo'sk8s/overlays/<env>/kustomization.yamland commits the bump as platform-bot with[ci skip], tomainfor prod andstagingfor staging.GitRepository+ FluxKustomization(5mintervals) reconcile that commit into the cluster.
The full resource-by-resource detail is in GitOps with Flux and the per-app wiring is in Kubernetes applications. Two points worth carrying over here:
[ci skip]on the automation commit is what stops the tag bump from re-triggeringrelease.ymland cutting another version.- Do not hand-edit an image tag in an app repo. The setter automation owns those lines and will overwrite them on its next pass.
Hotfix path¶
The configured path for an urgent production fix — no special mode, no separate workflow:
- Open a PR against the app repo's
mainbranch (release-approversreview applies). - Merge. The push to
mainrunsrelease.yml; Release Workflows cuts a stable patch version from the commit types and publishes the image. - The prod
ImagePolicysees the new tag within roughly 5 minutes (ImageRepositoryscan interval). ImageUpdateAutomationcommits the tag bump back tomain(another ~5m interval).- The Flux
Kustomizationreconciles it into the cluster (5m interval, or force it — see below).
Worst case that is roughly 15 minutes of polling end to end, plus the build. Forcing the reconcile shortens the tail but not the image scan.
Rolling prod back is a roll forward
The prod ImagePolicy selects the newest tag in its range, so removing or moving a
tag does not move prod backwards. Reverting means merging the revert to main and
letting it cut a new, higher stable version. admin-backend carries a hotfix/staging
branch on origin; whether it is live or a remnant is to be confirmed.
How to tell it landed¶
Verification is the standard Flux check set — the commands are in
GitOps with Flux → Operational quick reference
and Kubernetes applications → Operational notes.
In short: flux get image repository / policy / update to see what the automation
selected, flux get kustomizations for reconcile state, and
flux reconcile kustomization <name> -n flux-system --with-source to stop waiting on the
interval.
Reconciliation failures are not pushed anywhere
The Flux notification-controller is installed (it ships with gotk-components.yaml
in both cluster overlays), but there are no Provider or Alert resources anywhere
in this repository. Nothing routes Flux events to Slack or PagerDuty. A failed
reconcile, a stuck ImageUpdateAutomation or a GitRepository that cannot resolve its
branch is discovered only by running the commands above — checking is a pull
operation today. The Slack and PagerDuty paths described in
Alerting & on-call are fed by Prometheus/Alertmanager and
CloudWatch, not by Flux.
Known inconsistencies to resolve¶
Recorded here so they are not rediscovered. None is currently causing a prod outage.
Two apps have no staging branch¶
camera-console and status-page have only main on origin. Verify:
git ls-remote --heads https://github.com/example-org/camera-console.git staging
git ls-remote --heads https://github.com/example-org/status-page.git staging
Both return nothing. Meanwhile the staging Flux wiring in this repo references that branch:
| App | Staging GitRepository file |
ref.branch |
Referenced by the staging overlay kustomization.yaml? |
|---|---|---|---|
camera-console |
kubernetes/apps/camera-console/overlays/staging/gitrepository.yaml |
staging |
No — the overlay lists only flux-kustomization, imagepolicy, imageupdateautomation |
status |
kubernetes/apps/status/overlays/staging/gitrepository.yaml |
staging |
Yes |
Two different failure shapes
For status, the GitRepository is applied and points at a branch that does not
exist, so the source will not resolve and the staging Kustomization has nothing to
apply. For camera-console, the GitRepository is never applied at all — neither the
prod nor the staging overlay kustomization.yaml lists gitrepository.yaml — so its
Flux Kustomization references a GitRepository named camera-console that the overlay
does not create. Resolving this means either creating the staging branches in the two
app repos, or changing the staging references to a branch that exists — plus, for
camera-console, deciding whether the unreferenced gitrepository.yaml files should be
wired in or removed. The camera-console wiring inconsistencies are also listed in
Kubernetes applications.
status-page builds no image¶
status-page has no docker-bake.hcl on main, so its release.yml runs versioning
only. A root Dockerfile is present and the repo has only two workflows (release.yml,
security.yml), neither of which builds it. kubernetes/apps/status/base/imagerepository.yaml
nevertheless scans ghcr.io/example-org/status. What currently produces that image, and
whether the existing tags are historical, is to be confirmed.
Release Workflows pins have drifted apart¶
Four repos are two revisions behind admin-backend and public-api (v2.3.0 vs
v2.4.4). The release contract they configure is the same, so behaviour matches, but the
older four lack the explicit admin-required-from pin and the manual bump input.