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 }}
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}"