This document describes how to cut a tagged release. Releases are
triggered by pushing an annotated, GPG-signed vX.Y.Z tag to main,
which fires the Release workflow (.github/workflows/release.yml).
The workflow:
- Verifies that
package.json"version"matches the tag. - Runs
make verify(lint + typecheck + audit + build). - Builds and pushes a multi-arch (
linux/amd64,linux/arm64) image toghcr.io/no42-org/coolmodfiles. - Signs the image (and its per-platform child manifests) with cosign keyless OIDC.
- Creates a draft GitHub release with auto-generated notes.
The release notes draft is then curated by hand and published.
We use SemVer. Tags are formatted vX.Y.Z (no
pre-release or build metadata).
Three places carry the version. Two are derived from the tag at build time; one is committed in the source tree and must be bumped manually before tagging:
| Source | How it's set | Authoritative? |
|---|---|---|
Git tag (vX.Y.Z) |
git tag -s by the releaser |
yes |
| Container image tags | docker/metadata-action from the git tag |
derived |
APP_VERSION env in the image |
--build-arg APP_VERSION=${{ github.ref_name }} |
derived |
package.json "version" |
committed source — bump before tagging | source-of-truth in npm tooling |
The release workflow enforces the alignment with a guard step
("Verify package.json version matches tag"). Pushing a tag whose number
disagrees with package.json aborts the release before anything is
built or signed.
git checkout main
git pull --ff-only
gh run list --branch main --limit 5The latest CI run on main must be success.
# Drops the version into package.json and package-lock.json without
# creating a tag or commit. We commit and tag explicitly below.
npm version X.Y.Z --no-git-tag-versionVerify the diff is exactly the two version fields (one in
package.json, one in package-lock.json at the root "version"
key):
git diff package.json package-lock.jsonUse a chore(release): conventional-commit. The commit must include the
Assisted-by: trailer if AI tooling was used.
git add package.json package-lock.json
git commit -m "chore(release): bump version to X.Y.Z"
git push origin mainIf branch protection requires a PR, open one and merge it before proceeding to the tag step.
The tag must point at the bump commit so the guard step sees the new version:
git pull --ff-only
git tag -s vX.Y.Z -m "$(cat <<'EOF'
vX.Y.Z
Highlights:
- <one bullet per notable change>
EOF
)"
git tag -v vX.Y.Z # confirm signatureThe tag must be GPG-signed (-s). Lightweight or unsigned tags will
not be accepted by downstream consumers that pin on the cosign identity
plus a verified tag.
git push origin vX.Y.ZThis kicks off the Release workflow. Watch it:
gh run watch --workflow release.yml --exit-statusIf the version-guard step fails: the tag is on the wrong commit (a
commit that does not have the bumped package.json). Delete the
local and remote tag, fix the commit history, re-tag.
git push origin --delete vX.Y.Z
git tag -d vX.Y.ZThe workflow leaves a draft GitHub release with auto-generated
notes. Edit it to a curated summary (see v0.4.0 for shape: highlights
on top, then sections for Supply chain, Security, Dependencies, Chore,
and a Container image block).
gh release edit vX.Y.Z --notes-file path/to/notes.md --draft=false --latestcosign verify ghcr.io/no42-org/coolmodfiles:vX.Y.Z \
--certificate-identity-regexp '^https://github\.com/no42-org/CoolModFiles/\.github/workflows/release\.yml@refs/tags/v.*$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.comA successful verification returns one or more signature payloads and prints "The cosign claims were validated".
Same procedure, branched off the affected tag rather than main:
git checkout -b hotfix/X.Y.(Z+1) vX.Y.Z
# ...fix...
npm version X.Y.(Z+1) --no-git-tag-version
git commit -am "chore(release): bump version to X.Y.(Z+1)"
# Open a PR back to main, get it merged, then tag the merge commit
# (or the rebased equivalent) and push as in §4–§5 above.Never tag a hot-fix on a branch that isn't reachable from main —
downstream latest tracking would silently rewind.
- Signed tag — the release identity in the cosign certificate is
the workflow path, but the artefact a human can locally re-verify is
the tag. Signing it prevents a stolen GitHub token from publishing a
release that survives local
git tag -vchecks. package.jsonbump committed before tagging — the npm-side identifier is the only version source that lives in the working tree. Without a manual bump,npm view, registry mirrors, SBOM tooling, and any downstreampackage-lock.jsonsnapshot would all carry a stale version even after the release is cut.- Guard step in CI — protects against forgetting step 2. Fails fast (before the build) so a malformed release doesn't burn cosign signatures or push container tags that have to be torn down.