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:
co-authored by
Claude Opus 5.5
parent
4413114d2a
commit
5416112db5
+9
-2
@@ -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"):
|
||||||
|
|||||||
+4
-49
@@ -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
|
|
||||||
[[ -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
|
exit 1
|
||||||
}
|
}
|
||||||
done
|
|
||||||
|
|
||||||
echo "Building Ekklesia Classroom ${VERSION}"
|
echo "Building Ekklesia Classroom ${VERSION}"
|
||||||
|
|
||||||
|
|||||||
Executable
+141
@@ -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"
|
||||||
Reference in New Issue
Block a user