Skip to main content

Release and verification

What a release contains​

A release is the git tag vX.Y.Z (semantic version, annotated tag; vX.Y.Z-rc.N is a pre-release), published from GitHub (github.com/fyannk/pgToolBox) by one workflow (.github/workflows/release.yml). It requires green CI and a passing security gate on the tagged commit, and nothing is ever published from main: a pull request that touches the release files (and a weekly schedule) runs the same build as a dry run in a separate workflow (release-dry-run.yml) that holds no publishing permission, pushing and signing nothing.

The order matters for what you verify: the images are pushed and signed first, then everything that names them is rewritten to their digests before it is built and signed itself. The images are scanned by digest, on both platforms, before latest moves and before the release becomes visible, and the release is published only once every pushed digest can be pulled anonymously. The full sequence is on CI and releases.

ArtifactReferenceVerify with
Operatorghcr.io/fyannk/pgtoolbox-operator:X.Y.Zcosign verify, gh attestation verify
Consoleghcr.io/fyannk/pgtoolbox-console:X.Y.Zcosign verify, gh attestation verify
Evidence agentghcr.io/fyannk/pgtoolbox-evidence-agent:X.Y.Zcosign verify, gh attestation verify
OLM bundleghcr.io/fyannk/pgtoolbox-bundle:X.Y.Zcosign verify, gh attestation verify
OLM catalogghcr.io/fyannk/pgtoolbox-catalog:X.Y.Zcosign verify, gh attestation verify
Helm chart, the operatoroci://ghcr.io/fyannk/charts/pgtoolbox, version X.Y.Zcosign verify, gh attestation verify on the chart reference
Helm chart, the team toolsoci://ghcr.io/fyannk/charts/pgtools, version X.Y.Zcosign verify, gh attestation verify on the chart reference
Installer manifestsinstall-operator.yaml, install-console.yaml on the release pageSHA256SUMS + SHA256SUMS.sigstore.json

The release workflow verifies each signature and attestation as it makes them; once the packages are in their final state, verify-release.yml repeats the check as an anonymous user would (pull by tag with no credentials, cosign verify, gh attestation verify, helm pull), and does so again every week against the latest stable release.

Images are multi-architecture (linux/amd64, linux/arm64) with BuildKit's SBOM and mode=max provenance, OCI labels (title, source, version, revision, created, licenses) and Go built with -trimpath -ldflags "-s -w". Images carry the exact version as their tag; latest is moved only for a release that is not a pre-release (a version with a - part such as 0.2.0-rc.1 is one: the GitHub release is marked pre-release and no floating tag moves), never from main, and only after the pushed digests passed the scan. The release page attaches the two charts, the two installer manifests (their images pinned by digest), one SPDX SBOM per image and platform (sbom-<image>-amd64.spdx.json, -arm64), digests.txt and chart-digests.txt (every reference with its digest), SHA256SUMS (a checksum of every other attached file) and SHA256SUMS.sigstore.json (its keyless signature); its notes are the matching CHANGELOG.md section followed by a link to the commit log (the compare view with the previous release, or the history for the first).

The documentation site (GitHub Pages) is rebuilt from the tag by the release workflow, and from main by the nightly and post-security rebuilds: it shows the release's documentation until the next of those, then main's.

The documentation embedded in the console is generated by the image build from the site of the tagged commit; nothing is committed for it. See Documentation for assistants.

The product version is written in charts/pgtoolbox/Chart.yaml and charts/pgtools/Chart.yaml (version and appVersion, the same in both); the release check fails if the tag disagrees with either, if the tagged commit is not on main, or if CHANGELOG.md has no section for the version. The OLM bundle and catalog tags follow it. The nested evidence module (pkg/evidence) is tagged separately, pkg/evidence/vX.Y.Z, and only when the wire contract changes; consumers then go get github.com/fyannk/pgToolBox/pkg/evidence@vX.Y.Z.

Before the tag​

