From 70a68738a8435734697e7855deeafa008e869db8 Mon Sep 17 00:00:00 2001 From: Roel de Cort Date: Tue, 19 May 2026 23:44:16 +0200 Subject: [PATCH 1/2] ci(release): use github app client ids Signed-off-by: Roel de Cort --- .github/workflows/prepare-release-as-pr.yml | 2 +- .github/workflows/release-please.yml | 2 +- .github/workflows/release-tag.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/prepare-release-as-pr.yml b/.github/workflows/prepare-release-as-pr.yml index 89a6561e3..8a1fca6ba 100644 --- a/.github/workflows/prepare-release-as-pr.yml +++ b/.github/workflows/prepare-release-as-pr.yml @@ -47,7 +47,7 @@ jobs: id: app-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: - app-id: ${{ secrets.OPENBAO_OPERATOR_RELEASE_PR_APP_ID }} + client-id: ${{ secrets.OPENBAO_OPERATOR_RELEASE_PR_CLIENT_ID }} private-key: ${{ secrets.OPENBAO_OPERATOR_RELEASE_PR_PRIVATE_KEY }} owner: ${{ github.repository_owner }} repositories: ${{ github.event.repository.name }} diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 98731be82..20af9d957 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -74,7 +74,7 @@ jobs: if: ${{ steps.release-artifact-guard.outputs.skip_release_please != 'true' }} uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: - app-id: ${{ secrets.OPENBAO_OPERATOR_RELEASE_PR_APP_ID }} + client-id: ${{ secrets.OPENBAO_OPERATOR_RELEASE_PR_CLIENT_ID }} private-key: ${{ secrets.OPENBAO_OPERATOR_RELEASE_PR_PRIVATE_KEY }} owner: ${{ github.repository_owner }} repositories: ${{ github.event.repository.name }} diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 76f23a24e..90ea9c441 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -39,7 +39,7 @@ jobs: id: app-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: - app-id: ${{ secrets.OPENBAO_OPERATOR_RELEASE_TAG_APP_ID }} + client-id: ${{ secrets.OPENBAO_OPERATOR_RELEASE_TAG_CLIENT_ID }} private-key: ${{ secrets.OPENBAO_OPERATOR_RELEASE_TAG_PRIVATE_KEY }} owner: ${{ github.repository_owner }} repositories: ${{ github.event.repository.name }} From 818df65bf2e440f2a881b66c25eeccaaa4ee0039 Mon Sep 17 00:00:00 2001 From: Roel de Cort Date: Tue, 19 May 2026 23:44:20 +0200 Subject: [PATCH 2/2] ci(release): add post-release verification helper Signed-off-by: Roel de Cort --- docs/contribute/release-management.md | 19 ++- hack/ci/verify-post-release.sh | 172 ++++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 4 deletions(-) create mode 100755 hack/ci/verify-post-release.sh diff --git a/docs/contribute/release-management.md b/docs/contribute/release-management.md index e3978ac09..c49472d48 100644 --- a/docs/contribute/release-management.md +++ b/docs/contribute/release-management.md @@ -137,6 +137,8 @@ Release-please remains the source of truth for release notes. After release-plea Keep `CHANGELOG.md` generated by release-please. Put hand-written release summaries, migration notes, and operator-facing callouts in `release-notes/X.Y.Z.md`; the website release generator and draft GitHub Release creation prepend that file to the generated changelog entry for the matching version. +For patch releases, make the source that release-please sees match the release note you want. Prefer one cherry-picked conventional commit per user-facing fix. If a backport PR is squash-merged, make the squash title deliberately user-facing because it becomes the generated changelog entry. Use `release-notes/X.Y.Z.md` for extra context, but do not hand-edit generated `CHANGELOG.md` sections. + + + Requires `gh`, `jq`, `git`, Docker Buildx, and `cosign`. Checks the remote tag, published GitHub Release assets, checksum signature, OCI Helm chart publication and signature, and leftover release-please PRs or branches. + + ` branch unless release-please still has an active release PR for that base branch. Release automation must use non-default tokens so the resulting tag and GitHub Release can trigger downstream workflows. Use two repo-scoped GitHub Apps: -- `OPENBAO_OPERATOR_RELEASE_PR_APP_ID` and `OPENBAO_OPERATOR_RELEASE_PR_PRIVATE_KEY` for PR-only `release-please` -- `OPENBAO_OPERATOR_RELEASE_TAG_APP_ID` and `OPENBAO_OPERATOR_RELEASE_TAG_PRIVATE_KEY` for the custom `Release Tag` workflow +- `OPENBAO_OPERATOR_RELEASE_PR_CLIENT_ID` and `OPENBAO_OPERATOR_RELEASE_PR_PRIVATE_KEY` for PR-only `release-please` +- `OPENBAO_OPERATOR_RELEASE_TAG_CLIENT_ID` and `OPENBAO_OPERATOR_RELEASE_TAG_PRIVATE_KEY` for the custom `Release Tag` workflow The tag app should be the only actor with semver tag ruleset bypass, and it only needs repository `contents: write`. diff --git a/hack/ci/verify-post-release.sh b/hack/ci/verify-post-release.sh new file mode 100755 index 000000000..5f1753d4b --- /dev/null +++ b/hack/ci/verify-post-release.sh @@ -0,0 +1,172 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat >&2 <<'USAGE' +usage: VERSION=X.Y.Z [REPO=dc-tec/openbao-operator] hack/ci/verify-post-release.sh + +Verifies the post-release invariants that should hold after the Release workflow +has published a stable release. + +Environment: + VERSION Required release version, for example 0.3.0. + REPO GitHub repository. Default: dc-tec/openbao-operator. + GIT_REMOTE Git remote used for branch/tag checks. Default: https://github.com/${REPO}.git. + ALLOW_DRAFT Set to 1 to allow a draft GitHub Release. Default: 0. +USAGE +} + +fail() { + echo "error: $*" >&2 + exit 1 +} + +info() { + echo "==> $*" +} + +require_cmd() { + local cmd="$1" + command -v "${cmd}" >/dev/null 2>&1 || fail "required command not found: ${cmd}" +} + +VERSION="${VERSION:-${1:-}}" +REPO="${REPO:-dc-tec/openbao-operator}" +OWNER="${REPO%%/*}" +GIT_REMOTE="${GIT_REMOTE:-https://github.com/${REPO}.git}" +ALLOW_DRAFT="${ALLOW_DRAFT:-0}" + +if [[ "${VERSION}" == "-h" || "${VERSION}" == "--help" ]]; then + usage + exit 0 +fi + +if [[ -z "${VERSION}" ]]; then + usage + exit 2 +fi + +if ! [[ "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-.+][0-9A-Za-z.-]+)?$ ]]; then + fail "VERSION must be SemVer, got '${VERSION}'" +fi + +for cmd in gh jq git docker cosign; do + require_cmd "${cmd}" +done + +required_assets=( + install.yaml + crds.yaml + checksums.txt + checksums.txt.bundle + checksums.txt.sigstore.json + checksums.intoto.jsonl + sbom-openbao-operator.spdx.json + sbom-openbao-init.spdx.json + sbom-openbao-backup.spdx.json + sbom-openbao-upgrade.spdx.json + provenance-index.json +) + +info "checking remote tag ${VERSION}" +git ls-remote --exit-code --tags "${GIT_REMOTE}" "refs/tags/${VERSION}" >/dev/null || + fail "release tag ${VERSION} was not found on ${GIT_REMOTE}" + +info "checking GitHub Release ${VERSION}" +release_json="$( + gh release view "${VERSION}" \ + --repo "${REPO}" \ + --json assets,isDraft,isPrerelease,tagName,url +)" + +release_tag="$(jq -r '.tagName' <<<"${release_json}")" +if [[ "${release_tag}" != "${VERSION}" ]]; then + fail "GitHub Release tagName is '${release_tag}', expected '${VERSION}'" +fi + +is_draft="$(jq -r '.isDraft' <<<"${release_json}")" +if [[ "${is_draft}" == "true" && "${ALLOW_DRAFT}" != "1" ]]; then + fail "GitHub Release ${VERSION} is still a draft" +fi + +mapfile -t asset_names < <(jq -r '.assets[].name' <<<"${release_json}" | LC_ALL=C sort) +missing_assets=() +for asset in "${required_assets[@]}"; do + if ! printf '%s\n' "${asset_names[@]}" | grep -Fxq "${asset}"; then + missing_assets+=("${asset}") + fi +done +if (( ${#missing_assets[@]} > 0 )); then + printf 'missing release assets:\n' >&2 + printf ' - %s\n' "${missing_assets[@]}" >&2 + exit 1 +fi + +release_url="$(jq -r '.url' <<<"${release_json}")" +info "release assets present: ${release_url}" + +tmpdir="$(mktemp -d)" +trap 'rm -rf "${tmpdir}"' EXIT + +info "downloading signature evidence" +gh release download "${VERSION}" \ + --repo "${REPO}" \ + --dir "${tmpdir}" \ + --clobber \ + --pattern checksums.txt \ + --pattern checksums.txt.bundle \ + --pattern provenance-index.json + +identity="https://github.com/${REPO}/.github/workflows/release.yml@refs/tags/${VERSION}" + +info "verifying checksums signature" +cosign verify-blob \ + --new-bundle-format=true \ + --bundle "${tmpdir}/checksums.txt.bundle" \ + --certificate-identity "${identity}" \ + --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ + "${tmpdir}/checksums.txt" >/dev/null + +provenance_tag="$(jq -r '.release.tag' "${tmpdir}/provenance-index.json")" +if [[ "${provenance_tag}" != "${VERSION}" ]]; then + fail "provenance-index.json release tag is '${provenance_tag}', expected '${VERSION}'" +fi + +chart_ref="ghcr.io/${OWNER}/charts/openbao-operator:${VERSION}" +info "checking Helm chart publication: ${chart_ref}" +chart_digest="$( + docker buildx imagetools inspect "${chart_ref}" --format '{{json .Manifest.Digest}}' | tr -d '"' +)" +if [[ -z "${chart_digest}" || "${chart_digest}" == "null" ]]; then + fail "could not resolve chart digest for ${chart_ref}" +fi + +info "verifying Helm chart signature" +cosign verify \ + --new-bundle-format=true \ + --certificate-identity "${identity}" \ + --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ + "ghcr.io/${OWNER}/charts/openbao-operator@${chart_digest}" >/dev/null + +info "checking for open release-please PRs" +open_release_prs="$( + gh pr list \ + --repo "${REPO}" \ + --state open \ + --json number,title,headRefName,url \ + --jq '.[] | select(.headRefName | startswith("release-please--branches--")) | "#\(.number) \(.title) [\(.headRefName)] \(.url)"' +)" +if [[ -n "${open_release_prs}" ]]; then + echo "${open_release_prs}" >&2 + fail "unexpected open release-please PRs remain" +fi + +info "checking for stale release-please branches" +stale_branches="$(git ls-remote --heads "${GIT_REMOTE}" 'release-please--branches--*')" +if [[ -n "${stale_branches}" ]]; then + echo "${stale_branches}" >&2 + fail "stale release-please branches remain" +fi + +info "post-release verification passed for ${VERSION}"