Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ 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 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
# (ghcr.io/...) as `artifact`. Extracts is_remote_artifact() from
Expand Down
60 changes: 48 additions & 12 deletions docs/production-release-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,17 +228,53 @@ 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.

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
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`).

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, and tag the merge commit as soon
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; 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
`# 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`.
`.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
next `X.Y.Z-rc.1`.
122 changes: 118 additions & 4 deletions scripts/check-canary-pins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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<release_version> positively does not exist
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand All @@ -212,6 +221,111 @@ published_prerelease() {
[[ "$state" == "false true" ]]
}

# Every GitHub release, one "<tag> <draft> <prerelease>" 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<stable_version> 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:-<no comment>}' 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 <family> <rc-number>: while v<family>-rc.<n> 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.
Expand Down
Loading
Loading