Two workflows cover release artifacts:
.github/workflows/publish-staging.yml(display name “Publish (staging)”) — runs on every push tostaging(orworkflow_dispatch)..github/workflows/publish-main.yml(display name “Publish (main)”) — runs on every push tomain. It does not rebuild app images; it promotes existingX.Y.Z-staging.Nimages in GHCR to immutableX.Y.Zand floating:latest, then creates the Git tag and a non-prerelease GitHub Release.
| Git branch | What happens | Immutable image / Git tag pattern | Floating GHCR tag |
|---|---|---|---|
staging |
Full build + push | X.Y.Z-staging.N (N via Git ref API) |
staging |
main |
Promote (crane copy) | X.Y.Z (from root package.json base) |
latest |
Changelog: Both staging prereleases (X.Y.Z-staging.N) and main RTM releases (X.Y.Z) read release notes from docs/development/CHANGELOGS/X.Y.Z.md. Bump the base version at the start of work with scripts/publish/bump-version.sh so the semver changelog file exists immediately, then update that file continuously as work lands.
Promotion: all product changes land on develop. Order: sync-develop-to-staging.sh, then (after a green Publish (staging)) sync-staging-to-main.sh. Do not update main directly from develop; main only advances from staging. There is no beta publish line.
For local CLI and local Docker parity, both Next.js apps use the same runtime-config contract:
RUNTIME_CONFIG_URLis the only required app-process env var.instrumentation.tsprewarms sidecar config when available.- Root layout fetches from the sidecar when
RUNTIME_CONFIG_URLis set (on failure, falls back togetRuntimeConfig()), updatessetRuntimeConfig, and injectsRuntimeConfigScriptfor the browser. getRuntimeConfig()falls back toprocess.envif sidecar config is temporarily unavailable in the current process.
Pre-release image tags use X.Y.Z-staging.N and a floating :staging so clusters can pin the same stream from different GitOps commits. The staging Git branch is the trigger; cluster folder names (e.g. metaboost-alpha) in GitOps stay independent of that name.
| Name | Meaning |
|---|---|
Git branch staging |
Triggers the build-and-push publish workflow; fast-forward from develop (sync-develop-to-staging.sh). |
Git branch main |
Triggers the promote-only workflow; fast-forward from staging (sync-staging-to-main.sh), not directly from develop. |
X.Y.Z-staging.N / :staging |
Image tags (SemVer prerelease + floating tag). Not a cluster or namespace name. |
GitOps metaboost-alpha (example) |
Environment folder/namespace. Independent of the word “staging” in image tags. |
The staging branch is the preprod build line. Default development branch remains develop. When you fast-forward staging from develop (or run Publish (staging) via Run workflow on a chosen ref), the GitHub Action validates, reserves X.Y.Z-staging.N, builds images, pushes to GHCR, verifies tags, creates a matching Git tag, and creates/updates a prerelease GitHub Release from docs/development/CHANGELOGS/X.Y.Z.md.
No Kubernetes manifests are applied from this repo to remote clusters. Clusters consume image pins from your GitOps repository (e.g. Argo CD Application targetRevision, Kustomize newTag).
Step-by-step GitOps file list: METABOOST-PUBLISH-GITOPS-BUMP-CHECKLIST.md.
-
Preprod (staging): fast-forward
stagingfromdevelop:./scripts/publish/sync-develop-to-staging.sh(or a PR with the same result).staginghas no feature commits of its own; it is a mirror ofdevelopat the preprod milestone. -
Build workflow:
.github/workflows/publish-staging.ymlruns on push tostaging, or use Run workflow on a chosen ref. Wait for Publish (staging) to succeed. -
Optional — manual run (staging): GitHub: Actions → Publish (staging) → Run workflow. You can set version_override (e.g.
0.1.2-staging.99) to skip the default atomic auto-increment and reserve a specific tag on the staging line. -
RTM (main): when the staging line is what you want in production, fast-forward
mainfromstaging:./scripts/publish/sync-staging-to-main.sh(or a PR with the same result; do not promotemaindirectly fromdevelop). This push triggers.github/workflows/publish-main.yml(promote toX.Y.Z/:latest, no app rebuild in that run).mainhas no feature commits of its own; it is a mirror ofstaging(and ofdevelopat a later milestone).
When bumping version via scripts/publish/bump-version.sh, the script regenerates the lockfile
under Linux (Docker) before committing so CI gets the correct optional deps. If you add or change
dependencies by hand, run ./scripts/development/update-lockfile-linux.sh and commit the updated
package-lock.json. See Lockfile (Linux).
Six images are built from the Dockerfiles under infra/docker/local/:
- api – Metaboost API
- management-api – Metaboost management API
- web – Next.js web app
- web-sidecar – Runtime-config sidecar for the web app
- management-web – Next.js management web app
- management-web-sidecar – Runtime-config sidecar for the management web app
Each image is tagged with :staging and an immutable
version tag X.Y.Z-staging.N. The base X.Y.Z comes from root package.json (prerelease
stripped). N is selected by the workflow's reserve-version job, which atomically creates
refs/tags/X.Y.Z-staging.N at the workflow commit via the GitHub Git Refs API and increments N
on 422 Reference already exists. Existing Git tags are inspected only as a starting hint to skip
empty N values; correctness comes from the atomic create itself.
GHCR is the storage and verification layer for image tags; it is not used to pick the next N.
Pin clusters with the version tag; use :staging only when you intentionally want
"latest staging."
On first publish where GHCR has no package yet, tag discovery 404 bootstraps at X.Y.Z-staging.0.
The pipeline publishes -staging.N and :staging as described above. GHCR may also list other tags (e.g. from earlier workflows); use the immutable version tag when you need a reproducible pin.
Pushes to main do not run a Docker build for these six app images. The job selects a single X.Y.Z-staging.M (minimum across images of each image’s max N for the package.json base on the commit), crane-copies each image to X.Y.Z and :latest, then creates Git tag X.Y.Z and a non-prerelease GitHub Release. See .github/workflows/publish-main.yml.
For version_override (staging) and for exact reserved tags, 422 Reference already exists is accepted only when
the existing tag already points to the workflow commit SHA. If the existing tag points to a
different commit, the workflow fails before publish-docker starts.
Replace OWNER and REPO with your GitHub org/user and repo name (e.g. myorg/metaboost).
# Pull by staging tag (latest preprod build from the staging branch pipeline)
docker pull ghcr.io/OWNER/REPO/api:staging
docker pull ghcr.io/OWNER/REPO/management-api:staging
docker pull ghcr.io/OWNER/REPO/web:staging
docker pull ghcr.io/OWNER/REPO/web-sidecar:staging
docker pull ghcr.io/OWNER/REPO/management-web:staging
docker pull ghcr.io/OWNER/REPO/management-web-sidecar:staging
# Or by version tag (immutable)
docker pull ghcr.io/OWNER/REPO/api:0.1.2-staging.2
docker pull ghcr.io/OWNER/REPO/management-api:0.1.2-staging.2
docker pull ghcr.io/OWNER/REPO/web:0.1.2-staging.2
docker pull ghcr.io/OWNER/REPO/web-sidecar:0.1.2-staging.2
docker pull ghcr.io/OWNER/REPO/management-web:0.1.2-staging.2
docker pull ghcr.io/OWNER/REPO/management-web-sidecar:0.1.2-staging.2
# After a successful main promote for 0.1.2, you can also pull by RTM or :latest, for example:
# docker pull ghcr.io/OWNER/REPO/api:0.1.2
# docker pull ghcr.io/OWNER/REPO/api:latestFor private repos, authenticate to GHCR first (e.g.
echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin).
- Staging (build + push):
.github/workflows/publish-staging.yml— runs on push tostagingorworkflow_dispatch. Display name: Publish (staging). - Main (promote + RTM):
.github/workflows/publish-main.yml.
The workflows support two tokens for GHCR tag discovery:
GHCR_REGISTRY_TOKEN(recommended): repository secret withpackages:readGITHUB_TOKEN(fallback): built-in token ifGHCR_REGISTRY_TOKENis not set
If GHCR tag listing fails, the staging workflow exits with guidance or use version override on manual dispatch.
Image push uses GITHUB_TOKEN with packages:write in the publish job.
Staging workflow behavior for GHCR tag discovery:
200: normal tag discovery and increment behavior404: first-run bootstrap, starts atX.Y.Z-staging.0401/403: auth or package permission issue (fails with guidance)- Other status codes: unexpected, fail fast
The reserve-version job in
.github/workflows/publish-staging.yml is the source of
truth for build versions on the staging line.
- It reserves the version by creating a Git tag via
POST /git/refsat the workflow commit SHA. - It retries on
422until an unusedNis reserved. - For exact-tag reservations via
version_override, it accepts422only when the tag already resolves to the same commit SHA. git ls-remote --tagsis only a smart-start hint to skip obvious gaps quickly.
This plan set is tracked at .llm/plans/completed/atomic-publish-version-reservation/00-EXECUTION-ORDER.md as historical context.
- This pipeline is publish-first; it does not apply Kubernetes manifests in-cluster or modify a GitOps repo from CI.
infra/k8s/alpha/is scaffold-only; remote overlays live in your GitOps repo.- Prefer immutable
X.Y.Z-staging.N(and matching Git tag on this repo) in overlays;:stagingonly when you want rolling “latest staging.” - Production: use
X.Y.Zand:latestafter a successfulPublish (main)run for that version.
- Tag discovery returns
404: Expected on first publish; bootstraps atX.Y.Z-staging.0. - Tag discovery returns
401or403: CheckGHCR_REGISTRY_TOKENand org package policy. - Emergency republish (staging): Manual dispatch with
version_override.