Skip to content

Commit 2996f1a

Browse files
authored
ci(release): tighten release workflow hygiene (#463)
Signed-off-by: Roel de Cort <roel.decort@adfinis.com>
1 parent 3ffbd2e commit 2996f1a

5 files changed

Lines changed: 190 additions & 7 deletions

File tree

.github/workflows/prepare-release-as-pr.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ jobs:
4747
id: app-token
4848
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
4949
with:
50-
app-id: ${{ secrets.OPENBAO_OPERATOR_RELEASE_PR_APP_ID }}
50+
client-id: ${{ secrets.OPENBAO_OPERATOR_RELEASE_PR_CLIENT_ID }}
5151
private-key: ${{ secrets.OPENBAO_OPERATOR_RELEASE_PR_PRIVATE_KEY }}
5252
owner: ${{ github.repository_owner }}
5353
repositories: ${{ github.event.repository.name }}

.github/workflows/release-please.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ jobs:
7474
if: ${{ steps.release-artifact-guard.outputs.skip_release_please != 'true' }}
7575
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
7676
with:
77-
app-id: ${{ secrets.OPENBAO_OPERATOR_RELEASE_PR_APP_ID }}
77+
client-id: ${{ secrets.OPENBAO_OPERATOR_RELEASE_PR_CLIENT_ID }}
7878
private-key: ${{ secrets.OPENBAO_OPERATOR_RELEASE_PR_PRIVATE_KEY }}
7979
owner: ${{ github.repository_owner }}
8080
repositories: ${{ github.event.repository.name }}

.github/workflows/release-tag.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
id: app-token
4040
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
4141
with:
42-
app-id: ${{ secrets.OPENBAO_OPERATOR_RELEASE_TAG_APP_ID }}
42+
client-id: ${{ secrets.OPENBAO_OPERATOR_RELEASE_TAG_CLIENT_ID }}
4343
private-key: ${{ secrets.OPENBAO_OPERATOR_RELEASE_TAG_PRIVATE_KEY }}
4444
owner: ${{ github.repository_owner }}
4545
repositories: ${{ github.event.repository.name }}

docs/contribute/release-management.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ Release-please remains the source of truth for release notes. After release-plea
137137

138138
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.
139139

140+
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.
141+
140142
</Callout>
141143

142144
<DecisionTable
@@ -159,13 +161,22 @@ Keep `CHANGELOG.md` generated by release-please. Put hand-written release summar
159161
{
160162
cells: [
161163
"Post-release",
162-
"GitHub Release exists, assets verify, provenance evidence is recorded, Artifact Hub metadata is visible, and announcement links are captured.",
164+
"GitHub Release exists, assets verify, provenance evidence is recorded, Artifact Hub metadata is visible, no unexpected release-please PR or branch remains, and announcement links are captured.",
163165
],
164166
emphasis: "caution",
165167
},
166168
]}
167169
/>
168170

171+
<CommandBlock
172+
language="bash"
173+
label="verify"
174+
title="Run post-release verification"
175+
code={`VERSION=X.Y.Z REPO=dc-tec/openbao-operator hack/ci/verify-post-release.sh`}
176+
>
177+
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.
178+
</CommandBlock>
179+
169180
<CommandBlock
170181
language="bash"
171182
label="verify"
@@ -193,14 +204,14 @@ jq '.release, .identity_constraints, .images, .chart, .release_artifacts.checksu
193204

194205
## Verifying artifacts {#5-verifying-artifacts}
195206

196-
Use the verification skeleton above as the default post-release evidence pack. Keep the exact workflow identity, tag ref, and digest-pinned subjects aligned with the artifacts that were just published.
207+
Run `hack/ci/verify-post-release.sh` first and use the verification skeleton above for deeper spot checks or release evidence capture. Keep the exact workflow identity, tag ref, and digest-pinned subjects aligned with the artifacts that were just published. Once the release is fully published, delete the matching `release-please--branches--<base>` branch unless release-please still has an active release PR for that base branch.
197208

198209
<Callout type="note" title="release-please token requirements">
199210

200211
Release automation must use non-default tokens so the resulting tag and GitHub Release can trigger downstream workflows. Use two repo-scoped GitHub Apps:
201212

