This document captures the release workflow. Cutting a release is a single command — pushing a vX.Y.Z Git tag — after which three GitHub Actions workflows do everything else.
We follow Semantic Versioning 2.0.0:
- Major — breaking changes to the input formats supported, the URL state schema, or the public surface of self-hosted dist bundles.
- Minor — new features (parsers, charts, exports) that don't break existing behavior.
- Patch — bug fixes, parser robustness improvements, dependency bumps.
Pre-1.0.0 we keep the 0.x.y line and treat minor bumps as the breaking-change channel — a 0.2.0 may break things that worked under 0.1.x. Bump to 1.0.0 once the input/format contract stabilizes.
| Artifact | Source | Pushed by |
|---|---|---|
dist.zip + dist.zip.sha512 + dist.zip.sigstore attached to GitHub Release |
the vX.Y.Z Git tag |
.github/workflows/release.yml |
ghcr.io/no42-org/blitsbom:rc / :main-<sha> (cosign-signed, SBOM-attested) |
every push to main |
.github/workflows/docker.yml |
ghcr.io/no42-org/blitsbom:X.Y.Z / :X.Y (cosign-signed, SBOM-attested) |
the vX.Y.Z Git tag |
.github/workflows/docker.yml (build-and-push job) |
ghcr.io/no42-org/blitsbom:latest (re-tag of the released :X.Y.Z, same digest, same signature) |
a non-prerelease tag push (skipped for prereleases) | .github/workflows/release.yml dispatches .github/workflows/docker.yml (promote-latest job) via gh workflow run |
Notes:
- GHCR's
:rctag is overwritten on every push tomain. Use:main-<sha>if you need to pin to a specific commit. - GHCR's
:latestis moved byrelease.ymlafter it creates a non-prerelease GitHub Release. The mechanism is aworkflow_dispatchcall: release.yml runsgh workflow run docker.yml --ref <tag> -f promote_tag=<version>, which fires docker.yml'spromote-latestjob at the workflow definition pinned to that tag. (Therelease: [released]event is not used — GitHub suppresses downstream events triggered by GITHUB_TOKEN to prevent recursive workflows, butworkflow_dispatchis exempt.) Net effect: prereleases never move:latest(release.yml skips the dispatch whenprerelease == true), and the sameworkflow_dispatchentry point doubles as a manual emergency-promote affordance —gh workflow run docker.yml --ref v0.2.8 -f promote_tag=0.2.8. Pin to:X.Y.Zfor stable references. Thepromote-latestjob is serialized via aconcurrency: promote-latestgroup so two parallel dispatches (e.g. a hotfix release racing the main release) cannot finish out of order and leave:latestpinned to the older version. make_latest: legacy(passed tosoftprops/action-gh-release) defers the "Latest release" decision to GitHub's own algorithm rather than forcing this release to be Latest. GitHub itself decides which non-prerelease release is "Latest";release.ymlhonors that decision by skipping the promote-latest dispatch entirely for prereleases. If GitHub does not mark the just-published release as Latest (e.g. an older-hotfix scenario), the promote-latest dispatch is still fired with that version'spromote_tag— confirm in the GitHub Releases UI which tag is marked "Latest" before relying on:latestto point at a specific version.- Cosign signing is keyless via Sigstore OIDC — no private keys are stored in the repo or as secrets. Each signature carries a Fulcio short-lived cert whose subject is the GitHub Actions workflow identity (
https://github.com/no42-org/blitsbom/.github/workflows/<workflow>.yml@refs/tags/vX.Y.Z), and the signing event is recorded in the Rekor public transparency log. Thepromote-latestre-tag does not re-sign — cosign signatures bind to the image digest, which is unchanged, socosign verify ghcr.io/no42-org/blitsbom:latestresolves to the same signed digest as:X.Y.Z. (Tag-listing tools such ascrane lswill not show a.sigcompanion next to:latestitself; verification works because cosign resolves the tag to its digest first.) Verification commands are in Verifying the release.
Pre-flight (manual, on main):
git checkout main
git pull --ff-only
make verify # lint + tests + purity check
make size-check # gzipped JS budget (60 KB)
make e2e # full file:// UX check across all sample SBOMsIf everything is green, decide on the next version (MAJOR.MINOR.PATCH) based on what's landed since the last tag. Then:
# Bump package.json (recorded in the bundle for diagnostics).
npm version --no-git-tag-version 0.2.0 # adjust to the chosen version
# Commit the version bump using the conventional-commits style.
git add package.json package-lock.json
git commit -m "chore(release): v0.2.0"
# Create the annotated tag and push everything.
git tag -a v0.2.0 -m "v0.2.0"
git push origin main
git push origin v0.2.0Pushing the tag fires both release.yml (GitHub Release + dist.zip + checksum + Sigstore bundle) and docker.yml (:0.2.0, :0.2 in GHCR — note: not yet :latest). When the tag is not a prerelease, release.yml's final step calls gh workflow run docker.yml --ref v0.2.0 -f promote_tag=0.2.0, which fires a third workflow run — docker.yml's promote-latest job — that re-tags the already-built :0.2.0 image as :latest via docker buildx imagetools create (no rebuild, same digest, same cosign signature). The corresponding push to main from the version-bump commit fires docker.yml separately for the :rc tag.
After the workflows turn green:
- GitHub Release — open https://github.com/no42-org/blitsbom/releases/latest; verify
dist.zip,dist.zip.sha512, anddist.zip.sigstoreare all attached and the auto-generated release notes look reasonable. Spot-check the checksum and the Sigstore signature:gh release download --pattern 'dist.zip*' sha512sum -c dist.zip.sha512 cosign verify-blob dist.zip \ --bundle dist.zip.sigstore \ --certificate-identity-regexp '^https://github\.com/no42-org/blitsbom/\.github/workflows/release\.yml@refs/tags/v' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com
- GHCR images — pull and run, sanity-check it loads:
Then load one of the sample SBOMs end-to-end and confirm the donut + table render.
docker run --rm -p 8080:80 ghcr.io/no42-org/blitsbom:0.2.0 open http://localhost:8080
- Container signature + SBOM attestation — verify the image is signed by this repo's release workflow, and pull the attached CycloneDX SBOM:
The
IMAGE=ghcr.io/no42-org/blitsbom:0.2.0 cosign verify "$IMAGE" \ --certificate-identity-regexp '^https://github\.com/no42-org/blitsbom/\.github/workflows/docker\.yml@refs/tags/v' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com cosign verify-attestation "$IMAGE" \ --type cyclonedx \ --certificate-identity-regexp '^https://github\.com/no42-org/blitsbom/\.github/workflows/docker\.yml@refs/tags/v' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ | jq -r '.payload | @base64d | fromjson | .predicate' > sbom.cdx.json
:rcand:main-<sha>images carry the same signature shape but with@refs/heads/maininstead of@refs/tags/v…— adjust the--certificate-identity-regexpaccordingly when verifying those.
For a patch release off the latest tag (e.g. v0.2.0 is broken; we want v0.2.1):
git checkout -b hotfix/0.2.1 v0.2.0
# fix the bug, commit
git checkout main
git merge --no-ff hotfix/0.2.1
git push origin main
# then cut v0.2.1 the normal way (npm version + tag + push)If you need to abandon a broken tag entirely, delete it rather than re-pointing — moving an already-fetched tag is unkind to anyone who pinned to it:
git push origin :refs/tags/v0.2.0
gh release delete v0.2.0 --yes --cleanup-tag
# Optionally clean up the GHCR tag manually via the package settings.Then cut a new patch version.
docker.ymlfails on first run with "denied: permission_denied" — the GHCR package didn't exist yet and is private by default. After the first successful publish, set the package visibility topublicunder Repository → Packages → blitsbom → Package settings, or grant the workflowpackages: write(already configured here).size-checkfails locally but passes in CI — localnode_modulesmay be stale. Runmake clean && make install && make size-check.- Sample-SBOM e2e times out — the
opennms-corefixture (~29 MB, 2839 components) is intentionally aggressive. If it regresses, look at recent changes to the parser or the store's$state.rawpatterns.
Per the project's Conventional Commits policy, release plumbing uses these scopes:
chore(release): vX.Y.Z— the version-bump commit before the tag.ci: …— workflow or pipeline changes.docs(releasing): …— updates to this file.
Every Claude-assisted commit must include the Assisted-by: trailer; never include Signed-off-by for AI-generated changes (only humans certify the DCO).