Releasing
The chart is published to GHCR as an OCI artifact under the maintainer’s namespace (oci://ghcr.io/purisev/universal-helm-chart for this repo; ${{ github.repository_owner }} for forks — see Forks below). Two CI workflows under .github/workflows/ handle the publishing automatically.
Cutting a release (tagged build)
-
Check
Chart.yaml. Theversionfield is pinned to the in-flight release line and must already match the version you want to publish — don’t bump per-PR.The
version-pinsCI job checks this for you, and checks that the version inChart.yamlis also the one every Argo CDtargetRevision, Fluxtag,helm install --versionand schema URL underdocs/names. Given a ref that names a version — arelease-X.Y.Zbranch or avX.Y.Ztag — it additionally holdsChart.yamlto that version. CI hands it the branch a PR targets, the branch it comes from and the tag, so the release PR asserts the version too rather than only the PRs into the release branch. Run it locally with:TARGET_REFS=release-<X.Y.Z> bash .github/scripts/check-version-pins.shIt runs on every CI run, and the branch preview build in
ci.yamlwill not publish while it is red.release.yamlruns it again on the tag itself, where the ref is the version the artifact is about to carry, so a chart cannot reach GHCR under a version that is not the one inside it. The migration guide and the ADRs are skipped, since naming older versions is what they are for. -
Date the release in
CHANGELOG.md. Every user-visible change goes under the heading of the in-flight version as it lands, and the heading reads## <X.Y.Z> - unreleaseduntil the release. Replaceunreleasedwith the release date. Theversion-pinsCI job fails whileCHANGELOG.mdhas no notes for the version inChart.yaml, andrelease.yamlrefuses a tag whose section is still markedunreleased. Print the notes a release will carry with:bash .github/scripts/release-notes.sh -
Tag and push from the matching release branch:
git checkout release-<X.Y.Z> git tag v<X.Y.Z> git push origin v<X.Y.Z> -
CI takes over.
release.yamlruns onv*tags: it lints, runs the unittest suite, checks the tag againstChart.yamland the docs, reads the release notes fromCHANGELOG.md, packages the chart, pushes the artifact tooci://ghcr.io/<your-github-namespace>(the workflow resolves your namespace from${{ github.repository_owner }}), and signs it keylessly via Sigstore/cosign — no key management, the signature is tied to this repo’s GitHub Actions OIDC identity. It then publishes the GitHub Release for the tag with the notes fromCHANGELOG.mdand three assets: the packaged chart, its Sigstore signature bundle (.sigstore.json) and its build provenance (.intoto.jsonl). A release that already exists for the tag gets the notes and the assets added to it. -
Verify the artifact:
helm pull oci://ghcr.io/purisev/universal-helm-chart --version <X.Y.Z> -
Verify the signature (optional, proves the artifact was actually built by this repo’s
release.yamland not pushed by hand or from a fork):cosign verify \ --certificate-identity-regexp "^https://github.com/purisev/universal-helm-chart/" \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ ghcr.io/purisev/universal-helm-chart:<X.Y.Z>The chart attached to the GitHub Release verifies the same way against its own bundle, and its provenance with the GitHub CLI:
cosign verify-blob \ --bundle universal-helm-chart-<X.Y.Z>.tgz.sigstore.json \ --certificate-identity-regexp "^https://github.com/purisev/universal-helm-chart/" \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ universal-helm-chart-<X.Y.Z>.tgz gh attestation verify universal-helm-chart-<X.Y.Z>.tgz --repo purisev/universal-helm-chart
Branch builds (PR previews)
ci.yaml can also publish a preview artifact, tagged <chart-version>-<branch-slug> — for example <X.Y.Z>-feat-xyz. It’s gated behind the publish-preview label (not automatic on every PR) and only runs once linting/tests have actually passed. Apply the label to a non-draft, non-fork PR to trigger it. Reviewers can then pull it directly without cloning:
helm template demo oci://ghcr.io/purisev/universal-helm-chart --version <X.Y.Z>-feat-xyz -f my-values.yaml
What gets published
The OCI artifact contains only what chart consumers need: Chart.yaml, templates/, values.yaml, values.yaml.example, values.schema.json, README.md, CHANGELOG.md, LICENSE, NOTICE, SECURITY.md. Everything else (docs/, tests/, test/, .github/) is excluded via .helmignore.
Forks
Both workflows resolve the GHCR namespace from ${{ github.repository_owner }}, so a fork publishes to oci://ghcr.io/<your-github-user>/universal-helm-chart automatically — no workflow edits required. Tag a release on your fork’s release-<X.Y.Z> branch the same way and release.yaml runs against your own GHCR namespace using your repo’s GITHUB_TOKEN.