Chart releases are tag-driven and decoupled from application releases:
the vX.Y.Z tag publishes the chart, then a GitHub release documents
it for humans.
-
Land everything for the release on
mainvia PR with CI green. -
Set the chart version.
charts/blittermib/Chart.yamlversion:must equal the release being cut — add achore: set chart version X.Y.Zcommit if it doesn't.appVersionnames the application release the chart is tested against; it is bumped deliberately by PR, never as part of tagging. Runmake docsin the same commit — the README's version badges are generated from exactly these two fields and are packaged into the (immutable) published tarball, so a bump without the regen ships stale badges forever (this bit v0.5.5). -
Tag and push — this is the publication trigger:
git tag -a vX.Y.Z -m "blittermib chart X.Y.Z — <one-line summary>" git push origin vX.Y.Zrelease.ymlre-runs the full quality gates, packages the chart (version stamped from the tag), pushes it tooci://ghcr.io/no42-org/charts, cosign-signs the digest, attests SLSA build provenance, and creates a draft GitHub release with the tarball, an SBOM, and signed checksums attached.The SBOM's subject is the application image the chart renders by default (
ghcr.io/no42-org/blittermib:<appVersion>), not the chart. A chart has no dependency graph — syft finds exactly one synthetic package in it, which tells an operator nothing thatchecksums.txtdoesn't. The image is what actually runs, so that is what gets a bill of materials. It followsappVersionautomatically, so an appVersion bump re-scans without anyone remembering to.Watch it:
gh run watch $(gh run list --workflow=release.yml --limit 1 --json databaseId -q '.[0].databaseId'). -
Publish the draft with a concise, curated note. The workflow already created the release and attached the artifacts — write the note and flip it out of draft, never create it by hand:
gh release edit vX.Y.Z --notes-file <notes.md> --draft=false
Note guidelines — curate, don't generate:
- Lead with what an operator sees: behavior changes first, then new values keys, then internals. One bullet per change, link the PR.
- Name new or changed
values.yamlkeys verbatim. - Call out the
appVersionfloor whenever it moves, and any compatibility constraint (e.g. "requires app ≥ X"). - Mark breaking changes BREAKING at the top.
- Include the install one-liner for the new version.
- No commit-log dumps, no auto-generated "what's changed" lists.
-
Verify the publication:
helm show chart oci://ghcr.io/no42-org/charts/blittermib --version X.Y.Z
plus the cosign verification command from the README, and the build provenance:
gh attestation verify oci://ghcr.io/no42-org/charts/blittermib:X.Y.Z \ --repo no42-org/blittermib-chart
The release's
checksums.txtis signed too; verify it with the same identity as the chart:cosign verify-blob checksums.txt \ --bundle checksums.txt.sigstore.json \ --certificate-identity-regexp='^https://github.com/no42-org/blittermib-chart/\.github/workflows/(publish|release)\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' \ --certificate-oidc-issuer=https://token.actions.githubusercontent.com
Every merge to main publishes a signed preview chart to a single
mutable tag:
helm install blittermib oci://ghcr.io/no42-org/charts/blittermib \
--version 0.0.0-mainIt is overwritten by the next merge, carries no SBOM, provenance or
GitHub release, and is not a release — 0.0.0-main sorts below every
real version. (The version string doubles as the OCI tag because
helm push derives the tag from the chart version and offers no way
to override it, so the tag has to be valid semver.)
Its real job is to keep the publish path warm. ci.yml and
release.yml both call the same publish.yml, so the package, push
and cosign steps a release depends on run on every merge instead of
only when you tag. The cosign v3 bundle bug that broke v0.5.10-rc1
would have surfaced at merge time under this arrangement.
The preview is signed by the same identity as a release, differing only in the ref — verify one with:
cosign verify ghcr.io/no42-org/charts/blittermib:0.0.0-main \
--certificate-identity-regexp='^https://github.com/no42-org/blittermib-chart/\.github/workflows/publish\.yml@refs/heads/main$' \
--certificate-oidc-issuer=https://token.actions.githubusercontent.comThat @refs/heads/main anchor is what stops a preview satisfying the
release verification command, and vice versa.
A tag with a semver prerelease suffix — v0.5.10-rc1 — goes through
exactly the same pipeline; the trigger glob matches it. The workflow
detects the suffix and marks the draft --prerelease, so publishing
it does not make it the repository's "Latest release".
Use one to exercise the pipeline end to end after changing it. Note
that the published chart is still real and still immutable: it lands
in oci://ghcr.io/no42-org/charts permanently, so name it clearly as
an rc and do not bump Chart.yaml for it.
- Chart
versionfollows semver: minor for behavioral or additive changes, patch for fixes with no values-surface change. - Published chart versions are immutable. Once a version is in GHCR, never retag or republish it — consumers who pulled it would silently diverge, and the original digest's Rekor transparency-log entry persists either way. If a release is broken, cut the next version. The only exception is a release that was provably never consumed; pulling it back requires deleting the GHCR package versions (chart manifest, cosign signature, referrer) before re-tagging.