feat: Gitea CI, and releases built once, previewed and promoted as one file
ci.yml runs tools/check.sh and the container gate on every push and pull request, with no secrets. Release 1 builds a version from its release commit on master or release/*, gates it, and keeps it in a Gitea pre-release. Release 2 gates that file again on the CI runner and publishes it to canary (Preview) from the release runner. Release 3 promotes the file Preview ran, on a person's approval. Import records a release from before Gitea kept the record. All of it is tools/release-ci.sh calling the existing scripts. docs/releasing.md: tracks, branches, each step, immutability, rollback, secrets, runners, Nightly and the hardware lab as designs, the Gitea administration still to do, and what was measured to make it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
583f9b1809
commit
22f25854aa
Executable
+276
@@ -0,0 +1,276 @@
|
||||
#!/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
|
||||
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) 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_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. 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 ]]
|
||||
Executable
+145
@@ -0,0 +1,145 @@
|
||||
#!/bin/bash
|
||||
# The Gitea workflows keep the rules docs/releasing.md makes of them.
|
||||
#
|
||||
# Read as text, job by job, because the runner that runs this has no YAML
|
||||
# library and must not need one. The rules are the ones that matter if a
|
||||
# workflow is edited carelessly later: what a pull request can reach,
|
||||
# what publishes and when, and that the work stays in the scripts.
|
||||
# Whether Gitea itself parses and plans them was measured with act_runner
|
||||
# (docs/releasing.md, "What was measured").
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
python3 - "$ROOT" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
flows = root / ".gitea/workflows"
|
||||
passed = failed = 0
|
||||
|
||||
|
||||
def check(label, condition, detail=""):
|
||||
global passed, failed
|
||||
if condition:
|
||||
print(f" ok {label}")
|
||||
passed += 1
|
||||
else:
|
||||
print(f" FAIL {label}")
|
||||
if detail:
|
||||
print(f" {detail}")
|
||||
failed += 1
|
||||
|
||||
|
||||
def read(name):
|
||||
return (flows / name).read_text()
|
||||
|
||||
|
||||
def triggers(text):
|
||||
block = re.search(r"^on:\n((?: .*\n|\n)+)", text, re.M)
|
||||
return set(re.findall(r"^ ([a-z_]+):", block.group(1), re.M)) if block else set()
|
||||
|
||||
|
||||
def jobs(text):
|
||||
"""{job id: its text}, from the jobs: block."""
|
||||
body = text[text.index("\njobs:\n") + 7:]
|
||||
found = {}
|
||||
for match in re.finditer(r"^ ([a-z_]+):\n((?: .*\n|\n)*)", body, re.M):
|
||||
found[match.group(1)] = match.group(2)
|
||||
return found
|
||||
|
||||
|
||||
def runs_on(job):
|
||||
match = re.search(r"^ runs-on: (\S+)$", job, re.M)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
names = sorted(p.name for p in flows.glob("*.yml"))
|
||||
check("the workflows are the ones documented", names == [
|
||||
"ci.yml", "release-build.yml", "release-import.yml",
|
||||
"release-preview.yml", "release-stable.yml"], str(names))
|
||||
|
||||
releasing = (root / "docs/releasing.md").read_text()
|
||||
|
||||
print("1. CI: every change, no secrets, nothing kept that could be installed")
|
||||
ci = read("ci.yml")
|
||||
check("it runs on push and on pull requests", triggers(ci) == {"push", "pull_request"}, str(triggers(ci)))
|
||||
check("it reads no secret and no token", "secrets." not in ci and "github.token" not in ci)
|
||||
check("every job runs on the CI runner",
|
||||
{runs_on(job) for job in jobs(ci).values()} == {"ekklesia-ci"})
|
||||
check("the checks are tools/check.sh, the same list a release build runs",
|
||||
"tools/check.sh --base" in ci
|
||||
and '"$ROOT/tools/check.sh"' in (root / "tools/build-release.sh").read_text())
|
||||
check("the container gate is the release gate, in its CI mode", "tools/release-gate.sh --ci" in ci)
|
||||
check("the package is inspected, not kept",
|
||||
"dist/" not in re.sub(r"(?s)^.*?Keep the package's contents", "", ci)
|
||||
.split("path:", 1)[-1])
|
||||
check("building proves the version and changes nothing else in the tree",
|
||||
'dpkg-deb --field "$deb" Version)" = "$version"' in ci
|
||||
and "git status --porcelain --untracked-files=no" in ci)
|
||||
|
||||
print("2. releases: only by hand, and only through the scripts")
|
||||
release_flows = [n for n in names if n.startswith("release-")]
|
||||
for name in release_flows:
|
||||
text = read(name)
|
||||
check(f"{name} only runs when a person runs it", triggers(text) == {"workflow_dispatch"},
|
||||
str(triggers(text)))
|
||||
# An input pasted into a script is a script anyone who can run the
|
||||
# workflow can write. Inputs reach the shell as environment only.
|
||||
runs = "\n".join(re.findall(r"run: \|?\n?((?: .*\n)+|.*\n)", text))
|
||||
check(f"{name} never pastes an input into a command",
|
||||
"${{" not in runs, re.findall(r"\$\{\{[^}]*\}\}", runs)[:3])
|
||||
check(f"{name}'s work is tools/release-ci.sh", "tools/release-ci.sh " in text)
|
||||
|
||||
print("3. who holds what")
|
||||
where = {}
|
||||
for name in names:
|
||||
for job_id, job in jobs(read(name)).items():
|
||||
where[f"{name}:{job_id}"] = (runs_on(job), job)
|
||||
|
||||
key_jobs = {k for k, (_, job) in where.items() if "CLASSROOM_UPDATES_SSH_KEY" in job}
|
||||
check("the update server's key is given to exactly Preview's publish and Stable's promote",
|
||||
key_jobs == {"release-preview.yml:publish", "release-stable.yml:promote"}, str(key_jobs))
|
||||
check("and those run only on the release runner",
|
||||
all(where[k][0] == "ekklesia-release" for k in key_jobs))
|
||||
check("nothing on the release runner builds or gates (it has no Docker)",
|
||||
all("release-ci.sh build" not in job and "release-ci.sh gate" not in job
|
||||
and "release-gate.sh" not in job
|
||||
for runner, job in where.values() if runner == "ekklesia-release"))
|
||||
check("nothing on the CI runner is given the key",
|
||||
all("CLASSROOM_UPDATES_SSH_KEY" not in job
|
||||
for runner, job in where.values() if runner == "ekklesia-ci"))
|
||||
check("the key is a secret; where it goes is a variable",
|
||||
all("secrets.CLASSROOM_UPDATES_SSH_KEY" in where[k][1]
|
||||
and "vars.CLASSROOM_UPDATES_KNOWN_HOSTS" in where[k][1] for k in key_jobs))
|
||||
|
||||
print("4. Preview and Stable")
|
||||
preview = jobs(read("release-preview.yml"))
|
||||
check("Preview gates on the CI runner, then publishes on the release runner",
|
||||
runs_on(preview["gate"]) == "ekklesia-ci" and runs_on(preview["publish"]) == "ekklesia-release"
|
||||
and "needs: gate" in preview["publish"])
|
||||
check("the publish job relies on that gate's report, by name",
|
||||
"needs.gate.outputs.report" in preview["publish"])
|
||||
check("Preview asks for the sha256 of the file", "sha256:" in read("release-preview.yml"))
|
||||
stable = read("release-stable.yml")
|
||||
check("Stable asks for the sha256, an approval and a typed confirmation",
|
||||
all(f" {field}:" in stable for field in ("sha256", "approval", "confirm"))
|
||||
and 'PROMOTE $VERSION TO STABLE' in stable)
|
||||
check("Stable never builds or gates", "build" not in re.sub(r"#.*", "", stable).replace("rebuild", "")
|
||||
and "release-gate" not in stable)
|
||||
|
||||
print("5. what the measurements found")
|
||||
check("artifacts are uploaded with v3, which Gitea 1.27 takes (v4 is refused)",
|
||||
all("upload-artifact@v4" not in read(n) for n in names)
|
||||
and "upload-artifact@v3" in ci)
|
||||
check("the release token falls back to Gitea's own, which was measured to work",
|
||||
all("secrets.RELEASE_API_TOKEN || github.token" in read(n) for n in release_flows))
|
||||
check("the docs say labels in a runner's config file win over --labels",
|
||||
"OVERRIDE --labels" in releasing)
|
||||
|
||||
print()
|
||||
print(f"{passed} passed, {failed} failed")
|
||||
sys.exit(1 if failed else 0)
|
||||
PY
|
||||
Reference in New Issue
Block a user