202-
- `OPENBAO_OPERATOR_RELEASE_PR_APP_ID` and `OPENBAO_OPERATOR_RELEASE_PR_PRIVATE_KEY` for PR-only `release-please`
203-
- `OPENBAO_OPERATOR_RELEASE_TAG_APP_ID` and `OPENBAO_OPERATOR_RELEASE_TAG_PRIVATE_KEY` for the custom `Release Tag` workflow
213+
- `OPENBAO_OPERATOR_RELEASE_PR_CLIENT_ID` and `OPENBAO_OPERATOR_RELEASE_PR_PRIVATE_KEY` for PR-only `release-please`
214+
- `OPENBAO_OPERATOR_RELEASE_TAG_CLIENT_ID` and `OPENBAO_OPERATOR_RELEASE_TAG_PRIVATE_KEY` for the custom `Release Tag` workflow
204215

205216
The tag app should be the only actor with semver tag ruleset bypass, and it only needs repository `contents: write`.
206217

hack/ci/verify-post-release.sh

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
usage() {
6+
cat >&2 <<'USAGE'
7+
usage: VERSION=X.Y.Z [REPO=dc-tec/openbao-operator] hack/ci/verify-post-release.sh
8+
9+
Verifies the post-release invariants that should hold after the Release workflow
10+
has published a stable release.
11+
12+
Environment:
13+
VERSION Required release version, for example 0.3.0.
14+
REPO GitHub repository. Default: dc-tec/openbao-operator.
15+
GIT_REMOTE Git remote used for branch/tag checks. Default: https://github.com/${REPO}.git.
16+
ALLOW_DRAFT Set to 1 to allow a draft GitHub Release. Default: 0.
17+
USAGE
18+
}
19+
20+
fail() {
21+
echo "error: $*" >&2
22+
exit 1
23+
}
24+
25+
info() {
26+
echo "==> $*"
27+
}
28+
29+
require_cmd() {
30+
local cmd="$1"
31+
command -v "${cmd}" >/dev/null 2>&1 || fail "required command not found: ${cmd}"
32+
}
33+
34+
VERSION="${VERSION:-${1:-}}"
35+
REPO="${REPO:-dc-tec/openbao-operator}"
36+
OWNER="${REPO%%/*}"
37+
GIT_REMOTE="${GIT_REMOTE:-https://github.com/${REPO}.git}"
38+
ALLOW_DRAFT="${ALLOW_DRAFT:-0}"
39+
40+
if [[ "${VERSION}" == "-h" || "${VERSION}" == "--help" ]]; then
41+
usage
42+
exit 0
43+
fi
44+
45+
if [[ -z "${VERSION}" ]]; then
46+
usage
47+
exit 2
48+
fi
49+
50+
if ! [[ "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-.+][0-9A-Za-z.-]+)?$ ]]; then
51+
fail "VERSION must be SemVer, got '${VERSION}'"
52+
fi
53+
54+
for cmd in gh jq git docker cosign; do
55+
require_cmd "${cmd}"
56+
done
57+
58+
required_assets=(
59+
install.yaml
60+
crds.yaml
61+
checksums.txt
62+
checksums.txt.bundle
63+
checksums.txt.sigstore.json
64+
checksums.intoto.jsonl
65+
sbom-openbao-operator.spdx.json
66+
sbom-openbao-init.spdx.json
67+
sbom-openbao-backup.spdx.json
68+
sbom-openbao-upgrade.spdx.json
69+
provenance-index.json
70+
)
71+
72+
info "checking remote tag ${VERSION}"
73+
git ls-remote --exit-code --tags "${GIT_REMOTE}" "refs/tags/${VERSION}" >/dev/null ||
74+
fail "release tag ${VERSION} was not found on ${GIT_REMOTE}"
75+
76+
info "checking GitHub Release ${VERSION}"
77+
release_json="$(
78+
gh release view "${VERSION}" \
79+
--repo "${REPO}" \
80+
--json assets,isDraft,isPrerelease,tagName,url
81+
)"
82+
83+
release_tag="$(jq -r '.tagName' <<<"${release_json}")"
84+
if [[ "${release_tag}" != "${VERSION}" ]]; then
85+
fail "GitHub Release tagName is '${release_tag}', expected '${VERSION}'"
86+
fi
87+
88+
is_draft="$(jq -r '.isDraft' <<<"${release_json}")"
89+
if [[ "${is_draft}" == "true" && "${ALLOW_DRAFT}" != "1" ]]; then
90+
fail "GitHub Release ${VERSION} is still a draft"
91+
fi
92+
93+
mapfile -t asset_names < <(jq -r '.assets[].name' <<<"${release_json}" | LC_ALL=C sort)
94+
missing_assets=()
95+
for asset in "${required_assets[@]}"; do
96+
if ! printf '%s\n' "${asset_names[@]}" | grep -Fxq "${asset}"; then
97+
missing_assets+=("${asset}")
98+
fi
99+
done
100+
if (( ${#missing_assets[@]} > 0 )); then
101+
printf 'missing release assets:\n' >&2
102+
printf ' - %s\n' "${missing_assets[@]}" >&2
103+
exit 1
104+
fi
105+
106+
release_url="$(jq -r '.url' <<<"${release_json}")"
107+
info "release assets present: ${release_url}"
108+
109+
tmpdir="$(mktemp -d)"
110+
trap 'rm -rf "${tmpdir}"' EXIT
111+
112+
info "downloading signature evidence"
113+
gh release download "${VERSION}" \
114+
--repo "${REPO}" \
115+
--dir "${tmpdir}" \
116+
--clobber \
117+
--pattern checksums.txt \
118+
--pattern checksums.txt.bundle \
119+
--pattern provenance-index.json
120+
121+
identity="https://github.com/${REPO}/.github/workflows/release.yml@refs/tags/${VERSION}"
122+
123+
info "verifying checksums signature"
124+
cosign verify-blob \
125+
--new-bundle-format=true \
126+
--bundle "${tmpdir}/checksums.txt.bundle" \
127+
--certificate-identity "${identity}" \
128+
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
129+
"${tmpdir}/checksums.txt" >/dev/null
130+
131+
provenance_tag="$(jq -r '.release.tag' "${tmpdir}/provenance-index.json")"
132+
if [[ "${provenance_tag}" != "${VERSION}" ]]; then
133+
fail "provenance-index.json release tag is '${provenance_tag}', expected '${VERSION}'"
134+
fi
135+
136+
chart_ref="ghcr.io/${OWNER}/charts/openbao-operator:${VERSION}"
137+
info "checking Helm chart publication: ${chart_ref}"
138+
chart_digest="$(
139+
docker buildx imagetools inspect "${chart_ref}" --format '{{json .Manifest.Digest}}' | tr -d '"'
140+
)"
141+
if [[ -z "${chart_digest}" || "${chart_digest}" == "null" ]]; then
142+
fail "could not resolve chart digest for ${chart_ref}"
143+
fi
144+
145+
info "verifying Helm chart signature"
146+
cosign verify \
147+
--new-bundle-format=true \
148+
--certificate-identity "${identity}" \
149+
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
150+
"ghcr.io/${OWNER}/charts/openbao-operator@${chart_digest}" >/dev/null
151+
152+
info "checking for open release-please PRs"
153+
open_release_prs="$(
154+
gh pr list \
155+
--repo "${REPO}" \
156+
--state open \
157+
--json number,title,headRefName,url \
158+
--jq '.[] | select(.headRefName | startswith("release-please--branches--")) | "#\(.number) \(.title) [\(.headRefName)] \(.url)"'
159+
)"
160+
if [[ -n "${open_release_prs}" ]]; then
161+
echo "${open_release_prs}" >&2
162+
fail "unexpected open release-please PRs remain"
163+
fi
164+
165+
info "checking for stale release-please branches"
166+
stale_branches="$(git ls-remote --heads "${GIT_REMOTE}" 'release-please--branches--*')"
167+
if [[ -n "${stale_branches}" ]]; then
168+
echo "${stale_branches}" >&2
169+
fail "stale release-please branches remain"
170+
fi
171+
172+
info "post-release verification passed for ${VERSION}"

0 commit comments

Comments
 (0)