Files
ekklesia 719c0f7718
CI / checks (push) Successful in 2m59s
CI / package and container gate (push) Canceled after 6m22s
test: isolate release CI from Gitea Actions environment
2026-09-30 23:53:15 -05:00

308 lines
15 KiB
Bash
Executable File

#!/bin/bash
# Preview, Stable and import, end to end, as the Gitea workflows run them.
#
# Nothing here reaches the real update server or the real Gitea. The
# update server is a directory served over HTTP on the loopback, with ssh
# and rsync stand-ins that act on it (as in test-release-versions.sh);
# Gitea is tests/fake-gitea.py. What runs is the real thing:
# tools/release-ci.sh, update_canary.sh, update_stable.sh,
# tools/release-gate.sh --still-current and tools/gitea-release.
#
# The container gate itself needs Docker and is not run here: a gate
# report stands in for it, and the question is whether a report that
# must not be relied on ever is.
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WORK="$(mktemp -d)"
PIDS=()
cleanup() {
for pid in "${PIDS[@]}"; do kill "$pid" 2>/dev/null; done
rm -rf "$WORK"
}
trap cleanup EXIT
passed=0
failed=0
check() {
local label="$1"
shift
if "$@" >"$WORK/last.log" 2>&1; then
echo " ok $label"
passed=$((passed + 1))
else
echo " FAIL $label"
sed 's/^/ /' "$WORK/last.log" | tail -n 8
failed=$((failed + 1))
fi
}
refuses() {
! "$@"
}
# Refused, and for the reason given: a refusal for some other reason
# would pass a test that means nothing.
refuses_saying() {
local reason="$1" out
shift
out="$("$@" 2>&1)" && return 1
grep -qF -- "$reason" <<< "$out" || { printf '%s\n' "$out"; return 1; }
}
fake_deb() {
local version="$1" out="$2" stage="$WORK/stage-$version-$RANDOM"
mkdir -p "$stage/DEBIAN"
printf 'Package: ekklesia-classroom\nVersion: %s\nArchitecture: amd64\nMaintainer: test\nDescription: test %s\n' \
"$version" "${3:-}" > "$stage/DEBIAN/control"
dpkg-deb --root-owner-group --build "$stage" "$out" >/dev/null
}
sha() { sha256sum "$1" | awk '{print $1}'; }
# ---------------------------------------------------------------------------
# The update server: stable on 0.2.6.4, canary on 0.2.6.5.
SERVER="$WORK/server"
mkdir -p "$SERVER/packages" "$SERVER/stable" "$SERVER/canary" "$WORK/bin"
for version in 0.2.2 0.2.6.4 0.2.6.5; do
fake_deb "$version" "$SERVER/packages/ekklesia-classroom_${version}_amd64.deb"
done
point() {
local channel="$1" version="$2" name="ekklesia-classroom_$2_amd64.deb"
printf '{\n "version": "%s",\n "package": "/packages/%s",\n "sha256": "%s",\n "requires_reboot": true\n}\n' \
"$version" "$name" "$(sha "$SERVER/packages/$name")" > "$SERVER/$channel/manifest.json"
ln -sfn "../packages/$name" "$SERVER/$channel/latest.deb"
}
point stable 0.2.6.4
point canary 0.2.6.5
cat > "$WORK/bin/ssh" <<'EOF'
#!/bin/bash
# ssh [-F CONFIG] HOST COMMAND: run COMMAND here, and note which
# configuration it was given ("-" for the machine's own).
config=-
if [[ "$1" == -F ]]; then config="$2"; shift 2; fi
echo "ssh $config" >> "${SSH_LOG:-/dev/null}"
shift
exec bash -c "$*"
EOF
cat > "$WORK/bin/rsync" <<'EOF'
#!/bin/bash
keep=0
args=()
while [[ $# -gt 0 ]]; do
case "$1" in
--ignore-existing) keep=1 ;;
-e) echo "rsync $2" >> "${SSH_LOG:-/dev/null}"; shift ;;
-*) ;;
*) args+=("$1") ;;
esac
shift
done
source="${args[0]}"
dest="${args[1]#*:}"
[[ -d "$dest" ]] && dest="$dest/$(basename "$source")"
[[ "$keep" == 1 && -e "$dest" ]] && exit 0
cp "$source" "$dest"
EOF
chmod +x "$WORK/bin/ssh" "$WORK/bin/rsync"
port() { python3 -c 'import socket; s = socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1])'; }
HTTP_PORT="$(port)"
python3 -m http.server --bind 127.0.0.1 --directory "$SERVER" "$HTTP_PORT" >/dev/null 2>&1 &
PIDS+=($!)
python3 "$ROOT/tests/fake-gitea.py" > "$WORK/gitea-port" 2>/dev/null &
PIDS+=($!)
for _ in $(seq 1 50); do
[[ -s "$WORK/gitea-port" ]] && curl -fsS "http://127.0.0.1:$HTTP_PORT/stable/manifest.json" >/dev/null 2>&1 && break
sleep 0.1
done
export PATH="$WORK/bin:$PATH"
export RELEASE_HOST=stand-in RELEASE_ROOT="$SERVER" RELEASE_URL="http://127.0.0.1:$HTTP_PORT"
export GITEA_URL="http://127.0.0.1:$(cat "$WORK/gitea-port")" GITEA_REPO=church/classroom GITEA_TOKEN=not-a-real-token
unset GITHUB_ACTIONS GITEA_ACTIONS GITHUB_RUN_NUMBER GITHUB_REF GITHUB_OUTPUT RELEASE_SSH_KEY
CI="$ROOT/tools/release-ci.sh"
GITEA="$ROOT/tools/gitea-release"
COMMIT="$(git -C "$ROOT" rev-parse HEAD)"
ci() { RELEASE_CI_WORK="$(mktemp -d -p "$WORK")" "$CI" "$@"; }
notes() { "$GITEA" notes "$1"; }
serves() { python3 -c 'import json, sys; print(json.load(open(sys.argv[1]))["version"])' "$SERVER/$1/manifest.json"; }
packages() { (cd "$SERVER/packages" && sha256sum -- *.deb); }
# A release as "Release 1 - build" leaves it: the package and its
# SHA256SUMS, in a Gitea pre-release.
make_release() {
local version="$1" dir="$WORK/built-$1" name="ekklesia-classroom_$1_amd64.deb"
mkdir -p "$dir"
fake_deb "$version" "$dir/$name"
(cd "$dir" && sha256sum "$name" > SHA256SUMS)
printf 'notes\n' > "$dir/notes.md"
"$GITEA" create "v$version" "$COMMIT" "Ekklesia Classroom $version" "$dir/notes.md" --prerelease >/dev/null
"$GITEA" upload "v$version" "$dir/$name" "$dir/SHA256SUMS" >/dev/null
sha "$dir/$name"
}
# A gate report as tools/release-gate.sh writes one.
report() {
local out="$1" version="$2" hash="$3" canary="$4" stable="$5" mode="${6:-release}"
local name="ekklesia-classroom_${version}_amd64.deb"
{
echo "release gate for $name"
echo "sha256 $hash"
echo "mode $mode"
echo "started 2026-09-29T00:00:00Z"
echo "PASS fresh install of $version (a new machine, on stable) (5 min)"
echo "PASS upgrade $canary -> $version on a canary room (canary serves $canary) (5 min)"
echo "PASS upgrade $stable -> $version on a stable room (stable serves $stable) (5 min)"
echo "finished 2026-09-29T00:20:00Z: $name passed the release gate"
} > "$out"
}
H6="$(make_release 0.2.6.6)"
PKG6="$WORK/built-0.2.6.6/ekklesia-classroom_0.2.6.6_amd64.deb"
mkdir -p "$WORK/reports"
report "$WORK/reports/gate-report-preview-good.txt" 0.2.6.6 "$H6" 0.2.6.5 0.2.6.4
report "$WORK/reports/gate-report-preview-other.txt" 0.2.6.6 "$(printf 'f%.0s' {1..64})" 0.2.6.5 0.2.6.4
report "$WORK/reports/gate-report-preview-stale.txt" 0.2.6.6 "$H6" 0.2.6.3 0.2.6.4
report "$WORK/reports/gate-report-preview-ci.txt" 0.2.6.6 "$H6" 0.2.6.5 0.2.6.4 ci
head -n -1 "$WORK/reports/gate-report-preview-good.txt" > "$WORK/reports/gate-report-preview-unfinished.txt"
"$GITEA" upload v0.2.6.6 "$WORK"/reports/*.txt >/dev/null
before="$(packages)"
echo "1. a gate report is only relied on when it is about these bytes, now"
GATE="$ROOT/tools/release-gate.sh"
check "a passing release report for this file, from what the channels serve, is current" \
"$GATE" --still-current "$WORK/reports/gate-report-preview-good.txt" "$PKG6"
check "one about other bytes is not" \
refuses_saying "it is about other bytes" "$GATE" --still-current "$WORK/reports/gate-report-preview-other.txt" "$PKG6"
check "nor one made before canary moved" \
refuses_saying "canary serves 0.2.6.5 now" "$GATE" --still-current "$WORK/reports/gate-report-preview-stale.txt" "$PKG6"
check "nor a CI gate's report" \
refuses_saying "not a release gate's report" "$GATE" --still-current "$WORK/reports/gate-report-preview-ci.txt" "$PKG6"
check "nor one that never finished" \
refuses_saying "does not end in a pass" "$GATE" --still-current "$WORK/reports/gate-report-preview-unfinished.txt" "$PKG6"
echo "2. Preview: the release's file, to canary, on a current report"
check "a checksum that is not the release's is refused" \
refuses_saying "is not sha256 eeee" ci preview 0.2.6.6 "$(printf 'e%.0s' {1..64})" gate-report-preview-good.txt
check "a stale report is refused" \
refuses_saying "canary serves 0.2.6.5 now" ci preview 0.2.6.6 "$H6" gate-report-preview-stale.txt
check "a report for other bytes is refused" \
refuses_saying "it is about other bytes" ci preview 0.2.6.6 "$H6" gate-report-preview-other.txt
check "and after all of that, canary is where it was and no package was added" \
bash -c "[[ \"\$(python3 -c 'import json; print(json.load(open(\"$SERVER/canary/manifest.json\"))[\"version\"])')\" == 0.2.6.5 ]] \
&& [[ \"\$(cd '$SERVER/packages' && sha256sum -- *.deb)\" == \"$before\" ]]"
check "the release's file is published to canary" \
ci preview 0.2.6.6 "$H6" gate-report-preview-good.txt
check "canary serves it, and latest.deb is that file" \
bash -c "[[ \$(python3 -c 'import json; m=json.load(open(\"$SERVER/canary/manifest.json\")); print(m[\"version\"], m[\"sha256\"])') == '0.2.6.6 $H6' ]] \
&& cmp -s '$SERVER/canary/latest.deb' '$PKG6'"
check "exactly one package was added, and no other changed" \
bash -c "diff <(printf '%s\n' \"$before\") <(cd '$SERVER/packages' && sha256sum -- *.deb) | grep -c '^[<>]' | grep -qx 1 \
&& ! diff <(printf '%s\n' \"$before\") <(cd '$SERVER/packages' && sha256sum -- *.deb) | grep -q '^<'"
check "stable was not touched" test "$(serves stable)" = 0.2.6.4
check "the Gitea release records it, with the sha256" \
bash -c "'$GITEA' notes v0.2.6.6 | grep -qF 'published to Preview (canary), sha256 \`$H6\`'"
check "publishing it again is refused" \
refuses_saying "0.2.6.6 has already been published" ci preview 0.2.6.6 "$H6" gate-report-preview-good.txt
echo "3. Stable: only the file Preview ran, only with an approval"
H7="$(make_release 0.2.6.7)"
check "a release that never went to Preview is refused" \
refuses_saying "has no record of $H7 going to Preview" ci stable 0.2.6.7 "$H7" "approved"
check "a checksum other than the one Preview ran is refused" \
refuses_saying "has no record of $H7 going to Preview" ci stable 0.2.6.6 "$H7" "approved"
check "a promotion with no approval is refused" \
refuses_saying "names who approved it" ci stable 0.2.6.6 "$H6" ""
check "stable is still 0.2.6.4 after all of that" test "$(serves stable)" = 0.2.6.4
before="$(packages)"
check "the approved file is promoted" \
ci stable 0.2.6.6 "$H6" "physical checks passed on the laptop"
check "stable serves exactly the file canary does" \
bash -c "[[ \$(python3 -c 'import json; m=json.load(open(\"$SERVER/stable/manifest.json\")); print(m[\"version\"], m[\"sha256\"])') == '0.2.6.6 $H6' ]] \
&& cmp -s '$SERVER/stable/latest.deb' '$SERVER/canary/latest.deb'"
check "no package was added or changed by promoting" test "$(packages)" = "$before"
check "the record has the promotion and its approval" \
bash -c "'$GITEA' notes v0.2.6.6 | grep -qF 'Approval: physical checks passed on the laptop'"
check "and the release is no longer a pre-release" \
bash -c "curl -fsS '$GITEA_URL/api/v1/repos/church/classroom/releases/tags/v0.2.6.6' | grep -q '\"prerelease\": false'"
check "the record reads in order: created, Preview, Stable" \
bash -c "'$GITEA' notes v0.2.6.6 | python3 -c 'import sys
t = sys.stdin.read()
order = [t.index(s) for s in (\"release created at\", \"published to Preview\", \"promoted to Stable\")]
assert order == sorted(order), order'"
echo "4. import: a release from before the record, as the server has it"
H5="$(sha "$SERVER/packages/ekklesia-classroom_0.2.6.5_amd64.deb")"
check "a checksum the server's file does not have is refused" \
refuses_saying "is not sha256 $H7" ci import 0.2.6.5 "$H7" "$COMMIT"
check "a commit that does not exist is refused" \
refuses_saying "is not a commit here" ci import 0.2.6.5 "$H5" "$(printf '0%.0s' {1..40})"
check "0.2.6.5 is recorded, with the server's own bytes" \
bash -c "RELEASE_CI_WORK=\$(mktemp -d -p '$WORK') '$CI' import 0.2.6.5 '$H5' '$COMMIT' \
&& '$GITEA' download v0.2.6.5 ekklesia-classroom_0.2.6.5_amd64.deb '$WORK/imported.deb' \
&& cmp -s '$WORK/imported.deb' '$SERVER/packages/ekklesia-classroom_0.2.6.5_amd64.deb'"
check "no channel serves it now, so the record claims neither" \
bash -c "! '$GITEA' notes v0.2.6.5 | grep -qE 'published to Preview|promoted to Stable'"
# A room of another era: stable serving 0.2.6.4, as it did before 0.2.6.6.
point stable 0.2.6.4
H4="$(sha "$SERVER/packages/ekklesia-classroom_0.2.6.4_amd64.deb")"
check "a release a channel serves is recorded as on it, read off the server" \
bash -c "RELEASE_CI_WORK=\$(mktemp -d -p '$WORK') '$CI' import 0.2.6.4 '$H4' '$COMMIT' \
&& '$GITEA' notes v0.2.6.4 | grep -qF 'promoted to Stable, sha256 \`$H4\`' \
&& ! '$GITEA' notes v0.2.6.4 | grep -q 'published to Preview' \
&& curl -fsS '$GITEA_URL/api/v1/repos/church/classroom/releases/tags/v0.2.6.4' | grep -q '\"prerelease\": false'"
check "a release that already has a record is not imported over it" \
refuses_saying "its record is kept as it is" ci import 0.2.6.6 "$H6" "$COMMIT"
check "a Gitea that cannot be asked is never taken as having no release" \
refuses_saying "could not ask Gitea whether v0.2.6.9 exists" \
env GITEA_URL=http://127.0.0.1:9 bash -c "RELEASE_CI_WORK=\$(mktemp -d -p '$WORK') '$CI' import 0.2.6.9 '$H5' '$COMMIT'"
echo "5. the release runner uses the key it is given, and only that"
H8="$(make_release 0.2.6.8)"
report "$WORK/reports/gate-report-preview-0.2.6.8.txt" 0.2.6.8 "$H8" "$(serves canary)" "$(serves stable)"
"$GITEA" upload v0.2.6.8 "$WORK/reports/gate-report-preview-0.2.6.8.txt" >/dev/null
before="$(packages)"
check "under Actions, a job with no key stops before anything is published" \
refuses_saying "A job never uses the runner's own ssh keys" \
env GITHUB_ACTIONS=true bash -c "RELEASE_CI_WORK=\$(mktemp -d -p '$WORK') '$CI' preview 0.2.6.8 '$H8' gate-report-preview-0.2.6.8.txt"
check "and nothing was" test "$(packages)" = "$before"
job="$(mktemp -d -p "$WORK")"
export SSH_LOG="$WORK/ssh.log"
: > "$SSH_LOG"
check "with a key, it publishes" \
env GITHUB_ACTIONS=true RELEASE_CI_WORK="$job" RELEASE_SSH_KEY="not a real key" \
RELEASE_SSH_KNOWN_HOSTS="10.0.0.1 ssh-ed25519 AAAA" RELEASE_SSH_ADDRESS=10.0.0.1 \
RELEASE_SSH_USER=classroom-publish "$CI" preview 0.2.6.8 "$H8" gate-report-preview-0.2.6.8.txt
check "every ssh and rsync it made used the job's own configuration" \
bash -c "[[ -s '$SSH_LOG' ]] && ! grep -v -e 'ssh $job/ssh/config' -e 'rsync ssh -F $job/ssh/config' '$SSH_LOG'"
check "which names the key, the host key and the account it was given" \
bash -c "grep -qx ' IdentityFile $job/ssh/key' '$job/ssh/config' \
&& grep -qx ' UserKnownHostsFile $job/ssh/known_hosts' '$job/ssh/config' \
&& grep -qx ' User classroom-publish' '$job/ssh/config' \
&& grep -qx ' StrictHostKeyChecking yes' '$job/ssh/config'"
check "and the key is gone when the job ends" test ! -e "$job/ssh/key"
unset SSH_LOG
echo "6. build: only from master or a maintenance branch"
check "a feature branch is refused before anything is built" \
bash -c "GITHUB_REF=refs/heads/feat/something RELEASE_CI_WORK=\$(mktemp -d -p '$WORK') '$CI' build 0.2.6.8 2>&1 \
| grep -q 'releases are built from master or release/\\*, not refs/heads/feat/something'"
echo
echo "$passed passed, $failed failed"
[[ $failed -eq 0 ]]