build: one list of checks, run by the release build and by CI alike

tools/check.sh is what build-release.sh ran before building — every
script's syntax, the manual, every tests/test-*.sh — moved, not copied,
so a new test is run by both with nothing else to edit. It now runs every
test even after one fails, keeps each one's output and a summary table
for a CI run, and with --base checks the change's hygiene too.

The manual's revision date honours SOURCE_DATE_EPOCH, so a release built
from a commit a day later ships the manual that commit holds.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-09-29 18:47:42 -05:00
co-authored by Claude Opus 5.5
parent 4413114d2a
commit 5416112db5
3 changed files with 156 additions and 53 deletions
+9 -2
View File
@@ -37,9 +37,10 @@ import argparse
import base64 import base64
import html import html
import mimetypes import mimetypes
import os
import re import re
import sys import sys
from datetime import date from datetime import date, datetime
from pathlib import Path from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
@@ -704,7 +705,13 @@ def main():
if problems: if problems:
return 1 return 1
revision = f"Revision {date.today():%d %B %Y}" # The day the source was committed, when the build says so, rather than
# the day it was built: a release built from a commit a day later must
# still ship the manual that commit holds, or the build changes the
# tree it was built from. SOURCE_DATE_EPOCH is the usual name for it.
epoch = os.environ.get("SOURCE_DATE_EPOCH")
day = datetime.fromtimestamp(int(epoch)).date() if epoch else date.today()
revision = f"Revision {day:%d %B %Y}"
if arguments.check: if arguments.check:
for name in ("classroom-manual.html", "classroom-manual.pdf"): for name in ("classroom-manual.html", "classroom-manual.pdf"):
+6 -51
View File
@@ -19,57 +19,12 @@ release_require_version "$VERSION" || exit 1
STAGE="$ROOT/packaging/root" STAGE="$ROOT/packaging/root"
OUTPUT="$ROOT/dist/ekklesia-classroom_${VERSION}_amd64.deb" OUTPUT="$ROOT/dist/ekklesia-classroom_${VERSION}_amd64.deb"
# Syntax check everything the package ships, by shebang, so this cannot # Sources, the teacher's manual and every test, before anything is built.
# drift out of date as classroom commands are added. # The same list CI runs on every change; see tools/check.sh.
echo "Validating sources" "$ROOT/tools/check.sh" || {
echo "Checks failed; nothing was built."
for script in "$ROOT/packaging/debian/postinst" "$ROOT"/scripts/* "$ROOT"/tests/*; do exit 1
[[ -f "$script" ]] || continue }
case "$(head -n 1 "$script")" in
*python3*)
python3 -m py_compile "$script"
;;
*bash*|*bin/sh*)
bash -n "$script"
;;
esac
done
for module in "$ROOT"/lib/*.py; do
[[ -f "$module" ]] || continue
python3 -m py_compile "$module"
done
for policy in "$ROOT"/config/chrome/policies/*/*.json; do
[[ -f "$policy" ]] || continue
python3 -c 'import json, sys; json.load(open(sys.argv[1]))' "$policy"
done
find "$ROOT/scripts" "$ROOT/lib" "$ROOT/tests" \
-name '__pycache__' -type d -prune -exec rm -rf {} + 2>/dev/null || true
# THE MANUAL IS PART OF THE RELEASE, NOT A JOB FOR AFTERWARDS.
#
# Built from docs/manual before the tests run, so that the test which
# checks the digital and printed versions against each other is checking
# what this package is about to ship rather than whatever was built last.
# A page that would make the printed manual wrong fails the build here.
echo "Building the teacher's manual"
"$ROOT/tools/build-manual"
for test in "$ROOT"/tests/test-*.sh; do
[[ -x "$test" ]] || continue
echo "Running $(basename "$test")"
"$test" > /dev/null || {
echo "Tests failed. Run it directly to see why:"
echo " $test"
exit 1
}
done
echo "Building Ekklesia Classroom ${VERSION}" echo "Building Ekklesia Classroom ${VERSION}"
Executable
+141
View File
@@ -0,0 +1,141 @@
#!/bin/bash
# EVERY CHECK THE REPOSITORY HAS THAT NEEDS NO CONTAINER AND NO SCREEN.
#
# tools/check.sh sources, the manual, every test
# tools/check.sh --base REF and the change since REF is clean
# CHECK_LOG_DIR=DIR tools/check.sh each test's output in DIR, and a
# summary.md for a CI run to show
#
# The one list. tools/build-release.sh runs it before it builds anything,
# and CI runs it on every push and pull request, so a test added to tests/
# is a test both of them run, with nothing else to edit.
#
# Every test runs even after one has failed, so a report names all of
# them; the exit status is 1 if any did.
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
base=""
while [[ $# -gt 0 ]]; do
case "$1" in
--base) base="$2"; shift 2 ;;
*) echo "Usage: $0 [--base REF]" >&2; exit 2 ;;
esac
done
logs="${CHECK_LOG_DIR:-$(mktemp -d)}"
mkdir -p "$logs"
summary="$logs/summary.md"
failed=0
{
echo "| Check | Result | Seconds |"
echo "| --- | --- | --- |"
} > "$summary"
# check NAME COMMAND...: run it, keep its output, say how it went.
check() {
local name="$1" started status log
shift
started=$SECONDS
log="$logs/$name.log"
"$@" > "$log" 2>&1
status=$?
if [[ $status -eq 0 ]]; then
printf ' ok %s\n' "$name"
echo "| $name | pass | $(( SECONDS - started )) |" >> "$summary"
else
printf ' FAIL %s\n' "$name"
tail -n 25 "$log" | sed 's/^/ /'
echo "| $name | **FAIL** | $(( SECONDS - started )) |" >> "$summary"
failed=1
fi
}
# Syntax check everything the package ships, by shebang, so this cannot
# drift out of date as classroom commands are added.
validate_sources() {
local script module policy
for script in "$ROOT/packaging/debian/postinst" "$ROOT"/scripts/* "$ROOT"/tests/* \
"$ROOT"/tools/* "$ROOT"/tools/lab/* "$ROOT"/update_*.sh; do
[[ -f "$script" ]] || continue
case "$(head -n 1 "$script")" in
*python3*)
python3 -m py_compile "$script" || return 1
;;
*bash*|*bin/sh*)
bash -n "$script" || return 1
;;
*)
# Sourced, so no #! line: tools/release-lib.sh.
if [[ "$script" == *.sh ]]; then
bash -n "$script" || return 1
fi
;;
esac
done
for module in "$ROOT"/lib/*.py; do
[[ -f "$module" ]] || continue
python3 -m py_compile "$module" || return 1
done
for policy in "$ROOT"/config/chrome/policies/*/*.json "$ROOT"/config/wallpapers/*/metadata.json; do
[[ -f "$policy" ]] || continue
python3 -c 'import json, sys; json.load(open(sys.argv[1]))' "$policy" || return 1
done
find "$ROOT/scripts" "$ROOT/lib" "$ROOT/tests" "$ROOT/tools" \
-name '__pycache__' -type d -prune -exec rm -rf {} + 2>/dev/null || true
}
# The change itself: no whitespace damage, and nothing built or published
# committed by mistake. Only with --base, because it is a question about a
# change, not about a tree.
hygiene() {
git diff --check "$base" HEAD || return 1
if git ls-files | grep -E '\.deb$|^(dist|repo|packaging/root)/'; then
echo "built or published files are committed (above)" >&2
return 1
fi
# A committed release version is one a release could be built from.
source "$ROOT/tools/release-lib.sh"
release_require_version "$(cat "$ROOT/VERSION")"
}
echo "Checking $(git rev-parse --short HEAD 2>/dev/null || echo 'the tree')"
check sources validate_sources
[[ -z "$base" ]] || check hygiene hygiene
# THE MANUAL IS PART OF THE RELEASE, NOT A JOB FOR AFTERWARDS.
#
# Built from docs/manual before the tests run, so that the test which
# checks the digital and printed versions against each other is checking
# what this package is about to ship rather than whatever was built last.
# A page that would make the printed manual wrong fails here.
check manual "$ROOT/tools/build-manual"
for test in "$ROOT"/tests/test-*.sh; do
[[ -x "$test" ]] || continue
check "$(basename "$test" .sh)" "$test"
done
echo
if [[ $failed -eq 0 ]]; then
echo "Every check passed. Logs: $logs"
else
echo "Checks FAILED. Each one's full output is in $logs"
fi
exit "$failed"