Skip to content
archmax.ai

Releasing

A release is cut by .github/workflows/release.yml. It tags main, writes the GitHub release and pushes the image ghcr.io/archmax-ai/automation. What a release contains and how it is numbered is on Releases; this page is how to cut one.

Label the pull request before you merge it:

Label Release
release a patch release, v1.4.2 → v1.4.3
release:minor a minor release, v1.4.2 → v1.5.0
release:major a major release, v1.4.2 → v2.0.0

Merging it starts the release. A pull request merged without one of these labels releases nothing; its change goes out with the next labelled merge. The labels have to exist in the repository; create them once:

Terminal window
gh label create release --color 0e8a16 --description "Cut a patch release when merged"
gh label create release:minor --color 1d76db --description "Cut a minor release when merged"
gh label create release:major --color b60205 --description "Cut a major release when merged"

Write the pull request’s title for the release notes: they are generated from the titles of the pull requests merged since the previous release.

  1. Plan. Every push to main starts a short planning run. It looks up the merged pull request the pushed commit belongs to and, when that carries a release label, dispatches the release on main with the largest increment among its labels. A push without a labelled pull request, such as a direct push, releases nothing.

  2. Gate. The dispatched run runs the CI workflow itself (ci.yml, called as a reusable workflow) on main’s head, which is the merge commit unless another commit landed first: typecheck, the spec gate, the unit and integration suites against MongoDB and Redis, the workspace build (the documentation site included), the secret scan, and the image build and smoke test on both linux/amd64 and linux/arm64. If any job fails, nothing is tagged.

  3. Release. gh release create vX.Y.Z --target <commit> --generate-notes creates the tag and the GitHub release together. The number comes from .github/scripts/next-release.sh, which reads the newest vX.Y.Z tag; you can run it locally to see what the next number would be:

    Terminal window
    .github/scripts/next-release.sh minor
  4. Image. It builds the image from the new tag, for linux/amd64 and linux/arm64, with APP_VERSION=X.Y.Z, and pushes it as X.Y.Z, X.Y and latest. The build is .github/workflows/image.yml, shared with the unstable and pull request images. Each platform is built natively on a runner of its own, arm64 on GitHub’s arm64 runner, never emulated. The two images are then pushed together under the tags.

Its last job starts the documentation workflow on main, so the site’s header badge shows the new release.

Every job in the repository’s workflows has a time limit of at most 30 minutes, so a step that hangs fails its job rather than running to GitHub’s default of six hours.

Releases run one at a time, so two labelled merges in a row are numbered one after the other; a running release is never cancelled. GitHub keeps only one waiting run behind it, though: a third labelled merge while one release runs and another waits cancels the waiting one. Its change still ships, since the third release is cut from a later main, but with the third pull request’s increment; dispatch a release by hand if the cancelled one asked for more. The gate runs in a concurrency group of its own, beside the ordinary CI run of the same push to main.

Actions → Release → Run workflow, on main, with the increment. Dry run is on by default: the gate runs and the run’s summary names the release number it would cut, and nothing is created. Turn it off to cut the release. A dispatch on any other branch fails before the gate.

Terminal window
gh workflow run release.yml --ref main -f bump=minor -f dry-run=true
  • The gate fails. Nothing was tagged. Fix it on main with another pull request, and label that one.
  • The image fails after the tag. The tag and the GitHub release exist; re-run the run’s failed jobs (image / build (amd64), image / build (arm64) or image / merge). They build from the tag, so the image is still exactly what was tagged. Do not delete the tag to start over: the next run would number the release again.

A deployment picks up a release when its image is pulled; nothing in a running deployment updates itself. Two things can need an operator after the new image runs:

  • A deployment refuses to boot when its sign-in variables don’t fit the mode ARCHMAX_SIGN_IN names (unset in production, ARCHMAX_SIGN_IN=oidc without ARCHMAX_OIDC_CLIENT_ID, an ARCHMAX_AWS_ALB_* variable beside ARCHMAX_SIGN_IN=oidc), naming the variable.

  • A release that changes how trigger deployments are computed, the configuration fingerprint that decides whether a deployment changed, or which trigger strategies are supported reaches existing environments only after the reconciliation tool runs against it:

    Terminal window
    pnpm --filter @archmax/core exec tsx tools/reconcile-triggers.ts --apply

    The pull request that makes such a change says so in its description.

Every push to main also gets an image without a release. When CI finishes on the push and every job passed, .github/workflows/unstable-image.yml builds that commit and pushes it as ghcr.io/archmax-ai/automation:unstable-<sha>, with APP_VERSION=unstable-<sha>. It then moves unstable to the new image. No label is needed, and nothing is tagged.

  • A commit whose CI fails gets no image, and unstable stays on the last one that passed.
  • CI cancels its run of a push to main when another push arrives, so of two merges in quick succession only the second gets an image.
  • unstable only moves forward. A re-run of an older commit’s CI pushes that commit’s unstable-<sha> and leaves unstable where it is.
  • If the build fails, re-run the workflow’s run. It builds the commit CI passed, not main’s head.

The workflow starts on the completion of the workflow named CI, so renaming ci.yml’s name stops it.

Every pull request from a branch of this repository is also built by .github/workflows/pr-image.yml and pushed as ghcr.io/archmax-ai/automation:pr-<number>; the workflow keeps one comment on the pull request with the docker pull line. Pull requests from forks and from Dependabot get no image, because their token cannot write packages; CI still builds their image without pushing it.

.github/workflows/image-cleanup.yml deletes the images of closed pull requests, old unstable images and untagged leftovers. A release’s image (X.Y.Z, X.Y, latest) and any tag it does not name are never deleted.

Image Deleted
pr-<number> when the pull request closes, merged or not; the daily sweep deletes any whose pull request is not open
unstable-<sha> by the daily sweep, all but the newest ten; unstable and its image are kept
untagged images by the daily sweep, once a day old: the images a rebuilt pr-<number> leaves behind

A multi-arch image is deleted together with its platform images and attestations, and a platform image a kept image points to is never deleted. The sweep runs every day at 03:17 UTC. Actions → Image cleanup → Run workflow starts it by hand. Dry run is on by default and logs what would be deleted, without deleting anything.

Terminal window
gh workflow run image-cleanup.yml --ref main -f dry-run=true

The workflow deletes with the repository’s own token, so the package has to grant this repository the Admin role: on the package’s page, Package settings → Manage Actions access. Without it, the cleanup fails and deletes nothing.