#!/bin/bash # 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 # # * a fresh install; # * an upgrade from what canary serves today, on a canary room; # * an upgrade from what stable serves today, on a stable room; # * an upgrade from each FROM_VERSION, on a stable room — the release a # room is really still on, which can be several behind stable. # # The releases it upgrades from are downloaded over HTTPS the way a # classroom downloads them, and each is checked against the SHA-256 its # manifest publishes, or, for a FROM_VERSION, against the version dpkg # reads out of it. Nothing here needs a credential or writes to the # update server, so a CI runner can run it on code nobody has reviewed. # # Every run is one line in RELEASE_GATE_REPORT (default: printed only), # so the result can be attached to the release it describes. # # 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 # with an empty hash, and match. shopt -s inherit_errexit ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" source "$ROOT/tools/release-lib.sh" 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=("$@") [[ -f "$candidate" ]] || { echo "Package not found: $candidate" >&2; exit 1; } version="$(release_package_version "$candidate")" release_require_version "$version" # The name a release is published under is the version dpkg reads out of # it. A file called 0.2.6.6 that says it is 0.2.6.5 is the wrong file. if [[ "$(basename "$candidate")" != "$(release_package_name "$version")" ]]; then echo "$(basename "$candidate") says it is $version." >&2 exit 1 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 record() { printf '%s\n' "$1" [[ -z "$report" ]] || printf '%s\n' "$1" >> "$report" } [[ -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 # proven to be what the manifest promises. fetch_channel() { local channel="$1" manifest from from_hash name got manifest="$(curl -fsS -H 'Cache-Control: no-cache' "$RELEASE_URL/$channel/manifest.json")" read -r from from_hash < <(python3 -c 'import json, sys m = json.loads(sys.argv[1]) print(m["version"], m["sha256"])' "$manifest") release_require_version "$from" [[ "$from_hash" =~ ^[0-9a-f]{64}$ ]] name="$(release_package_name "$from")" curl -fsS -o "$work/$name" "$RELEASE_URL/packages/$name" got="$(sha256sum "$work/$name" | awk '{print $1}')" if [[ "$got" != "$from_hash" ]]; then echo "$channel serves $name with sha256 $got; its manifest says $from_hash" >&2 return 1 fi printf '%s\n' "$work/$name" } # A published release by version, for the rooms still on an older one. fetch_version() { local from="$1" name release_require_version "$from" name="$(release_package_name "$from")" curl -fsS -o "$work/$name" "$RELEASE_URL/packages/$name" if [[ "$(release_package_version "$work/$name")" != "$from" ]]; then echo "$name does not say it is $from" >&2 return 1 fi printf '%s\n' "$work/$name" } run() { local label="$1" started shift started=$SECONDS echo echo "== $label" "$@" record "PASS $label ($(( (SECONDS - started) / 60 )) min)" } # A new machine is a stable room: that is what a fresh device.conf says, # so that is the manifest its updater is asked about. run "fresh install of $version (a new machine, on stable)" \ env VALIDATION_CHANNEL=stable \ "$ROOT/tools/validate-trixie-release.sh" "$candidate" for channel in canary stable; do previous="$(fetch_channel "$channel")" 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 run "upgrade $from -> $version on a $channel room ($channel serves $from)" \ env VALIDATION_CHANNEL="$channel" \ "$ROOT/tools/validate-trixie-release.sh" "$candidate" "$previous" done for from in "${extra_from[@]}"; do previous="$(fetch_version "$from")" run "upgrade $from -> $version on a stable room" \ env VALIDATION_CHANNEL=stable \ "$ROOT/tools/validate-trixie-release.sh" "$candidate" "$previous" done 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