diff --git a/tools/release-gate.sh b/tools/release-gate.sh index dca36e1..945621f 100755 --- a/tools/release-gate.sh +++ b/tools/release-gate.sh @@ -2,6 +2,8 @@ # THE RELEASE GATE: may this exact file go to a classroom? # # tools/release-gate.sh PACKAGE.deb [FROM_VERSION...] +# tools/release-gate.sh --ci PACKAGE.deb for CI; see below +# tools/release-gate.sh --still-current REPORT PACKAGE.deb # # Runs tools/validate-trixie-release.sh against PACKAGE as # @@ -22,6 +24,18 @@ # # It stops at the first failure: a gate that carries on reports a pass # for a machine it never finished building. +# +# --ci is for a commit, not a release. Between releases master's VERSION +# is what canary already serves, and no room can upgrade to a version it +# has; such an upgrade is recorded as SKIP, with why, instead of failing +# every commit. Its report says it is a CI report and can never stand in +# for a release's. +# +# --still-current REPORT PACKAGE asks whether a gate report may still be +# relied on to publish PACKAGE: it has to be a release gate's report, for +# exactly these bytes, that passed, and whose upgrades were from exactly +# what canary and stable serve now. A report made before either channel +# moved is not current, and the gate has to be run again. Exit 0 or 1. set -euo pipefail # The fetches below run in $(...), which otherwise ignores set -e: a # manifest that failed to download would go on to compare an empty hash @@ -31,11 +45,27 @@ shopt -s inherit_errexit ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" source "$ROOT/tools/release-lib.sh" -if [[ $# -lt 1 ]]; then - echo "Usage: $0 PACKAGE.deb [FROM_VERSION...]" >&2 +usage() { + echo "Usage: $0 [--ci] PACKAGE.deb [FROM_VERSION...]" >&2 + echo " $0 --still-current REPORT PACKAGE.deb" >&2 exit 2 +} + +mode=release + +case "${1:-}" in + --ci) mode=ci; shift ;; + --still-current) mode=check; shift ;; +esac + +if [[ "$mode" == check ]]; then + [[ $# -eq 2 ]] || usage + gate_report="$1" + shift fi +[[ $# -ge 1 ]] || usage + candidate="$(realpath "$1")" shift extra_from=("$@") @@ -53,6 +83,36 @@ if [[ "$(basename "$candidate")" != "$(release_package_name "$version")" ]]; the fi hash="$(sha256sum "$candidate" | awk '{print $1}')" + +# The version a channel serves, read the way a classroom reads it. +served() { + curl -fsS -H 'Cache-Control: no-cache' "$RELEASE_URL/$1/manifest.json" \ + | python3 -c 'import json, sys; print(json.load(sys.stdin)["version"])' +} + +if [[ "$mode" == check ]]; then + stale() { echo "The gate report $gate_report cannot be relied on: $1" >&2; exit 1; } + name="$(basename "$candidate")" + + [[ -f "$gate_report" ]] || stale "there is no such file" + grep -qx "release gate for $name" "$gate_report" || stale "it is not about $name" + grep -qx "mode release" "$gate_report" || stale "it is not a release gate's report" + grep -qx "sha256 $hash" "$gate_report" || stale "it is about other bytes than $hash" + [[ "$(tail -n 1 "$gate_report")" == *": $name passed the release gate" ]] \ + || stale "it does not end in a pass" + ! grep -q '^SKIP' "$gate_report" || stale "it skipped something" + grep -q "^PASS fresh install of $version " "$gate_report" || stale "it has no fresh install" + + for channel in canary stable; do + now="$(served "$channel")" + grep -qF "PASS upgrade $now -> $version on a $channel room ($channel serves $now)" "$gate_report" \ + || stale "$channel serves $now now, and the report did not upgrade from that" + done + + echo "The gate report for $name, sha256 $hash, is current: canary and stable are where it left them." + exit 0 +fi + report="${RELEASE_GATE_REPORT:-}" work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT @@ -65,6 +125,7 @@ record() { [[ -z "$report" ]] || : > "$report" record "release gate for $(basename "$candidate")" record "sha256 $hash" +record "mode $mode" record "started $(date -u +%Y-%m-%dT%H:%M:%SZ)" # A channel's current package, fetched as a classroom fetches it and @@ -129,6 +190,11 @@ for channel in canary stable; do from="$(release_package_version "$previous")" if ! release_version_gt "$version" "$from"; then + if [[ "$mode" == ci ]]; then + record "SKIP upgrade $from -> $version on a $channel room: $channel serves $from, not older than this commit's VERSION (a CI build, not a release)" + continue + fi + echo "$channel is on $from; $version is not newer, so no room could take it." >&2 exit 1 fi @@ -146,4 +212,8 @@ for from in "${extra_from[@]}"; do "$ROOT/tools/validate-trixie-release.sh" "$candidate" "$previous" done -record "finished $(date -u +%Y-%m-%dT%H:%M:%SZ): $(basename "$candidate") passed the release gate" +if [[ "$mode" == ci ]]; then + record "finished $(date -u +%Y-%m-%dT%H:%M:%SZ): $(basename "$candidate") passed the CI gate, which is not a release gate" +else + record "finished $(date -u +%Y-%m-%dT%H:%M:%SZ): $(basename "$candidate") passed the release gate" +fi diff --git a/update_canary.sh b/update_canary.sh index bf664a4..eb6b132 100755 --- a/update_canary.sh +++ b/update_canary.sh @@ -75,7 +75,16 @@ fi # to fail here, not on a Sunday. See tools/release-gate.sh. hash="$(sha256sum "$package_path" | awk '{print $1}')" -"$ROOT/tools/release-gate.sh" "$package_path" ${RELEASE_GATE_FROM:-} +# RELEASE_GATE_REPORT_PASSED=REPORT relies on a gate already run on this +# file instead — Preview's gate job, which has Docker, where the publish +# job has the key and no Docker. The report is only accepted if it is a +# pass for exactly these bytes, from exactly what canary and stable serve +# now; anything else and nothing is published. +if [[ -n "${RELEASE_GATE_REPORT_PASSED:-}" ]]; then + "$ROOT/tools/release-gate.sh" --still-current "$RELEASE_GATE_REPORT_PASSED" "$package_path" +else + "$ROOT/tools/release-gate.sh" "$package_path" ${RELEASE_GATE_FROM:-} +fi # Checked again after the gate, which takes long enough for somebody else # to have published in the meantime — or for the file to have changed.