Releasing a new version¶
Step-by-step guide for maintainers publishing a Kollect release.
Related: CONTRIBUTING.md (commits), development/setup.md (local tasks), ROADMAP.md (feature status).
Overview¶
Releases are tag-driven: push a tag vX.Y.Z on main and
.github/workflows/release.yaml builds, scans, signs, and
publishes artifacts. Version numbers are not bumped by CI — commit charts/kollect/Chart.yaml
and CHANGELOG.md on main first.
While the API is v1alpha1, use minor (0.3.0, 0.4.0, …) for themed feature tranches or
breaking operator behaviour; patch (0.2.1) for fixes on the current minor line. Breaking
commits use ! in the subject (see CONTRIBUTING.md).
Versioning policy¶
Kollect uses frequent pre-1.0 minors. The current release is shown in GitHub Releases; the authoritative history is the changelog.
| Policy | Detail |
|---|---|
| Cadence | Release when a coherent, validated change set is ready; no fixed calendar |
| RC tags | vX.Y.Z-rc.N — soak on green main; use workflow_dispatch with draft + prerelease |
| Breaking changes | feat!: / BREAKING CHANGE: → minor bump pre-v1.0 |
| Compatibility | Patch releases stay compatible with their minor line; breaking changes require a minor |
Before 1.0, a minor release may include breaking API or default changes. Mark them with feat!: or
BREAKING CHANGE:, document migration steps in the changelog, and call them out in release notes.
Patch releases stay compatible with their minor line.
Retroactive version anchors¶
History before the first GitHub release is split with lightweight tags (changelog anchors only):
| Tag | Commit | Milestone |
|---|---|---|
v0.0.1 |
13546aff |
Kubebuilder scaffold |
v0.0.2 |
1e6f6719 |
Core v1alpha1 CRDs |
v0.0.3 |
66421337 |
Helm chart, extraction, inventory HTTP |
v0.0.4 |
4234960b |
ADR-0201 platform pivot MVP |
v0.1.0-rc.1 – rc.3 |
2026-06-05 – 06 | Pre-strategy RCs (finalizers, helm, e2e, release pipeline) |
v0.2.0-rc.1 |
2026-06-07 | Sink-family tranche |
Push changelog anchor tags once (if not already on the remote):
git tag v0.0.1 13546aff
git tag v0.0.2 1e6f6719
git tag v0.0.3 66421337
git tag v0.0.4 4234960b
git push origin v0.0.1 v0.0.2 v0.0.3 v0.0.4
Pre-release checklist¶
git checkout main && git pull
RELEASE_SHA="$(git rev-parse HEAD)"
echo "Tagging: ${RELEASE_SHA}"
task verify
task lint
task test
task helm-test
task changelog:verify
Ensure CI, preflight, and kind-smoke (e2e-smoke.yaml) are green on ${RELEASE_SHA}
on GitHub Actions. Docs-only or path-filtered commits that skipped a required job are not
eligible — re-dispatch the skipped workflow on that exact SHA.
Then run the read-only Release gate against that immutable SHA. It rejects commits not reachable
from protected main, missing/cancelled/unsuccessful required checks, and commits that are not the
merge commit of a merged-to-main PR. Non-author APPROVE is not required (solo-maintainer
policy; Environment release + tag ruleset still gate publication):
gh workflow run release-gate.yaml -f sha="${RELEASE_SHA}"
gh run list --workflow release-gate.yaml --limit 1
The publishing workflow independently repeats this eligibility check before registry login, signing, attestations, or release uploads. Gate scripts are always loaded from the default branch so a candidate tag cannot supply its own verifier. Publication checks out the proven immutable SHA (not a movable tag ref alone) and refuses to continue if the tag no longer resolves to that SHA.
Operator protection (required)¶
Configure these in the GitHub repo settings (not expressible in workflow YAML alone):
- Environment
release— required reviewers (and optionally a wait timer) on the write-capable Release job. Without this, a malicious tagged workflow copy that drops the eligibility job could still obtain write/id-tokenpermissions. - Tag rules — restrict
v*.*.*creation to protectedmain/ allowed actors so arbitrary commits cannot be tagged into the release path.
L4 pre-release gate¶
Before tagging, require one of:
- Green
e2e-nightlyworkflow run on${RELEASE_SHA}(re-run viaworkflow_dispatchif the scheduled cron has not yet picked up the commit), or - Manual
test-e2eworkflow dispatch on that SHA, or - Local
task test:e2eon the release commit (document run ID / timestamp in the release notes).
L3 integration (test-integration in CI) remains the merge gate for sink backends; nightly L4
no longer duplicates export-integration or object-store jobs.
Git export test repository (optional)¶
For full remote git SHA assert in e2e-nightly, e2e-extended, and test-e2e
git-export jobs, set repository variable GIT_EXPORT_TEST_REPO in GitHub → Settings →
Actions → Variables (clone URL of a dedicated test repo). Workflows pass ${{ vars.GIT_EXPORT_TEST_REPO }}
with GITHUB_TOKEN; this cannot be set from workflow YAML. Without the variable, git-export jobs
verify inventory HTTP hash only (degraded mode).
RC pre-release on GitHub Actions¶
The release workflow accepts draft and prerelease inputs only on workflow_dispatch.
Pushing a tag matching v*.*.* triggers a non-draft release automatically — use rc tags with
dispatch inputs when you need draft/prerelease metadata.
Steps (maintainer, on green main):
git checkout main && git pull
RELEASE_SHA="$(git rev-parse HEAD)"
git tag v0.3.0-rc.1 "${RELEASE_SHA}"
git push origin v0.3.0-rc.1
Then trigger a draft pre-release rebuild if needed:
gh workflow run release.yaml \
-f tag=v0.3.0-rc.1 \
-f draft=true \
-f prerelease=true
Monitor: gh run list --workflow=release.yaml --limit 3
Skip tag push if you only want local validation — task release-dry-run covers assets without
publishing to GHCR or GitHub Releases.
Version and changelog¶
1. Preview unreleased notes¶
task changelog
2. Choose the version¶
| Change | Example bump |
|---|---|
| Themed feature tranche / breaking operator behaviour | 0.2.0 → 0.3.0 |
| Bug fixes on current minor | 0.2.0 → 0.2.1 |
| Soak before minor GA | Tag 0.3.0-rc.1 first |
3. Bump the Helm chart¶
Edit charts/kollect/Chart.yaml:
# kollect-doc: ignore Helm Chart.yaml, not a kollect CR
version: 0.3.0
appVersion: "0.3.0"
Align version and appVersion with the git tag (v0.3.0 → 0.3.0).
Also bump the artifacthub.io/images annotation in the same file — it is a hardcoded tag,
not derived from appVersion:
# kollect-doc: ignore Helm Chart.yaml annotations, not a kollect CR
annotations:
artifacthub.io/images: |
- name: kollect
image: ghcr.io/platformrelay/kollect:v0.3.0
An explicit artifacthub.io/images list overrides Artifact Hub's automatic image extraction,
so a stale tag makes the listing advertise (and security-scan) the previous release's image.
hack/test/dist_artifacthub_chart_test.sh fails the build if the tag does not equal
v<appVersion>, so CI catches a missed bump — but fix it here, not in CI.
4. Regenerate CHANGELOG.md¶
task changelog:write
git add charts/kollect/Chart.yaml CHANGELOG.md
git commit -m ":bookmark: chore(release): prepare v0.3.0"
Cut a release¶
Land the release prep through a protected-main PR (rebase-merge). A second human review is not
required for solo-maintainer releases. Refetch and record the exact resulting main SHA:
git fetch origin main
git switch main
git pull --ff-only origin main
RELEASE_SHA="$(git rev-parse HEAD)"
gh workflow run release-gate.yaml -f sha="${RELEASE_SHA}"
Only after that gate succeeds on the same immutable SHA:
git tag v0.3.0 "${RELEASE_SHA}"
git push origin v0.3.0 # triggers release workflow, which repeats eligibility before login
Do not git push origin main outside branch protection, and do not move an existing release tag.
CI publishes the GitHub Release, GHCR image, OCI Helm chart, and attached assets.
Dry-run locally before tagging:
VERSION=0.3.0 task release-dry-run
ls -la dist/
Rebuild assets for an existing tag: Actions → Release → Run workflow → enter the tag
(optional draft / prerelease inputs). Rebuilds still require eligibility on the tag's commit SHA
and the protected release environment.
What CI publishes¶
| Output | Location |
|---|---|
| Container image (operator) | ghcr.io/platformrelay/kollect:v<version> only, linux/amd64 + arm64 (the bare :<version> tag on this repo path hosts the Helm chart — DR-FIND-07) |
| Container image (pipeline CLI) | ghcr.io/platformrelay/kollect-pipeline:<version> (and :v<version>), linux/amd64 + arm64 |
| OCI SBOM + SLSA provenance | GHCR attestations on operator and pipeline images |
| GitHub Release | git-cliff section + install footer; assets below |
install-crds.yaml |
CRD bundle |
install.yaml |
Full operator install (image pinned to tag) |
kollect-<version>.tgz |
Helm chart tarball |
sbom.spdx.json |
SPDX SBOM for operator image (Syft) |
sbom-pipeline.spdx.json |
SPDX SBOM for kollect-pipeline image (Syft) |
checksums.txt |
SHA256 of release files |
<asset>.sigstore.json |
Sigstore bundle for each release asset (cosign keyless) |
release-provenance.intoto.jsonl |
Combined SLSA provenance attestation for release assets |
| Helm chart (OCI) | oci://ghcr.io/platformrelay/kollect |
Release notes are assembled by hack/assemble-release-notes.sh
and .github/release-notes-install.md.
Submit to OperatorHub (manual fallback)¶
The operatorhub-pr job in the release workflow opens the two upstream PRs automatically, but
it skips silently when OPERATORHUB_PAT is not configured — it logs
OPERATORHUB_PAT not configured; skipping OperatorHub submission. and the job still goes green.
Check the job log after a release; if it skipped, submit by hand:
TAG=v0.18.0
REPO=platformrelay # the OWNER; the image name is appended below
# DR-FIND-07 — READ THIS BEFORE RESOLVING THE DIGEST.
# ghcr.io/platformrelay/kollect holds TWO artifact kinds:
# ghcr.io/platformrelay/kollect:0.18.0 -> the HELM CHART
# ghcr.io/platformrelay/kollect:v0.18.0 -> the CONTROLLER IMAGE <- you want this one
# Both give you a valid sha256 digest, so a mistake here passes every string check and
# only surfaces ~30 minutes into the upstream pipeline as a DeployableByOLM timeout
# (CreateContainerError "image not known"). Always digest the V-PREFIXED tag.
IMAGE_DIGEST="$(crane digest "ghcr.io/${REPO}/kollect:${TAG}")"
# No crane? docker buildx imagetools inspect "ghcr.io/${REPO}/kollect:${TAG}" \
# --format '{{.Manifest.Digest}}'
VERSION="${TAG#v}" IMAGE_DIGEST="${IMAGE_DIGEST}" GH_TOKEN=<pat> hack/operatorhub-pr.sh
hack/operatorhub-pr.sh refuses to submit a digest that is not a runnable container image
(hack/lib/olm-image-digest.sh), so the chart digest is rejected before anything reaches a
third-party repository.
DRY_RUN=1 generates and validates the bundle without pushing, but it exits before the
runnable-image check — that check needs a registry, and keeping it off the dry-run path is what
lets the offline meta-tests run with a synthetic digest. A clean dry run therefore says nothing
about whether IMAGE_DIGEST points at a real image; only a live submission verifies that.
To correct an already-open submission, push a new commit to the kollect-v<version> branch on
the platformrelay fork of each upstream repo — that re-triggers the hosted pipelines.
Verify after release¶
Container images (GHCR)¶
TAG=v0.2.0-rc.1 # or your release tag
REPO=platformrelay # the OWNER; the image name is appended below
# DR-FIND-07: the operator image is published at the v-prefixed tag only; the
# bare "${TAG#v}" tag on this repo path is the Helm chart, so digest by ${TAG}.
OP_DIGEST="$(crane digest ghcr.io/${REPO}/kollect:${TAG})"
PIPELINE_DIGEST="$(crane digest ghcr.io/${REPO}/kollect-pipeline:${TAG#v})"
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github.com/platformrelay/kollect/.+' \
"ghcr.io/${REPO}/kollect@${OP_DIGEST}"
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github.com/platformrelay/kollect/.+' \
"ghcr.io/${REPO}/kollect-pipeline@${PIPELINE_DIGEST}"
SLSA provenance and SPDX SBOM attestations are published to GHCR (via actions/attest) and the
repository Attestations page:
gh attestation verify "ghcr.io/${REPO}/kollect@${OP_DIGEST}" \
--owner platformrelay --repo kollect
gh attestation verify "ghcr.io/${REPO}/kollect-pipeline@${PIPELINE_DIGEST}" \
--owner platformrelay --repo kollect
GitHub Release assets (OpenSSF Scorecard Signed-Releases)¶
Each release asset ships with a Sigstore bundle (<file>.sigstore.json) and a combined SLSA
provenance bundle (release-provenance.intoto.jsonl). Verify a downloaded artifact:
TAG=v0.2.0-rc.1
VERSION="${TAG#v}"
gh release download "${TAG}" --pattern 'kollect-*.tgz' --dir /tmp/kollect-verify
cd /tmp/kollect-verify
cosign verify-blob \
--bundle "kollect-${VERSION}.tgz.sigstore.json" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github.com/platformrelay/kollect/.+' \
"kollect-${VERSION}.tgz"
Checksums: sha256sum -c checksums.txt after downloading all unsigned assets.
Rebuild an existing tag with signing¶
gh workflow run release.yaml \
-f tag=v0.2.0-rc.1 \
-f draft=false \
-f prerelease=true
Confirm CHANGELOG.md on main has an empty Unreleased section (run task changelog:write
after tagging if needed).