Releasing
A TRUSCA release is driven entirely by pushing a vX.Y.Z git tag. The
.github/workflows/release.yml
workflow does the rest. The design goal is that no release is ever announced
before it has been proven installable from the exact images users will pull.
The gate at a glance
The workflow runs four stages in order, each depending on the previous one:
build— builds each image (trusca-backend,trusca-backend-worker,trusca-frontend) on native amd64 + arm64 runners and pushes them to GitHub Container Registry by digest.merge— assembles a multi-arch manifest list per image and applies the version tags (X.Y.Zimmutable,X.Ymovable — never:latest).release— creates the GitHub Release as a draft. Notes come fromdocs-site/docs/release-notes/X.Y.Z.mdwhen present, otherwise from GitHub's auto-generated notes. The job also generates a CycloneDX SBOM of the release's own source tree (syft) and attaches it as a Release asset (trusca-X.Y.Z.cdx.json) — an SCA product ships its own SBOM.release-gate— pulls the freshly publishedX.Y.Zimages, boots the productiondocker-compose.yml(with the smalldocker-compose.smoke.ymloverlay that publishes the backend + frontend ports so the smoke can run without Traefik/DNS/TLS), and runs the documented Quickstart first-scan smoke: health poll →create_super_admin→ login → projects API. On success it runsgh release edit <tag> --draft=false --latestto reveal the Release.
build ──▶ merge ──▶ release (draft) ──▶ release-gate ──▶ reveal (draft=false)
push tag GitHub Release pull + boot public + latest
by version stays hidden published images only if smoke
digest tags + first-scan smoke passed
Why images publish before the Release is revealed
The container images are published in build + merge, before the Release
exists. This is deliberate: the gate can only prove a release is installable by
pulling and running the actual published images the way an operator would. The
Release is the human-facing announcement, so it is held back — as a draft —
until that proof succeeds.
Failure semantics
If any release-gate step fails, the reveal step is skipped (it has no
if: always() guard, so it only runs on the success path). The result is:
- The image tags stay published and pullable.
X.Y.ZandX.Ywere pushed in themergestage and are not rolled back. An operator can still pull them, and a re-run of the workflow reuses them. - The GitHub Release stays a draft. It is not visible on the Releases page,
is not marked
latest, and does not notify watchers. Nothing announces a release whose images failed to boot.
To recover, fix the underlying problem and re-run the workflow for the same tag
(or dispatch it manually with the tag input). The release job is idempotent:
it leaves an existing draft untouched, and release-gate re-pulls the same
published images and re-runs the smoke. Only when the smoke passes does the
draft flip to public.
If you have independently verified a release whose gate is failing for an
unrelated (e.g. infrastructure) reason, a maintainer can reveal it by hand with
gh release edit vX.Y.Z --draft=false --latest. Prefer fixing the gate.
Cutting a release
- Refresh the vendored endoflife.date snapshot so the release ships current
lifecycle data (EOL verdicts are stamped offline from this file):
python3 scripts/refresh_eol_snapshot.pyfromapps/backend, and commit the updated snapshot with the release-prep changes. - Documentation sweep — the release ships its docs, so before tagging:
- Write the release notes at
docs-site/docs/release-notes/X.Y.Z.md(EN + KO mirror, wired intosidebars.ts), sourced from the[Unreleased]section ofCHANGELOG.md— then move those entries under a new[X.Y.Z]heading. - Walk the
[Unreleased]entries once more and confirm every user-facing feature also landed in the relevant guide page (user-guide / admin-guide / ci-integration), not only in the release notes. A feature without a guide section is a release blocker — this is the "docs accompany features" rule enforced at the moment it is cheapest to fix. - If a new UI surface shipped, capture its screenshot via
make screenshots-captureand reference it from the guide section.
- Write the release notes at
- Bump
IMAGE_TAGin.env.exampletoX.Y.Z. - Tag and push:
git tag vX.Y.Z && git push origin vX.Y.Z. - Watch the
release-gatejob. When it goes green the Release is public and markedlatestautomatically — no manual step is needed.
See also
- Getting started — dev stack, first PR.
- Install with Docker Compose — the operator path the gate exercises.
- Quickstart — the first-scan scenario the gate smoke mirrors.