Releasing
هذا المحتوى غير متوفر بلغتك بعد.
A release is a pushed tag. The gate runs on the maintainer’s machine first; the tag starts
.github/workflows/release.yml,
which runs scripts/release.sh --ci on a GitHub-hosted runner and creates the GitHub release.
The full procedure, with every command and a table of what to do when a stage fails, is the
release runbook. This
page is the map.
Versions and tags
Section titled “Versions and tags”- Git tags are
vX.Y.Z, orvX.Y.Z-<prerelease>such asv1.0.0-rc.1: semver without build metadata, checked exactly byscripts/release/version.mjs. The version must be newer than every otherv*tag, and a pushed tag is never moved. - Image tags drop the
v: alwaysX.Y.Z, plusX.YandXonly when this is the newest final release of that line, so a patch to an old line never moves a newer line’s tags. A prerelease gets its exact tag only. There is never alatest. - The chart’s version is the release’s version.
scripts/release/chart.shpackages the chart with--version X.Y.Z --app-version X.Y.Z; the committedcharts/kept/Chart.yamlstays0.0.0-dev(scripts/check-helm.shfails if it is edited by hand). Each published chart pins one image digest, and the release still refuses a chart version the registry already holds. - Commits follow Conventional Commits; the changelog is built from them.
What a release publishes
Section titled “What a release publishes”In this order, stopping at the first failure (D87, D186, D187):
- a multi-arch image (linux/amd64 and linux/arm64) built from
git archiveof the tagged commit, with an SPDX SBOM and minimal provenance in its index, pushed by digest, with no tag; - each architecture pulled by digest and smoked with
scripts/smoke-image.sh(arm64 natively, amd64 under emulation), and/versionchecked to report this version and commit; - the index digest and every manifest under it signed with cosign, then verified;
- the tags, all pointing at that one signed digest, each verified;
- the Helm chart as an OCI artifact beside the image, its image pinned by the signed digest, signed too;
- the release record
docs/releases/X.Y.Z.md: digests, tags, the chart and the verify commands; - the GitHub release
vX.Y.Z: the changelog section and the record as its notes, withTHIRD-PARTY-NOTICES.txtandX.Y.Z.mdattached, marked a prerelease forX.Y.Z-….
Nothing is tagged until both architectures passed and the signature verified, so a failed run
leaves only untagged digests behind. The default registry is ghcr.io/keptinventory/kept (the
chart under ghcr.io/keptinventory/charts/kept); the runbook records that this path was inferred
from the Dockerfile and is to be confirmed (its step W2).
Cutting one
Section titled “Cutting one”The short version of the runbook’s “Each release”:
-
On
main, clean, at the commit to release, write the changelog section and commit it alone:Terminal window node scripts/changelog.mjs --version X.Y.Z --from vPREVIOUS --write CHANGELOG.mdgit commit -m "chore(release): X.Y.Z" -- CHANGELOG.md -
Wait for the
ciworkflow to pass on that commit on main. It is the gate (D222): every step ofscripts/ci-local.shexcept perf, as required jobs. The release workflow’s preflight refuses a tag whose commit hasn’t passed it. -
Push the tag. The tag push starts the workflow:
Terminal window git tag -a vX.Y.Z -m "Kept X.Y.Z" && git push origin vX.Y.Z -
When the run is green, download the release record into
docs/releases/and commit it. -
Verify from outside, as a user would (verifying a release).
What the workflow does
Section titled “What the workflow does”release.yml runs only on a pushed tag in Kept’s version format, or by hand
(workflow_dispatch) for an existing tag after a failed run. Never on a branch push or a pull
request, so Actions minutes go to releases only. It is one job, release, on the arm64 runner
(ubuntu-24.04-arm) with a 90-minute timeout, and one release runs at a time.
- Preflight, before any build: the tag is a release version; signing is either fully set up or
fully absent (half is refused); an unsigned
1.0.0or later is refused; the GitHub release doesn’t exist yet; the tagged commit has a successfulcirun on main. scripts/release.sh --ci: the stages above. It also refuses a tagged commit that isn’t onorigin/mainand a version or chart version the registry already holds.- The GitHub release, after checking the notes for AI attribution (D173).
- Cleanup, always: the key file and the registry logins are removed.
The layer cache is Actions’ cache, scoped by ref: it helps a re-run of the same tag, not the next
release. The gate is ci.yml, run on every pull request and on main; the release only checks
that it passed for the tagged commit.
The laptop fallback
Section titled “The laptop fallback”When Actions can’t run, bash scripts/release.sh X.Y.Z does the same stages from the laptop, with
the cosign key file (KEPT_COSIGN_KEY, COSIGN_PASSWORD), a GHCR login and gh as the
repository owner (the runbook’s M1–M3). It never commits, tags or pushes: at the end it prints
those steps for you. It needs the pinned tools (bash scripts/tools.sh fetch) and 15 GB free.
Don’t push a tag from a laptop release while release.yml is in the tagged commit unless you mean
the workflow to run as well; it stops at “already published” in its first minutes.
The dry run
Section titled “The dry run”Rehearse the whole pipeline locally, with nothing published outside the machine:
bash scripts/release.sh 0.1.0 --dry-run# or as ci-local's opt-in step:KEPT_RELEASE_DRY_RUN=1 bash scripts/ci-local.sh --from release-dry-runIt builds HEAD from git archive, pushes to a throwaway registry:3.1.2 container on a spare port,
smokes both architectures, signs with a throwaway key pair, tags, publishes and signs the chart,
and must end with a verified signature and no container left behind. ci-local’s
release-dry-run step skips (loudly) unless KEPT_RELEASE_DRY_RUN=1 is set.
Related
Section titled “Related”- Signing: the key, what is signed, and why it isn’t keyless yet.
- The changelog.
- Runbooks and scripts: every ci-local step.
- The 1.0 checklist.