From 87889aab204d1f345297bdcc0fbd5a551e1c9537 Mon Sep 17 00:00:00 2001 From: ErenAri Date: Sun, 18 Oct 2026 23:06:24 +0300 Subject: [PATCH 1/6] release: let the stable PR and tag build pass the stable canary-pin check check-canary-pins.sh always ran check_pin stable "v": the stable pin had to carry that exact version comment and equal the commit the tag resolves to. With no bootstrap for the stable pin (DEF-1), the stable release PR (stable_version = release_version = X.Y.Z, release_channel stable) fails required CI because vX.Y.Z is not a tag yet, and the vX.Y.Z tag build fails too, because the pin would have to name the tagged commit itself, which that commit cannot contain. docs/production-release-process.md already says to repin the stable step once the new stable tag exists; the check made that sequence impossible to follow. Mirror the existing candidate bootstrap for the stable pin. With release_channel stable, while vX.Y.Z positively does not exist, or resolves to the commit under check (the tag build), the stable pin may instead name the most recent published stable release before X.Y.Z: the newest vA.B.C, A.B.C < X.Y.Z, listed by the GitHub releases API as non-draft and non-prerelease, whose tag resolves to the pinned full SHA. As for the candidate, a missing, mutable or short pin fails, and every lookup failure fails closed: an unreachable or malformed tag answer, a pinned tag that does not resolve, a failed or partial API listing, or a malformed line. Once vX.Y.Z exists at any other commit the rule is strict exactly as before, so the first commit after the tag must carry the repin. The prerelease channel's stable pin stays strict, and the candidate rules are unchanged. check-canary-pins_test.sh: section 27 no longer asserts that the stable pin never bootstraps; it now tags the new stable at another commit and asserts the previous stable pin is still refused there. Sections 29-38 cover the stable PR, the tag build (also with the v0.4.0 draft staged and after its publication), the strict commit after the tag and its repin, pins on the new or a later version, an older-than-latest stable, an rc, a draft, a release marked prerelease, a tag without a release, a mismatched SHA, a missing tag, mutable and short refs, a missing pin, each lookup failure, and the unchanged prerelease channel. The gh stub also answers the paginated release listing, for the exact projection the script requests. The new sections are red against the unmodified script and green here. The release process doc spells out the stable PR and tag build phase. Signed-off-by: ErenAri --- docs/production-release-process.md | 17 ++- scripts/check-canary-pins.sh | 122 ++++++++++++++++++- scripts/check-canary-pins_test.sh | 183 +++++++++++++++++++++++++++-- 3 files changed, 306 insertions(+), 16 deletions(-) diff --git a/docs/production-release-process.md b/docs/production-release-process.md index f15d6bd..10d2b2b 100644 --- a/docs/production-release-process.md +++ b/docs/production-release-process.md @@ -234,11 +234,18 @@ graduation report and draft assets, records Only this path may update the `X.Y` and `latest` image aliases or GitHub's latest release. +The stable release pull request and the `vX.Y.Z` tag build cannot pin a tag +that does not exist yet or that names their own commit, so they keep the +`# bpfcompat-pin: stable` step on the most recent published (non-draft, +non-prerelease) stable release before `X.Y.Z`. `scripts/check-canary-pins.sh` +accepts that pin only while `vX.Y.Z` is absent or resolves to the commit under +check; a tag or GitHub release lookup it cannot complete fails the check. + Once the new stable tag exists, repin the `# bpfcompat-pin: stable` step in `.github/workflows/consumer-canary.yml` to its commit SHA and update the `# vX.Y.Z` comment beside it. `scripts/check-canary-pins.sh` runs inside the -release-consistency gate and fails the build until that is done, because a -canary left on the previous release stops testing the path the documentation -now sends every reader down. The `# bpfcompat-pin: candidate` step is not -enforced while `release_channel` is `stable`; repin it with the next -`X.Y.Z-rc.1`. +release-consistency gate and fails every later commit until that is done, +because a canary left on the previous release stops testing the path the +documentation now sends every reader down. The `# bpfcompat-pin: candidate` +step is not enforced while `release_channel` is `stable`; repin it with the +next `X.Y.Z-rc.1`. diff --git a/scripts/check-canary-pins.sh b/scripts/check-canary-pins.sh index 09c2602..4f6bd7c 100755 --- a/scripts/check-canary-pins.sh +++ b/scripts/check-canary-pins.sh @@ -23,7 +23,7 @@ # documented consumer path has no canary at all, which is the gap this guard # exists to close, so it is a failure and not a skip. # -# Preparing the next release candidate is the one state the strict rule +# Preparing the next release candidate is one state the strict rule # cannot be met in: the commit that sets release_version to a new RC (and the # tag build of that very commit) cannot pin the canary to a tag that does not # exist yet, or to itself. While v positively does not exist @@ -32,7 +32,18 @@ # (vX.Y.Z-rc.M, M < N) that is a published, non-draft GitHub prerelease and # whose tag resolves to the pinned full SHA. Any lookup or API failure fails # closed. Every later commit is strict again, so the canary must be repinned -# to the new RC before anything else lands. The stable pin rule is unchanged. +# to the new RC before anything else lands. +# +# The stable release is the same state for the stable pin: the PR that sets +# stable_version to X.Y.Z (release_channel `stable`) and the vX.Y.Z tag build +# cannot pin the canary to vX.Y.Z either. With release_channel `stable`, while +# vX.Y.Z positively does not exist or resolves to the commit under check, the +# stable pin may still name the most recent published stable release before +# X.Y.Z -- the newest vA.B.C, A.B.C < X.Y.Z, that is a non-draft, +# non-prerelease GitHub release -- whose tag resolves to the pinned full SHA. +# Any lookup or API failure fails closed, and every later commit is strict +# again: repin the stable step once the new stable tag exists. The prerelease +# channel's stable pin is always strict. # # `candidate` is enforced only while release_channel is `prerelease`. In the # stable channel there is no active prerelease to graduate, release_version @@ -201,8 +212,6 @@ check_pin() { echo "[canary-pins] ${channel} ${expected} ${sha}" } -check_pin stable "v${stable_version}" - # A release published as a non-draft GitHub prerelease. Any failure to ask # (no gh, no token, API error) is a failure, never a pass. canary_repo="Kernel-Guard/bpfcompat" @@ -212,6 +221,111 @@ published_prerelease() { [[ "$state" == "false true" ]] } +# Every GitHub release, one " " line each. Any +# failure to ask (no gh, no token, API error, a page that fails part-way) or +# a line that is not in that shape is a failure, never a pass. +release_states() { + local out line + out="$(gh api --paginate "repos/${canary_repo}/releases?per_page=100" \ + --jq '.[] | "\(.tag_name) \(.draft) \(.prerelease)"' 2>/dev/null)" || return 1 + while IFS= read -r line; do + [[ -z "$line" || "$line" =~ ^[^[:space:]]+\ (true|false)\ (true|false)$ ]] || return 1 + done <<<"$out" + printf '%s\n' "$out" +} + +# version_lt A B: X.Y.Z version A sorts strictly before X.Y.Z version B. +version_lt() { + local -a a b + local i + IFS=. read -r -a a <<<"$1" + IFS=. read -r -a b <<<"$2" + for i in 0 1 2; do + if (( 10#${a[i]} != 10#${b[i]} )); then + (( 10#${a[i]} < 10#${b[i]} )) + return + fi + done + return 1 +} + +# check_bootstrap_stable: while v is being prepared or built, +# the canary may still pin the most recent published stable release before +# it, provided that release's tag resolves to the pinned commit. +check_bootstrap_stable() { + local sha="${pin_sha[stable]:-}" line="${pin_line[stable]:-}" version="${pin_version[stable]:-}" + if [[ -z "$sha" ]]; then + note_fail "${workflow} declares no '# bpfcompat-pin: stable' pin, so the stable consumer path has no canary" + return + fi + if [[ ! "$sha" =~ ^[0-9a-f]{40}$ ]]; then + note_fail "${workflow}:${line} stable pin '${sha}' is not a full 40-character commit SHA" + return + fi + if [[ ! "$version" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then + note_fail "${workflow}:${line} stable pin '${version:-}' must be an earlier vX.Y.Z stable release while v${stable_version} is being prepared" + return + fi + if ! version_lt "${BASH_REMATCH[1]}" "$stable_version"; then + note_fail "${workflow}:${line} stable pin ${version} is not earlier than v${stable_version}" + return + fi + tag_lookup "$version" + if [[ "$LOOKUP_STATE" != found ]]; then + note_fail "${workflow}:${line} could not resolve ${version} (${LOOKUP_STATE}); failing closed" + return + fi + if [[ "$LOOKUP_SHA" != "$sha" ]]; then + note_fail "${workflow}:${line} stable pin is ${sha} but ${version} is ${LOOKUP_SHA}" + return + fi + local states tag draft prerelease newest="" + if ! states="$(release_states)"; then + note_fail "${workflow}:${line} could not list the GitHub releases to verify ${version} (no gh, no token, API error, or a malformed answer); failing closed" + return + fi + if ! grep -Fxq -- "${version} false false" <<<"$states"; then + note_fail "${workflow}:${line} ${version} is not a verified published stable release (draft, prerelease, or no GitHub release)" + return + fi + while read -r tag draft prerelease; do + [[ "$draft $prerelease" == "false false" && "$tag" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]] || continue + version_lt "${BASH_REMATCH[1]}" "$stable_version" || continue + if [[ -z "$newest" ]] || version_lt "${newest#v}" "${tag#v}"; then + newest="$tag" + fi + done <<<"$states" + if [[ "$newest" != "$version" ]]; then + note_fail "${workflow}:${line} stable pin ${version} is not the most recent published stable release before v${stable_version} (${newest} is)" + return + fi + echo "[canary-pins] stable bootstrap: v${stable_version} is being prepared; canary still pins published stable ${version} ${sha} (repin after the tag)" +} + +if [[ "$release_channel" == stable && "$stable_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + tag_lookup "v${stable_version}" + case "$LOOKUP_STATE" in + absent) + # The new stable release is being prepared: its tag positively does not exist. + check_bootstrap_stable + ;; + found) + head_commit="$(git rev-parse -q --verify HEAD 2>/dev/null || true)" + if [[ "$head_commit" =~ ^[0-9a-f]{40}$ && "$LOOKUP_SHA" == "$head_commit" ]]; then + # The tag build of the new stable release itself. + check_bootstrap_stable + else + check_pin stable "v${stable_version}" + fi + ;; + *) + note_fail "cannot determine whether v${stable_version} exists (remote unreachable or malformed answer); failing closed" + ;; + esac +else + check_pin stable "v${stable_version}" +fi + # check_bootstrap_candidate : while v-rc. is # being prepared or built, the canary may still pin an EARLIER rc of the SAME # release that is a published prerelease and resolves to the pinned commit. diff --git a/scripts/check-canary-pins_test.sh b/scripts/check-canary-pins_test.sh index 3e6a4b6..b24da25 100755 --- a/scripts/check-canary-pins_test.sh +++ b/scripts/check-canary-pins_test.sh @@ -234,11 +234,23 @@ fx="$tmp/bootstrap" mkdir -p "$fx/bin" cat >"$fx/bin/gh" <<'STUB' #!/usr/bin/env bash +# $GH_RELEASES holds one " " line per GitHub release. # gh api repos/Kernel-Guard/bpfcompat/releases/tags/ --jq ... -[[ "$1" == api && "$2" == repos/Kernel-Guard/bpfcompat/releases/tags/* ]] || exit 99 -tag="${2##*/}" -line="$(grep -m1 "^${tag} " "$GH_RELEASES" 2>/dev/null)" || exit 1 -echo "${line#* }" +if [[ "$1" == api && "$2" == repos/Kernel-Guard/bpfcompat/releases/tags/* ]]; then + tag="${2##*/}" + line="$(grep -m1 "^${tag} " "$GH_RELEASES" 2>/dev/null)" || exit 1 + echo "${line#* }" + exit 0 +fi +# gh api --paginate repos/Kernel-Guard/bpfcompat/releases?per_page=100 --jq ... +# (the stable bootstrap; only the exact projection it asks for is answered). +if [[ "$1" == api && "$2" == --paginate && + "$3" == "repos/Kernel-Guard/bpfcompat/releases?per_page=100" && + "$4" == --jq && "$5" == '.[] | "\(.tag_name) \(.draft) \(.prerelease)"' ]]; then + cat -- "$GH_RELEASES" 2>/dev/null || exit 1 + exit 0 +fi +exit 99 STUB chmod +x "$fx/bin/gh" @@ -346,16 +358,21 @@ ffail "commit after the rc tag with a stale candidate pin" "candidate pin is com # ...and repinned to the new RC it passes strictly. { pin stable "$S" v0.3.7; pin candidate "$(fsha v0.4.0-rc.4)" v0.4.0-rc.4; } | fcanary fpass "repinned to the new rc after the tag" -# 27. Stable channel is unchanged: wrong stable pin fails, correct passes, -# and no bootstrap applies to the stable pin. +# 27. Stable channel, strict path: whenever v exists at a +# commit other than the one under check, a wrong stable pin fails, the +# correct one passes, and a pin left on the previous stable release fails +# even though that release is published -- the stable bootstrap (29-38) +# never outlives the new stable tag. fmeta 0.3.7 0.3.7 stable { pin stable "$R3" v0.3.7; } | fcanary ffail "stable channel: stale stable pin" "stable pin is ${R3} but v0.3.7 is ${S}" { pin stable "$S" v0.3.7; } | fcanary fpass "stable channel: correct stable pin" +printf 'v0.3.7 false false\n' >>"$fx/releases" +( cd "$fx/work" && gitq tag -a v0.3.8 -m v0.3.8 HEAD^ && gitq push -q origin --tags ) fmeta 0.3.8 0.3.8 stable { pin stable "$S" v0.3.7; } | fcanary -ffail "stable channel: stable pin never bootstraps to an older release" "stable pin is commented 'v0.3.7' but release.yaml says v0.3.8" +ffail "stable channel: previous stable pin once v0.3.8 is tagged at another commit" "stable pin is commented 'v0.3.7' but release.yaml says v0.3.8" # 28. Every workflow step that reaches the canary-pin check -- directly or via # `make production-tech-check` -- must provide GH_TOKEN, or the RC @@ -388,4 +405,156 @@ if [[ -n "$missing" ]]; then fi echo "[canary-pins-test] every workflow path into the canary-pin check provides GH_TOKEN" +# 29-38. The stable bootstrap (DEF-1). The stable release PR sets +# stable_version = release_version = X.Y.Z with release_channel stable +# before vX.Y.Z exists, and the vX.Y.Z tag build checks the very commit +# that tag names; neither can pin the canary to vX.Y.Z. While vX.Y.Z +# positively does not exist, or resolves to the commit under check, the +# stable pin may still name the most recent published stable release +# before X.Y.Z (non-draft, non-prerelease GitHub release); every later +# commit is strict again. Same harness as 16-27. The fixture mirrors the +# real cycle: v0.3.6 and v0.3.7 published, v0.3.8 only a draft, +# v0.4.0-rc.3 a prerelease, and research-v1 a published release whose +# tag is not a version; the API lists them in no particular order. +new_stable_fixture() { + rm -rf "$fx/work" "$fx/origin.git" + git init -q --bare "$fx/origin.git" + git init -q -b main "$fx/work" + mkdir -p "$fx/work/scripts" "$fx/work/.github/workflows" + cp "$ROOT_DIR/$script" "$fx/work/scripts/check-canary-pins.sh" + ( + cd "$fx/work" + gitq remote add origin "$fx/origin.git" + for t in v0.3.6 v0.3.7 v0.3.8 v0.4.0-rc.3; do + gitq commit --allow-empty -m "$t" + gitq tag -a "$t" -m "$t" + done + gitq commit --allow-empty -m "release: v0.4.0 stable metadata" + gitq push -q origin main --tags + ) + printf 'v0.3.6 false false\nv0.4.0-rc.3 false true\nresearch-v1 false false\nv0.3.8 true false\nv0.3.7 false false\n' >"$fx/releases" +} +new_stable_fixture +T6="$(fsha v0.3.6)"; T7="$(fsha v0.3.7)"; T8="$(fsha v0.3.8)"; RC3="$(fsha v0.4.0-rc.3)" + +# 29. The stable PR: v0.4.0 positively absent, the pin stays on v0.3.7. +fmeta 0.4.0 0.4.0 stable +pin stable "$T7" v0.3.7 | fcanary +fpass "stable PR before its tag: most recent published stable accepted" +grep -Fq "[canary-pins] stable bootstrap: v0.4.0 is being prepared; canary still pins published stable v0.3.7 ${T7}" "$fx/out.log" || + { echo "[canary-pins-test] stable bootstrap must be announced" >&2; cat "$fx/out.log" >&2; exit 1; } +{ pin stable "$T7" v0.3.7; pin candidate "$RC3" v0.4.0-rc.3; } | fcanary +fpass "stable PR before its tag, last cycle's candidate pin left in place" +grep -Fq "candidate pin not enforced" "$fx/out.log" || + { echo "[canary-pins-test] candidate pin must stay unenforced in the stable channel" >&2; exit 1; } + +# 30. Naming the new (or a later) release before its tag exists still fails. +pin stable "$T7" v0.4.0 | fcanary +ffail "stable PR pins v0.4.0 before the tag exists" "stable pin v0.4.0 is not earlier than v0.4.0" +pin stable "$T7" v0.4.1 | fcanary +ffail "stable PR pins a later release" "stable pin v0.4.1 is not earlier than v0.4.0" + +# 31. A published stable release that is not the most recent one before v0.4.0. +pin stable "$T6" v0.3.6 | fcanary +ffail "bootstrap pin on an older-than-latest stable" "stable pin v0.3.6 is not the most recent published stable release before v0.4.0 (v0.3.7 is)" + +# 32. Not a published stable release: an rc, a draft, a release marked +# prerelease, a tag with no release at all. +pin stable "$RC3" v0.4.0-rc.3 | fcanary +ffail "bootstrap pin on a release candidate" "stable pin 'v0.4.0-rc.3' must be an earlier vX.Y.Z stable release while v0.4.0 is being prepared" +pin stable "$T7" "" | fcanary +ffail "bootstrap pin with no version comment" "stable pin '' must be an earlier vX.Y.Z stable release" +pin stable "$T8" v0.3.8 | fcanary +ffail "bootstrap pin on a draft release" "v0.3.8 is not a verified published stable release" +pin stable "$T7" v0.3.7 | fcanary +sed -i 's/^v0.3.7 false false$/v0.3.7 false true/' "$fx/releases" +ffail "bootstrap pin on a release marked prerelease" "v0.3.7 is not a verified published stable release" +sed -i '/^v0.3.7 /d' "$fx/releases" +ffail "bootstrap pin on a tag with no GitHub release" "v0.3.7 is not a verified published stable release" +printf 'v0.3.7 false false\n' >>"$fx/releases" +fpass "bootstrap restored once v0.3.7 is published again" + +# 33. The pinned commit must be the one the named tag resolves to, and the +# named tag must resolve at all. +pin stable "$T6" v0.3.7 | fcanary +ffail "bootstrap pin whose tag resolves elsewhere" "stable pin is ${T6} but v0.3.7 is ${T7}" +pin stable "$T7" v0.3.5 | fcanary +ffail "bootstrap pin names a tag that does not exist" "could not resolve v0.3.5 (absent); failing closed" + +# 34. A mutable or short ref is not a pin, and a missing pin is not a skip. +pin stable v0.3.7 v0.3.7 | fcanary +ffail "bootstrap pin is a tag ref" "stable pin 'v0.3.7' is not a full 40-character commit SHA" +pin stable "${T7:0:12}" v0.3.7 | fcanary +ffail "bootstrap pin is a short SHA" "stable pin '${T7:0:12}' is not a full 40-character commit SHA" +pin candidate "$RC3" v0.4.0-rc.3 | fcanary +ffail "bootstrap without a stable pin" "declares no '# bpfcompat-pin: stable' pin" + +# 35. Every lookup failure fails closed: an unanswerable API, an API that +# fails part-way through pagination, a malformed API answer, and a remote +# that cannot say whether v0.4.0 exists. +pin stable "$T7" v0.3.7 | fcanary +mv "$fx/releases" "$fx/releases.off" +ffail "GitHub API unanswerable" "could not list the GitHub releases to verify v0.3.7" +mv "$fx/releases.off" "$fx/releases" +mkdir -p "$fx/partialgh" +cat >"$fx/partialgh/gh" <<'STUB' +#!/usr/bin/env bash +# A first page that looks fine, then a failed request for the next one. +printf 'v0.3.7 false false\n' +exit 1 +STUB +chmod +x "$fx/partialgh/gh" +ffail "GitHub API fails part-way through" "could not list the GitHub releases to verify v0.3.7" "$fx/partialgh" +cp "$fx/releases" "$fx/releases.good" +printf 'v0.3.9 false\n' >>"$fx/releases" +ffail "malformed GitHub API answer" "could not list the GitHub releases to verify v0.3.7" +cp "$fx/releases.good" "$fx/releases" +git -C "$fx/work" remote set-url origin "$fx/no-such-origin.git" +ffail "unreachable remote" "cannot determine whether v0.4.0 exists" +git -C "$fx/work" remote remove origin +ffail "no remote configured" "cannot determine whether v0.4.0 exists" +git -C "$fx/work" remote add origin "$fx/origin.git" +cat >"$fx/badgit/git" <>"$fx/releases" +fpass "tag build with the v0.4.0 draft staged" +sed -i 's/^v0.4.0 true false$/v0.4.0 false false/' "$fx/releases" +fpass "tag build re-checked after v0.4.0 is published" +pin stable "$(git -C "$fx/work" rev-parse HEAD^)" v0.4.0 | fcanary +ffail "tag build pins its parent commit as v0.4.0" "stable pin v0.4.0 is not earlier than v0.4.0" +pin stable "$T6" v0.3.6 | fcanary +ffail "tag build pins an older-than-latest stable" "stable pin v0.3.6 is not the most recent published stable release before v0.4.0 (v0.3.7 is)" + +# 37. Any later commit is strict again: the previous stable pin fails, and the +# repin to the new stable passes on the strict path, not the bootstrap. +( cd "$fx/work" && gitq commit --allow-empty -m "next change on main" ) +pin stable "$T7" v0.3.7 | fcanary +ffail "commit after the stable tag with the previous stable pin" "stable pin is commented 'v0.3.7' but release.yaml says v0.4.0" +pin stable "$(fsha v0.4.0)" v0.4.0 | fcanary +fpass "repinned to the new stable after the tag" +if ! grep -Fq "[canary-pins] stable v0.4.0 $(fsha v0.4.0)" "$fx/out.log" || grep -q "stable bootstrap" "$fx/out.log"; then + echo "[canary-pins-test] the repinned stable must pass strictly" >&2; cat "$fx/out.log" >&2; exit 1 +fi + +# 38. release_channel: prerelease is untouched: its stable pin is always +# strict, even while v does not exist and the pin names +# the most recent published stable before it. +fmeta 0.4.1 0.5.0-rc.1 prerelease +pin stable "$(fsha v0.4.0)" v0.4.0 | fcanary +ffail "prerelease channel: no stable bootstrap" "stable pin is commented 'v0.4.0' but release.yaml says v0.4.1" +fmeta 0.4.1 0.4.1 stable +fpass "the same pin is the stable bootstrap once the channel is stable" + echo "[canary-pins-test] PASS" From 462287dfbbbea82f9296a2c4628664d6be410182 Mon Sep 17 00:00:00 2001 From: ErenAri Date: Sun, 18 Oct 2026 23:12:10 +0300 Subject: [PATCH 2/6] docs: list the stable-only tag gates and the operator-note order The Stable Release runbook omitted two gates the stable tag build enforces (every blocks_stable distribution decision recorded; the stable validator byte-identical to the graduated RC's), and a v* tag cannot be reused after a refused build. It also had the operator record the promotion marker after checking the readiness report, while the report reads that record as an input; the record now comes first and the promotion dispatch stays a separate decision. Signed-off-by: ErenAri --- docs/production-release-process.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/docs/production-release-process.md b/docs/production-release-process.md index 10d2b2b..f202859 100644 --- a/docs/production-release-process.md +++ b/docs/production-release-process.md @@ -228,11 +228,22 @@ test; production evidence must verify the candidate attestation online. After every operational gate passes, set both `stable_version` and `release_version` to `X.Y.Z`, set `release_channel` to `stable`, update stable -documentation, and tag the checked merge commit. The operator checks the -graduation report and draft assets, records -`[bpfcompat-solo-promotion:v1]`, and dispatches the exact promotion inputs. -Only this path may update the `X.Y` and `latest` image aliases or GitHub's -latest release. +documentation, and tag the checked merge commit. The operator's +`[bpfcompat-solo-promotion:v1]` record is an input of the final readiness +report, so it is written before that report is generated; the operator then +checks the graduation report and draft assets and dispatches the exact +promotion inputs as a separate decision. Only this path may update the `X.Y` +and `latest` image aliases or GitHub's latest release. + +Before tagging, confirm two stable-only gates, because the stable tag build +refuses otherwise and a `v*` tag cannot be reused: + +- `scripts/check-distribution-policy.sh --require-decided` passes, that is, + every decision listed in `blocks_stable` in `distribution/policy.json` is + recorded. +- The stable commit's validator rebuilds to the graduated release candidate's + `bpfcompat-validator-static-linux-amd64` sha256 + (`scripts/check-validator-matches-candidate.sh`). The stable release pull request and the `vX.Y.Z` tag build cannot pin a tag that does not exist yet or that names their own commit, so they keep the From f61c2916eb3e1b9480ca1f84797c5bd3e061c564 Mon Sep 17 00:00:00 2001 From: ErenAri Date: Sun, 18 Oct 2026 23:30:11 +0300 Subject: [PATCH 3/6] release: let the stable PR document its own unreleased tag DEF-2, found by the independent review of DEF-1's fix: the release- consistency gate requires the stable PR's docs to name vX.Y.Z, while scripts/check-doc-pins.sh refused any tag-form pin whose tag does not exist, so the stable PR could not pass required CI even with the canary-pin fix. While release.yaml says release_channel stable, a tag-form doc pin on exactly v is accepted when the remote positively has no such tag (git ls-remote --exit-code returns 2). An unreachable remote, another version, a SHA pin or the prerelease channel still fail. The check gains its first regression test, run in CI. The runbook also states the stable order and that no other stable release may be published between the stable PR's CI and the tag build, because the stable pin may only name the newest published stable below X.Y.Z. Signed-off-by: ErenAri --- .github/workflows/ci.yml | 5 +++ docs/production-release-process.md | 14 ++++++- scripts/check-doc-pins.sh | 35 ++++++++++++++-- scripts/check-doc-pins_test.sh | 66 ++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 5 deletions(-) create mode 100755 scripts/check-doc-pins_test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d64282..7bf7091 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,11 @@ jobs: # v0.3.6 shipped a broken action; the docs kept pointing at it for two # weeks because nothing checked the pins a reader copy-pastes. run: bash scripts/check-doc-pins.sh + - name: Documented action pins regression + # The stable release PR documents vX.Y.Z before its tag exists; only + # that tag, only in the stable channel, and only when the remote + # positively lacks it. + run: bash scripts/check-doc-pins_test.sh - name: Action OCI-artifact detection regression # Guards the fix that lets the action accept remote OCI references # (ghcr.io/...) as `artifact`. Extracts is_remote_artifact() from diff --git a/docs/production-release-process.md b/docs/production-release-process.md index f202859..b5a8514 100644 --- a/docs/production-release-process.md +++ b/docs/production-release-process.md @@ -235,8 +235,10 @@ checks the graduation report and draft assets and dispatches the exact promotion inputs as a separate decision. Only this path may update the `X.Y` and `latest` image aliases or GitHub's latest release. -Before tagging, confirm two stable-only gates, because the stable tag build -refuses otherwise and a `v*` tag cannot be reused: +The order is: the operator's record, the final readiness report, the stable +pull request, the tag, then promotion. Before tagging, also confirm these +stable-only gates, which are easy to miss; the stable tag build refuses +otherwise, and a `v*` tag cannot be reused: - `scripts/check-distribution-policy.sh --require-decided` passes, that is, every decision listed in `blocks_stable` in `distribution/policy.json` is @@ -245,6 +247,14 @@ refuses otherwise and a `v*` tag cannot be reused: `bpfcompat-validator-static-linux-amd64` sha256 (`scripts/check-validator-matches-candidate.sh`). +Publish no other stable release between the stable pull request's CI and the +`vX.Y.Z` tag build: the stable pin may only name the newest published stable +release below `X.Y.Z`, so one published in that window (a maintenance release, +for example) fails the tag build. Re-run `make check-release-consistency` on +the exact commit immediately before tagging. The stable pull request's docs may +already name `vX.Y.Z`: `scripts/check-doc-pins.sh` accepts that one tag while +it positively does not exist yet. + The stable release pull request and the `vX.Y.Z` tag build cannot pin a tag that does not exist yet or that names their own commit, so they keep the `# bpfcompat-pin: stable` step on the most recent published (non-draft, diff --git a/scripts/check-doc-pins.sh b/scripts/check-doc-pins.sh index 977c6ce..589201a 100755 --- a/scripts/check-doc-pins.sh +++ b/scripts/check-doc-pins.sh @@ -18,6 +18,13 @@ # (docs/external-ci-proof.md) is history, not an instruction, and is left alone. # `@main` and `@latest` are deliberate and exempt, as is any line carrying an # explicit `doc-pins: allow` marker. +# +# The stable release pull request is the one state rule 2 cannot be met in: +# the release-consistency gate requires its docs to name vX.Y.Z, and that tag +# is created only after the PR merges. So while release.yaml says +# release_channel `stable`, a tag-form pin on exactly v is +# accepted when the remote positively has no such tag; an unreachable remote +# still fails. Commit-SHA pins never get this exemption. set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")/.." @@ -45,6 +52,20 @@ tag_commit() { printf '%s' "$sha" } +# The release the stable PR is preparing, or nothing outside the stable channel. +release_value() { awk -F': *' -v k="$1" '$1 == k { gsub(/["\r]/, "", $2); print $2 }' release.yaml; } +preparing="" +[[ "$(release_value release_channel)" == stable ]] && preparing="v$(release_value stable_version)" +prepared="" + +# tag_absent : the remote answered and has no such tag (git ls-remote +# --exit-code returns 2); a remote that cannot be queried is not "absent". +tag_absent() { + local rc=0 + git ls-remote --exit-code --tags origin "refs/tags/$1" >/dev/null 2>&1 || rc=$? + [[ $rc -eq 2 ]] +} + while IFS= read -r hit; do file="${hit%%:*}" rest="${hit#*:}" @@ -78,8 +99,12 @@ while IFS= read -r hit; do elif [[ "$ref" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then version="$ref" if [[ -z "$(tag_commit "$version")" ]]; then - note_fail "$file:$line pins $version, which is not a released tag" - continue + if [[ -n "$preparing" && "$version" == "$preparing" ]] && tag_absent "$version"; then + prepared="$version" + else + note_fail "$file:$line pins $version, which is not a released tag" + continue + fi fi else note_fail "$file:$line pins '$ref', which is neither a release tag nor a commit SHA" @@ -107,4 +132,8 @@ if (( fail )); then exit 1 fi -echo "[doc-pins] ok: every documented action pin resolves to ${!versions[*]}" +if [[ -n "$prepared" ]]; then + echo "[doc-pins] ok: every documented action pin names ${prepared}, the stable release being prepared (its tag does not exist yet)" +else + echo "[doc-pins] ok: every documented action pin resolves to ${!versions[*]}" +fi diff --git a/scripts/check-doc-pins_test.sh b/scripts/check-doc-pins_test.sh new file mode 100755 index 0000000..5113d3c --- /dev/null +++ b/scripts/check-doc-pins_test.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Regression cover for scripts/check-doc-pins.sh. Each case builds a throwaway +# repository whose `origin` is a local bare repository holding the tags, with +# a release.yaml and a README that pins the action. +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT +failures=0 +fail() { echo "[doc-pins-test] FAIL: $*" >&2; failures=$((failures + 1)); } +ok() { echo "[doc-pins-test] ok: $*"; } +gitq() { git -c user.name=t -c user.email=t@example.invalid -c commit.gpgsign=false -c tag.gpgsign=false "$@" >/dev/null 2>&1; } + +# fixture [tags...] +fixture() { + local d="$tmp/$1" channel="$2" stable="$3" pin="$4"; shift 4 + mkdir -p "$d/origin.git" "$d/work/scripts" + gitq -C "$d/origin.git" init --bare + gitq -C "$d/work" init -b main + cp "$ROOT_DIR/scripts/check-doc-pins.sh" "$d/work/scripts/" + printf 'stable_version: %s\nrelease_version: %s\nrelease_channel: %s\n' "$stable" "$stable" "$channel" >"$d/work/release.yaml" + printf '# x\n```yaml\n - %s\n```\n' "$pin" >"$d/work/README.md" + gitq -C "$d/work" add -A + gitq -C "$d/work" commit -m base + for t in "$@"; do gitq -C "$d/work" tag -a "$t" -m "$t"; done + gitq -C "$d/work" remote add origin "$d/origin.git" + gitq -C "$d/work" push origin main --tags + # The CI checkout carries no tags locally; the check must ask the remote. + for t in "$@"; do gitq -C "$d/work" tag -d "$t"; done + W="$d/work" +} +expect() { # [stderr-substring] + local want="$1" desc="$2" needle="${3:-}" rc=0 + bash "$W/scripts/check-doc-pins.sh" >"$tmp/out" 2>&1 || rc=$? + if [[ "$want" == pass && $rc -eq 0 ]] || [[ "$want" == fail && $rc -ne 0 && ( -z "$needle" || $(grep -cF -- "$needle" "$tmp/out") -gt 0 ) ]]; then + ok "$desc" + else + fail "$desc (rc=$rc)"; sed 's/^/ | /' "$tmp/out" >&2 + fi +} + +fixture released prerelease 0.3.7 'uses: Kernel-Guard/bpfcompat@v0.3.7' v0.3.7 +expect pass "prerelease channel: pin on an existing release tag" + +fixture stablepr stable 0.4.0 'uses: Kernel-Guard/bpfcompat@v0.4.0' v0.3.7 +expect pass "stable PR: docs name v0.4.0 while its tag positively does not exist yet" + +fixture stabletagged stable 0.4.0 'uses: Kernel-Guard/bpfcompat@v0.4.0' v0.3.7 v0.4.0 +expect pass "stable channel after the tag: the pin resolves normally" + +fixture otherversion stable 0.4.0 'uses: Kernel-Guard/bpfcompat@v0.4.1' v0.3.7 +expect fail "stable PR: a pin on another missing version still fails" "pins v0.4.1, which is not a released tag" + +fixture rcchannel prerelease 0.4.0 'uses: Kernel-Guard/bpfcompat@v0.4.0' v0.3.7 +expect fail "prerelease channel: a missing tag is never accepted" "pins v0.4.0, which is not a released tag" + +fixture shapin stable 0.4.0 "uses: Kernel-Guard/bpfcompat@$(printf 'a%.0s' {1..40}) # v0.4.0" v0.3.7 +expect fail "stable PR: a SHA pin claiming the unreleased version fails" "claims v0.4.0, but that tag does not exist" + +fixture offline stable 0.4.0 'uses: Kernel-Guard/bpfcompat@v0.4.0' v0.3.7 +git -C "$W" remote set-url origin "$tmp/offline/does-not-exist.git" +expect fail "stable PR: an unreachable remote fails closed" "pins v0.4.0, which is not a released tag" + +if [[ $failures -ne 0 ]]; then echo "[doc-pins-test] $failures failure(s)" >&2; exit 1; fi +echo "[doc-pins-test] PASS" From 227320497f6f19883cdc070dbdaa6682234b2693 Mon Sep 17 00:00:00 2001 From: ErenAri Date: Sun, 18 Oct 2026 23:57:04 +0300 Subject: [PATCH 4/6] release: let SHA-pinned templates keep the previous stable during the stable window DEF-3, found by the independent review of f61c291's simulated stable PR: the docs/integrations templates are pinned by commit SHA (their README makes that policy), so they cannot name vX.Y.Z before its tag exists, and check-doc-pins.sh's one-version rule then refused the stable PR ("docs pin more than one bpfcompat version"). During the stable window -- the stable PR while vX.Y.Z is absent, and the vX.Y.Z tag build itself -- exactly one other documented version may sit beside v: the newest earlier stable release tag on the remote. Every later commit is strict, so the templates are repinned with the consumer canary once the tag exists (now stated in the runbook). check-release-consistency_test.sh no longer hard-codes 0.3.7 as the stable channel's version; it reads stable_version from release.yaml, so the stable PR does not have to edit the test. Signed-off-by: ErenAri --- docs/production-release-process.md | 19 +++++--- scripts/check-doc-pins.sh | 53 ++++++++++++++++++++++- scripts/check-doc-pins_test.sh | 44 +++++++++++++++++++ scripts/check-release-consistency_test.sh | 7 ++- 4 files changed, 114 insertions(+), 9 deletions(-) diff --git a/docs/production-release-process.md b/docs/production-release-process.md index b5a8514..9d145d4 100644 --- a/docs/production-release-process.md +++ b/docs/production-release-process.md @@ -251,9 +251,14 @@ Publish no other stable release between the stable pull request's CI and the `vX.Y.Z` tag build: the stable pin may only name the newest published stable release below `X.Y.Z`, so one published in that window (a maintenance release, for example) fails the tag build. Re-run `make check-release-consistency` on -the exact commit immediately before tagging. The stable pull request's docs may -already name `vX.Y.Z`: `scripts/check-doc-pins.sh` accepts that one tag while -it positively does not exist yet. +the exact commit immediately before tagging, and tag the merge commit as soon +as the pull request merges: from then until the tag exists, `main`'s docs name +a release that is not out yet. In the stable pull request, the tag-form action +references in `README.md` and `docs/` name `vX.Y.Z`, which +`scripts/check-doc-pins.sh` accepts while that tag positively does not exist. +The SHA-pinned integration templates in `docs/integrations/` cannot name it +yet; they stay on the most recent earlier stable release during the stable +pull request and the tag build. The stable release pull request and the `vX.Y.Z` tag build cannot pin a tag that does not exist yet or that names their own commit, so they keep the @@ -263,9 +268,11 @@ accepts that pin only while `vX.Y.Z` is absent or resolves to the commit under check; a tag or GitHub release lookup it cannot complete fails the check. Once the new stable tag exists, repin the `# bpfcompat-pin: stable` step in -`.github/workflows/consumer-canary.yml` to its commit SHA and update the -`# vX.Y.Z` comment beside it. `scripts/check-canary-pins.sh` runs inside the -release-consistency gate and fails every later commit until that is done, +`.github/workflows/consumer-canary.yml` and the SHA-pinned templates in +`docs/integrations/` to its commit SHA, and update the `# vX.Y.Z` comments +beside them. `scripts/check-canary-pins.sh` (inside the release-consistency +gate) and `scripts/check-doc-pins.sh` fail every later commit until that is +done, because a canary left on the previous release stops testing the path the documentation now sends every reader down. The `# bpfcompat-pin: candidate` step is not enforced while `release_channel` is `stable`; repin it with the diff --git a/scripts/check-doc-pins.sh b/scripts/check-doc-pins.sh index 589201a..f0584be 100755 --- a/scripts/check-doc-pins.sh +++ b/scripts/check-doc-pins.sh @@ -25,6 +25,13 @@ # release_channel `stable`, a tag-form pin on exactly v is # accepted when the remote positively has no such tag; an unreachable remote # still fails. Commit-SHA pins never get this exemption. +# +# The integration templates are pinned by commit SHA (docs/integrations), so +# they cannot name vX.Y.Z until its tag exists. During the same stable window +# -- the stable PR while vX.Y.Z is absent, and the vX.Y.Z tag build itself -- +# rule 3 allows exactly one other version beside v: the most +# recent earlier stable release tag (vA.B.C < X.Y.Z). Every later commit is +# strict again, so the templates are repinned once the tag exists. set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")/.." @@ -58,6 +65,35 @@ preparing="" [[ "$(release_value release_channel)" == stable ]] && preparing="v$(release_value stable_version)" prepared="" +# version_lt A B: X.Y.Z version A sorts strictly before X.Y.Z version B. +version_lt() { + local -a a b + local i + IFS=. read -r -a a <<<"$1" + IFS=. read -r -a b <<<"$2" + for i in 0 1 2; do + if (( 10#${a[i]} != 10#${b[i]} )); then + (( 10#${a[i]} < 10#${b[i]} )) + return + fi + done + return 1 +} + +# previous_stable : the newest vA.B.C tag on the remote below it, or +# nothing when the remote cannot be listed. +previous_stable() { + local tags tag newest="" + tags="$(git ls-remote --tags --refs origin 'refs/tags/v*' 2>/dev/null)" || return 0 + while read -r _ tag; do + tag="${tag#refs/tags/}" + [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || continue + version_lt "${tag#v}" "${1#v}" || continue + if [[ -z "$newest" ]] || version_lt "${newest#v}" "${tag#v}"; then newest="$tag"; fi + done <<<"$tags" + printf '%s' "$newest" +} + # tag_absent : the remote answered and has no such tag (git ls-remote # --exit-code returns 2); a remote that cannot be queried is not "absent". tag_absent() { @@ -120,7 +156,18 @@ if (( ${#versions[@]} == 0 )); then note_fail "found no 'uses: Kernel-Guard/bpfcompat@...' pins in README.md or docs -- the docs moved, or this check no longer matches them" fi -if (( ${#versions[@]} > 1 )); then +# The stable window: v is being prepared (positively absent) +# or this is its tag build (it resolves to HEAD). +window_previous="" +if [[ -n "$preparing" && ${#versions[@]} -eq 2 && -n "${versions[$preparing]:-}" ]]; then + head_commit="$(git rev-parse -q --verify 'HEAD^{commit}' 2>/dev/null || true)" + if [[ -n "$prepared" ]] || [[ -n "$head_commit" && "$(tag_commit "$preparing")" == "$head_commit" ]]; then + window_previous="$(previous_stable "$preparing")" + fi +fi +if (( ${#versions[@]} == 2 )) && [[ -n "$window_previous" && -n "${versions[$window_previous]:-}" ]]; then + echo "[doc-pins] stable window for ${preparing}: SHA-pinned templates may stay on ${window_previous} until the tag exists (${versions[$window_previous]% })" +elif (( ${#versions[@]} > 1 )); then note_fail "docs pin more than one bpfcompat version:" for v in "${!versions[@]}"; do echo " $v -> ${versions[$v]}" >&2 @@ -132,7 +179,9 @@ if (( fail )); then exit 1 fi -if [[ -n "$prepared" ]]; then +if [[ -n "$window_previous" ]]; then + echo "[doc-pins] ok: documented pins name ${preparing}, the stable release being prepared or built; SHA-pinned templates stay on ${window_previous} until its tag exists" +elif [[ -n "$prepared" ]]; then echo "[doc-pins] ok: every documented action pin names ${prepared}, the stable release being prepared (its tag does not exist yet)" else echo "[doc-pins] ok: every documented action pin resolves to ${!versions[*]}" diff --git a/scripts/check-doc-pins_test.sh b/scripts/check-doc-pins_test.sh index 5113d3c..ade9c52 100755 --- a/scripts/check-doc-pins_test.sh +++ b/scripts/check-doc-pins_test.sh @@ -30,6 +30,21 @@ fixture() { for t in "$@"; do gitq -C "$d/work" tag -d "$t"; done W="$d/work" } +# template [file]: an integration template SHA-pinned to , +# committed on top of the fixture (the version's tag is on the base commit). +template() { + mkdir -p "$W/docs/integrations" + printf 'jobs:\n t:\n steps:\n - uses: Kernel-Guard/bpfcompat@%s # %s\n' \ + "$(git -C "$W" rev-list --max-parents=0 HEAD)" "$1" >"$W/docs/integrations/${2:-t}.yml" + gitq -C "$W" add -A + gitq -C "$W" commit -m "template $1" +} +# tag_head : tag the current HEAD on the remote (the tag build of it). +tag_head() { + gitq -C "$W" tag -a "$1" -m "$1" + gitq -C "$W" push origin "$1" + gitq -C "$W" tag -d "$1" +} expect() { # [stderr-substring] local want="$1" desc="$2" needle="${3:-}" rc=0 bash "$W/scripts/check-doc-pins.sh" >"$tmp/out" 2>&1 || rc=$? @@ -62,5 +77,34 @@ fixture offline stable 0.4.0 'uses: Kernel-Guard/bpfcompat@v0.4.0' v0.3.7 git -C "$W" remote set-url origin "$tmp/offline/does-not-exist.git" expect fail "stable PR: an unreachable remote fails closed" "pins v0.4.0, which is not a released tag" +# The integration templates are SHA-pinned (docs/integrations/README.md), so +# they cannot name vX.Y.Z before its tag exists: during the stable release +# window they stay on the most recent earlier stable release. +fixture tplpr stable 0.4.0 'uses: Kernel-Guard/bpfcompat@v0.4.0' v0.3.6 v0.3.7 +template v0.3.7 +expect pass "stable PR: templates keep their SHA pin on the newest earlier stable" + +fixture tplold stable 0.4.0 'uses: Kernel-Guard/bpfcompat@v0.4.0' v0.3.6 v0.3.7 +template v0.3.6 +expect fail "stable PR: a template on an older stable than the newest fails" "docs pin more than one bpfcompat version" + +fixture tplthree stable 0.4.0 'uses: Kernel-Guard/bpfcompat@v0.4.0' v0.3.6 v0.3.7 +template v0.3.7 a +template v0.3.6 b +expect fail "stable PR: three documented versions fail" "docs pin more than one bpfcompat version" + +fixture tplbuild stable 0.4.0 'uses: Kernel-Guard/bpfcompat@v0.4.0' v0.3.7 +template v0.3.7 +tag_head v0.4.0 +expect pass "tag build: the new tag is HEAD, templates still on the previous stable" + +fixture tplafter stable 0.4.0 'uses: Kernel-Guard/bpfcompat@v0.4.0' v0.3.7 v0.4.0 +template v0.3.7 +expect fail "after the tag: templates must be repinned to the new release" "docs pin more than one bpfcompat version" + +fixture tplrc prerelease 0.3.7 'uses: Kernel-Guard/bpfcompat@v0.3.7' v0.3.6 v0.3.7 +template v0.3.6 +expect fail "prerelease channel: two documented versions fail as before" "docs pin more than one bpfcompat version" + if [[ $failures -ne 0 ]]; then echo "[doc-pins-test] $failures failure(s)" >&2; exit 1; fi echo "[doc-pins-test] PASS" diff --git a/scripts/check-release-consistency_test.sh b/scripts/check-release-consistency_test.sh index b0c907d..b3576ac 100755 --- a/scripts/check-release-consistency_test.sh +++ b/scripts/check-release-consistency_test.sh @@ -26,6 +26,9 @@ restore() { rm -rf "$tmp" } trap restore EXIT +current_stable="$( + awk -F': ' '$1 == "stable_version" {print $2; exit}' release.yaml +)" current_release="$( awk -F': ' '$1 == "release_version" {print $2; exit}' release.yaml )" @@ -53,7 +56,9 @@ EOF BPFCOMPAT_RELEASE_METADATA=release.yaml "$script" >"$tmp/current.log" grep -Fq 'channel=prerelease' "$tmp/current.log" -write_metadata 0.3.7 0.3.7 stable +# The current stable release as a stable channel: docs and canary pins must +# already agree with it (the stable PR changes stable_version, so this follows). +write_metadata "$current_stable" "$current_stable" stable BPFCOMPAT_RELEASE_METADATA="$tmp/release.yaml" "$script" >"$tmp/stable.log" grep -Fq 'channel=stable' "$tmp/stable.log" From b9149e7188df116b553ad3664f6103ed38f4519e Mon Sep 17 00:00:00 2001 From: ErenAri Date: Mon, 19 Oct 2026 02:37:40 +0300 Subject: [PATCH 5/6] release: bind the stable window to the canary pin; fix the channel test Review of 2273204 (full stable-sequence simulation): - DEF-4: check-release-consistency_test.sh still asserted that the current tree is on the prerelease channel, so the stable PR's required CI, the push to main and the v0.4.0 tag build's quality gates would fail. It now reads release_channel from release.yaml. - The stable window's second documented version is now the release the consumer canary's '# bpfcompat-pin: stable' step names, which check-canary-pins.sh verifies is the newest published stable release, so the two checks cannot disagree (a stable tag without a published release no longer qualifies), and no remote tag listing is involved. - Only commit-SHA pins count toward that second version; a tag-form pin on the previous release is refused as before. - The ci.yml step comment and the runbook describe the window precisely; docs name an unreleased version until promotion, not only until the tag. Signed-off-by: ErenAri --- .github/workflows/ci.yml | 7 ++-- docs/production-release-process.md | 9 ++--- scripts/check-doc-pins.sh | 40 ++++++++++++++--------- scripts/check-doc-pins_test.sh | 24 ++++++++++++++ scripts/check-release-consistency_test.sh | 3 +- 5 files changed, 59 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7bf7091..cae4cb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,9 +62,10 @@ jobs: # weeks because nothing checked the pins a reader copy-pastes. run: bash scripts/check-doc-pins.sh - name: Documented action pins regression - # The stable release PR documents vX.Y.Z before its tag exists; only - # that tag, only in the stable channel, and only when the remote - # positively lacks it. + # The stable release window: docs may name vX.Y.Z before its tag + # exists (only that tag, only in the stable channel, only when the + # remote positively lacks it), and SHA-pinned templates may stay on + # the release the consumer canary's stable pin names until the tag. run: bash scripts/check-doc-pins_test.sh - name: Action OCI-artifact detection regression # Guards the fix that lets the action accept remote OCI references diff --git a/docs/production-release-process.md b/docs/production-release-process.md index 9d145d4..5f4fa37 100644 --- a/docs/production-release-process.md +++ b/docs/production-release-process.md @@ -252,13 +252,14 @@ Publish no other stable release between the stable pull request's CI and the release below `X.Y.Z`, so one published in that window (a maintenance release, for example) fails the tag build. Re-run `make check-release-consistency` on the exact commit immediately before tagging, and tag the merge commit as soon -as the pull request merges: from then until the tag exists, `main`'s docs name -a release that is not out yet. In the stable pull request, the tag-form action +as the pull request merges: from then until promotion, `main`'s docs name a +release that is not published yet. In the stable pull request, the tag-form action references in `README.md` and `docs/` name `vX.Y.Z`, which `scripts/check-doc-pins.sh` accepts while that tag positively does not exist. The SHA-pinned integration templates in `docs/integrations/` cannot name it -yet; they stay on the most recent earlier stable release during the stable -pull request and the tag build. +yet; during the stable pull request and the tag build they stay, pinned by +SHA, on the release the consumer canary's `# bpfcompat-pin: stable` step names +(the most recent published stable release before `X.Y.Z`). The stable release pull request and the `vX.Y.Z` tag build cannot pin a tag that does not exist yet or that names their own commit, so they keep the diff --git a/scripts/check-doc-pins.sh b/scripts/check-doc-pins.sh index f0584be..4189989 100755 --- a/scripts/check-doc-pins.sh +++ b/scripts/check-doc-pins.sh @@ -29,9 +29,11 @@ # The integration templates are pinned by commit SHA (docs/integrations), so # they cannot name vX.Y.Z until its tag exists. During the same stable window # -- the stable PR while vX.Y.Z is absent, and the vX.Y.Z tag build itself -- -# rule 3 allows exactly one other version beside v: the most -# recent earlier stable release tag (vA.B.C < X.Y.Z). Every later commit is -# strict again, so the templates are repinned once the tag exists. +# rule 3 allows exactly one other version beside v, and only +# as commit-SHA pins: the earlier release the consumer canary's stable pin +# names (scripts/check-canary-pins.sh verifies that pin is the newest published +# stable release before X.Y.Z). Every later commit is strict again, so the +# templates are repinned with the canary once the tag exists. set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")/.." @@ -64,6 +66,7 @@ release_value() { awk -F': *' -v k="$1" '$1 == k { gsub(/["\r]/, "", $2); print preparing="" [[ "$(release_value release_channel)" == stable ]] && preparing="v$(release_value stable_version)" prepared="" +declare -A tag_form=() # version_lt A B: X.Y.Z version A sorts strictly before X.Y.Z version B. version_lt() { @@ -80,18 +83,18 @@ version_lt() { return 1 } -# previous_stable : the newest vA.B.C tag on the remote below it, or -# nothing when the remote cannot be listed. -previous_stable() { - local tags tag newest="" - tags="$(git ls-remote --tags --refs origin 'refs/tags/v*' 2>/dev/null)" || return 0 - while read -r _ tag; do - tag="${tag#refs/tags/}" - [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || continue - version_lt "${tag#v}" "${1#v}" || continue - if [[ -z "$newest" ]] || version_lt "${newest#v}" "${tag#v}"; then newest="$tag"; fi - done <<<"$tags" - printf '%s' "$newest" +# canary_stable_version: the `# vA.B.C` comment on the action pin that follows +# the `# bpfcompat-pin: stable` marker in the consumer canary, or nothing. +canary_stable_version() { + awk ' + /#[[:space:]]*bpfcompat-pin:[[:space:]]*stable[[:space:]]*$/ { want = 1; next } + want && /uses:[[:space:]]*Kernel-Guard\/bpfcompat@/ { + if (match($0, /#[[:space:]]*v[0-9]+\.[0-9]+\.[0-9]+[[:space:]]*$/)) { + v = substr($0, RSTART, RLENGTH); sub(/^#[[:space:]]*/, "", v); sub(/[[:space:]]*$/, "", v); print v + } + exit + } + ' .github/workflows/consumer-canary.yml 2>/dev/null || true } # tag_absent : the remote answered and has no such tag (git ls-remote @@ -134,6 +137,7 @@ while IFS= read -r hit; do fi elif [[ "$ref" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then version="$ref" + tag_form["$version"]=1 if [[ -z "$(tag_commit "$version")" ]]; then if [[ -n "$preparing" && "$version" == "$preparing" ]] && tag_absent "$version"; then prepared="$version" @@ -162,7 +166,11 @@ window_previous="" if [[ -n "$preparing" && ${#versions[@]} -eq 2 && -n "${versions[$preparing]:-}" ]]; then head_commit="$(git rev-parse -q --verify 'HEAD^{commit}' 2>/dev/null || true)" if [[ -n "$prepared" ]] || [[ -n "$head_commit" && "$(tag_commit "$preparing")" == "$head_commit" ]]; then - window_previous="$(previous_stable "$preparing")" + candidate_previous="$(canary_stable_version)" + if [[ "$candidate_previous" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]] && + version_lt "${BASH_REMATCH[1]}" "${preparing#v}" && [[ -z "${tag_form[$candidate_previous]:-}" ]]; then + window_previous="$candidate_previous" + fi fi fi if (( ${#versions[@]} == 2 )) && [[ -n "$window_previous" && -n "${versions[$window_previous]:-}" ]]; then diff --git a/scripts/check-doc-pins_test.sh b/scripts/check-doc-pins_test.sh index ade9c52..173e5ea 100755 --- a/scripts/check-doc-pins_test.sh +++ b/scripts/check-doc-pins_test.sh @@ -21,6 +21,13 @@ fixture() { cp "$ROOT_DIR/scripts/check-doc-pins.sh" "$d/work/scripts/" printf 'stable_version: %s\nrelease_version: %s\nrelease_channel: %s\n' "$stable" "$stable" "$channel" >"$d/work/release.yaml" printf '# x\n```yaml\n - %s\n```\n' "$pin" >"$d/work/README.md" + # The consumer canary's stable pin names the release the stable window keeps + # (CANARY, default v0.3.7); NO_CANARY leaves the file out. + if [[ -z "${NO_CANARY:-}" ]]; then + mkdir -p "$d/work/.github/workflows" + printf 'jobs:\n s:\n steps:\n # bpfcompat-pin: stable\n - uses: Kernel-Guard/bpfcompat@%s # %s\n' \ + "$(printf 'a%.0s' {1..40})" "${CANARY:-v0.3.7}" >"$d/work/.github/workflows/consumer-canary.yml" + fi gitq -C "$d/work" add -A gitq -C "$d/work" commit -m base for t in "$@"; do gitq -C "$d/work" tag -a "$t" -m "$t"; done @@ -102,6 +109,23 @@ fixture tplafter stable 0.4.0 'uses: Kernel-Guard/bpfcompat@v0.4.0' v0.3.7 v0.4. template v0.3.7 expect fail "after the tag: templates must be repinned to the new release" "docs pin more than one bpfcompat version" +fixture tplform stable 0.4.0 'uses: Kernel-Guard/bpfcompat@v0.4.0' v0.3.7 +mkdir -p "$W/docs"; printf 'jobs:\n t:\n steps:\n - uses: "Kernel-Guard/bpfcompat@v0.3.7"\n' >"$W/docs/stale.yml" +expect fail "stable PR: a tag-form pin on the previous stable is not a SHA-pinned template" "docs pin more than one bpfcompat version" + +CANARY=v0.3.6 fixture tplcanary stable 0.4.0 'uses: Kernel-Guard/bpfcompat@v0.4.0' v0.3.6 v0.3.7 +template v0.3.7 +expect fail "stable PR: templates must stay on the release the canary pins" "docs pin more than one bpfcompat version" + +fixture tplbuildwrong stable 0.4.0 'uses: Kernel-Guard/bpfcompat@v0.4.0' v0.3.6 v0.3.7 +template v0.3.6 +tag_head v0.4.0 +expect fail "tag build: templates on another release than the canary's fail" "docs pin more than one bpfcompat version" + +NO_CANARY=1 fixture tplnocanary stable 0.4.0 'uses: Kernel-Guard/bpfcompat@v0.4.0' v0.3.7 +template v0.3.7 +expect fail "stable PR: without a canary stable pin there is no window" "docs pin more than one bpfcompat version" + fixture tplrc prerelease 0.3.7 'uses: Kernel-Guard/bpfcompat@v0.3.7' v0.3.6 v0.3.7 template v0.3.6 expect fail "prerelease channel: two documented versions fail as before" "docs pin more than one bpfcompat version" diff --git a/scripts/check-release-consistency_test.sh b/scripts/check-release-consistency_test.sh index b3576ac..495f9f6 100755 --- a/scripts/check-release-consistency_test.sh +++ b/scripts/check-release-consistency_test.sh @@ -54,7 +54,8 @@ EOF } BPFCOMPAT_RELEASE_METADATA=release.yaml "$script" >"$tmp/current.log" -grep -Fq 'channel=prerelease' "$tmp/current.log" +current_channel="$(awk -F': ' '$1 == "release_channel" {print $2; exit}' release.yaml)" +grep -Fq "channel=${current_channel}" "$tmp/current.log" # The current stable release as a stable channel: docs and canary pins must # already agree with it (the stable PR changes stable_version, so this follows). From 4cb4516bda42cf4877397518c5f780ded8dfe54a Mon Sep 17 00:00:00 2001 From: ErenAri Date: Mon, 19 Oct 2026 02:55:00 +0300 Subject: [PATCH 6/6] release: read the canary's first version comment, as check-canary-pins does Review of b9149e7: on a canary line with two version comments ("# v0.3.7 # v0.3.6"), check-canary-pins.sh read the first and check-doc-pins.sh the last, so the stable window could admit templates on a different release than the verified canary pin. Both now read the first, and a test pins it. Signed-off-by: ErenAri --- scripts/check-doc-pins.sh | 6 ++++-- scripts/check-doc-pins_test.sh | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/check-doc-pins.sh b/scripts/check-doc-pins.sh index 4189989..8c8eebe 100755 --- a/scripts/check-doc-pins.sh +++ b/scripts/check-doc-pins.sh @@ -89,8 +89,10 @@ canary_stable_version() { awk ' /#[[:space:]]*bpfcompat-pin:[[:space:]]*stable[[:space:]]*$/ { want = 1; next } want && /uses:[[:space:]]*Kernel-Guard\/bpfcompat@/ { - if (match($0, /#[[:space:]]*v[0-9]+\.[0-9]+\.[0-9]+[[:space:]]*$/)) { - v = substr($0, RSTART, RLENGTH); sub(/^#[[:space:]]*/, "", v); sub(/[[:space:]]*$/, "", v); print v + # The first "# v..." comment, exactly as scripts/check-canary-pins.sh + # reads it, so the two checks always see the same version. + if (match($0, /#[[:space:]]*v[0-9][0-9A-Za-z._-]*/)) { + v = substr($0, RSTART, RLENGTH); sub(/^#[[:space:]]*/, "", v); print v } exit } diff --git a/scripts/check-doc-pins_test.sh b/scripts/check-doc-pins_test.sh index 173e5ea..f64f6fb 100755 --- a/scripts/check-doc-pins_test.sh +++ b/scripts/check-doc-pins_test.sh @@ -122,6 +122,10 @@ template v0.3.6 tag_head v0.4.0 expect fail "tag build: templates on another release than the canary's fail" "docs pin more than one bpfcompat version" +CANARY='v0.3.7 # v0.3.6' fixture tpltwocomments stable 0.4.0 'uses: Kernel-Guard/bpfcompat@v0.4.0' v0.3.6 v0.3.7 +template v0.3.6 +expect fail "stable PR: the canary's first version comment counts, as in check-canary-pins" "docs pin more than one bpfcompat version" + NO_CANARY=1 fixture tplnocanary stable 0.4.0 'uses: Kernel-Guard/bpfcompat@v0.4.0' v0.3.7 template v0.3.7 expect fail "stable PR: without a canary stable pin there is no window" "docs pin more than one bpfcompat version"