<service>/vx.y.z tag from main causes
Depot CI to build the image, push it to GHCR, and cut a GitHub release.
Promotion through canary into production then happens in the
unkeyed/infra repo. For how the image
itself is built, see Builds.
Design choices worth knowing
- One image per service, not a monolith. Each service gets its own
GHCR repository (
ghcr.io/unkeyed/<service>) and its own tag namespace (<service>/vx.y.z). Services release independently; bumpingapidoes not force a release ofvault. Helm charts and promotion pins are also per-service for the same reason. - Tags must be on
main. Releases ship code that has already passed CI. The release workflow refuses to publish anything for a tag whose commit is not reachable fromorigin/main, so feature branches can never become a release. - The release workflow does not re-test. Tests ran when the code merged
to
main. The release job’s only responsibilities are build, push, and cut a GitHub release. - Tags are immutable. Once published, a tag (and its image) sticks around as historical record. Bad releases are addressed by forward-fix tags, never by deleting or re-pushing the broken one.
Releasing a service
The unkey side is two steps: tag the service, then wait for CI to publish the image. The infra side (canary rollout, production promotion, rollback) lives in the infra deploy guide and picks up from there.1. Tag the service from main
Sync main, then tag and push:
api, frontline, vault, heimdall,
krane, control-api, control-worker. Use semantic versioning and never
reuse a tag.
Pre-release tags
Tags with a SemVer pre-release suffix (anything after a hyphen, e.g.api/v1.2.3-beta, vault/v0.4.0-rc.1, heimdall/v2.0.0-alpha.3) are
supported. They build and push the image the same way and produce a GitHub
release marked as a pre-release so it does not show up as “Latest”. Use them
when you want a real, immutable artifact to canary in infra without claiming
it as a stable version. The same rules still apply: the tag must be on
main, and tags are immutable, so iterate by bumping the pre-release suffix
(-rc.1 -> -rc.2) rather than re-pushing.
2. Wait for CI
depot ci run list --repo unkeyed/unkey --trigger push --status running
shows the in-flight workflow. When it finishes, confirm both artifacts exist
before moving on:
- The image at
ghcr.io/unkeyed/<service>:v<x>.<y>.<z>(visible in the GitHub Packages tab, or viadocker manifest inspect). - A GitHub release at
releases/tag/<service>/v<x>.<y>.<z>.
main and cut a new
patch tag. Do not delete and re-push the failed one.
After CI: deploy in unkeyed/infra
Follow the
infra deploy guide.
Two things to be aware of for per-service releases: helm values live at
eks-cluster/helm-chart/<service>/values.yaml with
image.repository: ghcr.io/unkeyed/<service>, and each service has its own
production promotion pin under
eks-cluster/promotions/production001/<service>.yaml.
CLI releases
The CLI ships through a separate path because it is an npm package, not a container image.cli/vx.y.z tags trigger .github/workflows/release.yaml,
which uses GoReleaser to build the CLI binaries and publish to npm. The
GoReleaser config is scoped to the cli/ tag prefix, so a service tag
never produces CLI artifacts and vice versa.