make release-preflight TAG=vX.Y.Z (or -rc.N) answers what the release workflow will ask, before anything is pushed: a dated CHANGELOG section, an annotated tag, and ci-ok and security-gate green on the tagged commit. Check runs must exist on that commit; a commit merged with the Actions token starts none, and automerge.yml dispatches them only on its 30-minute schedule, while wait-ci gives up after about two minutes of empty polls. Checklist before pushing a tag:

  1. Date the CHANGELOG heading (## [X.Y.Z] - YYYY-MM-DD) in the commit you are going to tag, before the first candidate: release-preflight and the release workflow refuse an Unreleased heading for a candidate as well as for the final (date ## [X.Y.Z], the core version, never ## [X.Y.Z-rc.1]). The candidate and the final tag name the same commit, so nothing may be committed between them (a dating commit would change the SHA and need its own full-tier run).
  2. Dispatch both on the exact head you will tag, whatever the last run says: gh workflow run ci.yml --ref main and gh workflow run security.yml --ref main. A push to main runs only the job families its diff touches, so the ci-ok of a docs-only commit (such as the one that dates the CHANGELOG) is green with the image, e2e and OLM jobs skipped; wait-ci and release-preflight therefore accept only a run started by workflow_dispatch or schedule (the full tier) on the tagged commit. Wait for green.
  3. make release-preflight TAG=vX.Y.Z-rc.1 passes (it also refuses a commit that is not an ancestor of origin/main: fetch first).
  4. Cut a stable tag only from a commit whose release candidate was verified: tag vX.Y.Z-rc.N first, let the whole workflow and verify-release.yml pass, then tag the same commit as vX.Y.Z (the full-tier runs of step 2 still apply: they are on that SHA).
  5. Only then push the tag (see CI).

Tags cannot be deleted​

The tag ruleset has no bypass actor, on purpose: a pushed tag can be neither moved nor deleted, not even by the owner, unless the ruleset itself is edited first. A mis-pushed stable tag is therefore not a typo you can undo. While its artifacts are within the retention window of the workflow they can be dealt with by editing the ruleset deliberately; past that window the answer is the next candidate, vX.Y.Z-rc.N+1, never a re-pushed tag.

Verify​

Every image, the bundle, the catalog and both charts are signed by digest with cosign keyless: the signature is bound to the identity of the release workflow run (a certificate issued by Sigstore's Fulcio to GitHub's OIDC token) and recorded in the Rekor transparency log. There is no key to obtain or trust. Each artifact also carries a GitHub build-provenance attestation (SLSA, signed by GitHub and stored next to the image in the registry), and each image carries BuildKit's SBOM and mode=max provenance, which describe the build and are trustworthy once you have verified the image digest they are attached to.

# cosign v3 or newer (v3.1.4 is the version the workflow signs with and the one
# this is tested against; v2 is not supported), and the GitHub CLI for the attestation.
IMAGE=ghcr.io/fyannk/pgtoolbox-console
DIGEST=sha256:... # from digests.txt on the release page, or: docker buildx imagetools inspect $IMAGE:X.Y.Z

# The signature: made by the release workflow of this repository, on a tag.
cosign verify \
--certificate-identity-regexp '^https://github.com/fyannk/pgToolBox/\.github/workflows/release\.yml@refs/tags/v' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
"$IMAGE@$DIGEST"

# The build provenance: which commit and workflow run built this digest.
gh attestation verify "oci://$IMAGE@$DIGEST" --repo fyannk/pgToolBox

# BuildKit's SBOM and provenance, read from the image index.
docker buildx imagetools inspect "$IMAGE@$DIGEST" --format '{{ json .SBOM }}'
docker buildx imagetools inspect "$IMAGE@$DIGEST" --format '{{ json .Provenance }}'

# `make verify-image` runs the first two for you
make verify-image IMAGE=$IMAGE@$DIGEST

Each of the other images is verified the same way, by tag or, better, by the digest listed in digests.txt. The bundle and catalog are images; the charts are OCI artifacts with a signature and a provenance attestation but no SBOM.

Pin by digest (digests.txt, the release notes) in anything that deploys: a tag can be moved, a digest cannot. The identity pattern above names the workflow file and the tag ref, so a signature made by any other workflow, branch or repository does not verify.

The charts: digest-pinned images and their own signatures​

The charts in the registry are not the charts in the git tree. At release, hack/release.sh packages a copy of each chart whose own images are referenced by digest, the tag kept in a comment next to it: in pgtoolbox the operator, the console it runs for a PgToolBoxConsole and the default evidence agent (image.digest, operator.console.image.digest, operator.evidenceAgent.image), in pgtools the console and the evidence agent (console.image.digest, evidence.image.digest) and the tools image of pgAdmin without the operator. The bundle's CSV (containerImage, the Deployment, the operator flags and relatedImages) and the catalog's bundle reference are rewritten the same way before their images are built, so Helm and OLM install exactly the digests that were signed. The git tree keeps tags because the digests do not exist until the images are pushed; make validate-packaging runs the rewrite with made-up digests and fails when the two renderings disagree or a reference stays on a tag. Each chart is signed by its own digest:

helm pull oci://ghcr.io/fyannk/charts/pgtoolbox --version X.Y.Z
cosign verify \
--certificate-identity-regexp '^https://github.com/fyannk/pgToolBox/\.github/workflows/release\.yml@refs/tags/v' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/fyannk/charts/pgtoolbox:X.Y.Z
gh attestation verify oci://ghcr.io/fyannk/charts/pgtoolbox:X.Y.Z --repo fyannk/pgToolBox
helm show values oci://ghcr.io/fyannk/charts/pgtoolbox --version X.Y.Z | grep -B1 'sha256:'
helm show values oci://ghcr.io/fyannk/charts/pgtools --version X.Y.Z | grep -B1 'sha256:'

The installer manifests and the other attached files​

The manifests, the chart archives, the SBOMs and digests.txt are not in a registry, so they are verified through SHA256SUMS, which is signed keyless as well:

# in a directory that holds the files downloaded from the release page
cosign verify-blob --bundle SHA256SUMS.sigstore.json \
--certificate-identity-regexp '^https://github.com/fyannk/pgToolBox/\.github/workflows/release\.yml@refs/tags/v' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
SHA256SUMS
sha256sum --check --ignore-missing SHA256SUMS # macOS: shasum -a 256 -c

The first command proves the checksum file was signed by the release workflow, the second that the files you downloaded are the ones it lists. Run both before applying a manifest; --ignore-missing lets you download only the files you need.

What this proves, and what it does not​

The signature proves that the release workflow of this repository, running on a tag, signed this digest, and the transparency log makes a signature made in secret visible. It does not prove the source is free of vulnerabilities or that the maintainers' account was not compromised; the source-level controls are the security page (scanner results of the latest run on main, including what did not run), the pinned and reviewed workflows, and the OpenSSF Scorecard. There is no key to rotate or revoke: trust follows the repository and the workflow path in the identity pattern.

Versions and compatibility​

make validate-packaging compares what the Helm chart, the kustomize bases and the OLM bundle render, field by field: RBAC rules, security contexts, probes, ports, the environment contract between the chart and the binaries, and the availability rule. It fails on any other write verb in the user ClusterRoles and on a chart environment variable the binary does not read.

The packaging gates besides that: ct lint and ct install of the chart (inside the chart e2e's kind cluster: the default console, two watched namespaces, the operator alone, and the console with its NetworkPolicy enforced), kubeconform and kube-linter on every rendering, operator-sdk bundle validate and the OLM scorecard (basic and olm suites) on the bundle, and a per-image profile check (numeric non-root user, exec entrypoint, OCI labels, licence file, runs under a read-only root, as an arbitrary UID in group 0 and with no capabilities, no shell, no setuid file) on the operator, console and evidence-agent images.

The nightly kind matrix runs the end-to-end suite across Kubernetes 1.34 and 1.36 and CloudNativePG 1.30.1, with the CloudNativePG main branch as a non-blocking extra. OpenShift is a manual checklist (console).