WUT_Computer_Science/run.sh
Krzysztof kuhy Rudnicki 7515bd0dc2 Add CI, install.sh and the run.sh harness
Two workflows, one per gate, each checking out kuhyx/utils into .utils and
calling the shared script -- the pattern every other repo here uses.
pre-commit.yml sets UTILS_ROOT to that checkout, without which the shims fail
with "shared gate not found", and both use fetch-depth 1 because the history
is ~494 MB and no gate reads it.

install.sh ends by verifying and exits non-zero if the gate cannot run; a
warning there would just recreate the bug it exists to prevent, since this
repo had no hooks at all until now. Confirmed both ways: --check exited 1
while the hooks were missing, then 0 once installed.

run.sh separates `status` (probe, seconds, safe in CI) from `run <dir>`
(actually executes one project). Conflating them is not hypothetical -- the
first version of the probe did exactly that and it cost 120 seconds and a
scare. Some course dirs already ship a run.sh from years ago, and
Programming/PSD/run.sh installs packages with sudo and starts Kafka; calling
it with an unknown --probe flag does not probe anything, it runs the project.
It blocked on a sudo prompt rather than installing (verified against
/var/log/pacman.log and the absence of a yay clone), but only by luck.

So probe_one now invokes a runner ONLY if it declares --probe, wraps it in
`timeout 20s` as defence in depth, and reports anything else as unprobed
without touching it. All 44 directories now probe instantly.

DOCS-runnability.md is generated with a per-environment column. Recording one
global status would make the CI check red by construction, since `runnable`
locally and `blocked` on ubuntu-latest is the normal case for the MATLAB,
Unity and .NET directories rather than a drift signal.

Per-directory run.sh files are the next step; all 44 currently read unprobed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172dj34FDpao6wsix8ZuCzE
2026-08-28 18:26:10 +02:00

147 lines
5.0 KiB
Bash
Executable File

#!/bin/bash
# ============================================================================
# run.sh -- drive this coursework archive.
#
# ./run.sh check run the full local gate (pre-commit, all files)
# ./run.sh install alias for ./install.sh
# ./run.sh status probe every course dir, rewrite DOCS-runnability.md
# ./run.sh run <dir> actually execute one course project
# ./run.sh sync-vendored show the mirrors that gates/vendored.txt feeds
#
# `status` PROBES: it asks each directory whether its toolchain is present and
# never executes the project, so it takes seconds and is safe in CI. Use
# `run <dir>` to actually run something. Those are very different commands and
# conflating them is how a "status check" turns into 44 builds.
# ============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
readonly RUNNABILITY_DOC="${SCRIPT_DIR}/DOCS-runnability.md"
# Exit codes a per-directory run.sh uses to report itself.
readonly EXIT_BLOCKED=78
readonly EXIT_NO_CODE=79
log() { printf '==> %s\n' "$*"; }
err() { printf 'error: %s\n' "$*" >&2; }
usage() {
grep -E '^# \./run\.sh' "${BASH_SOURCE[0]}" | sed 's/^# //'
exit "${1:-0}"
}
course_dirs() {
find "${SCRIPT_DIR}/Programming" "${SCRIPT_DIR}/NotProgramming" \
-mindepth 1 -maxdepth 1 -type d | sort
}
cmd_check() {
log "running the full gate"
( cd "$SCRIPT_DIR" && pre-commit run --all-files )
}
cmd_install() { exec "${SCRIPT_DIR}/install.sh" "$@"; }
# Probe one directory. Prints "<status>\t<detail>"; never executes a project.
#
# The guard below is load-bearing. Some course dirs already ship their own
# run.sh from years ago -- Programming/PSD/run.sh installs packages with sudo
# and starts Kafka. Calling one of those with an unknown --probe flag does not
# probe anything, it RUNS the project. So a runner is only invoked if it
# actually declares --probe; anything else is reported unprobed and left alone.
# `timeout` is defence in depth for a script that declares the flag and then
# blocks on input anyway.
probe_one() {
local dir="$1" runner="$1/run.sh"
if [[ -x "$runner" ]] && grep -qF -- '--probe' "$runner"; then
local out status
out="$(timeout 20s "$runner" --probe 2> /dev/null)" && status=0 || status=$?
case "$status" in
0) printf 'runnable\t%s\n' "${out:-ok}" ;;
"$EXIT_BLOCKED") printf 'blocked\t%s\n' "${out:-unknown}" ;;
"$EXIT_NO_CODE") printf 'no-code\t%s\n' "${out:-unknown}" ;;
*) printf 'error\t%s\n' "probe exited $status" ;;
esac
return 0
fi
if [[ -x "$runner" ]]; then
printf 'unprobed\tpre-existing run.sh, no --probe support\n'
else
printf 'unprobed\tno run.sh yet\n'
fi
}
cmd_status() {
local env_label="${CI:+ci}"
env_label="${env_label:-local}"
log "probing course directories (${env_label})"
local tmp
tmp="$(mktemp)"
trap 'rm -f "${tmp:-}"' RETURN
local dir rel line
while IFS= read -r dir; do
rel="${dir#"${SCRIPT_DIR}"/}"
line="$(probe_one "$dir")"
printf '%s\t%s\n' "$rel" "$line" >> "$tmp"
done < <(course_dirs)
write_runnability_doc "$tmp" "$env_label"
log "wrote ${RUNNABILITY_DOC##*/}"
awk -F'\t' '{ n[$2]++ } END { for (k in n) printf " %-10s %d\n", k, n[k] }' "$tmp"
}
write_runnability_doc() {
local data="$1" env_label="$2"
{
cat <<'MD'
# Runnability
Generated by `./run.sh status`. Do not edit by hand.
Status is recorded PER ENVIRONMENT. A directory that is `runnable` locally and
`blocked` in CI is the normal case, not drift: the runner has no MATLAB, no
Unity and no .NET SDK. CI compares only its own column.
MD
printf '| Course | %s | Detail |\n|---|---|---|\n' "$env_label"
awk -F'\t' '{ printf "| `%s` | %s | %s |\n", $1, $2, $3 }' "$data"
} > "$RUNNABILITY_DOC"
}
cmd_run() {
local target="${1:-}"
[[ -n "$target" ]] || { err "run needs a directory"; usage 1; }
local runner="${SCRIPT_DIR}/${target}/run.sh"
[[ -x "$runner" ]] || { err "no executable run.sh in $target"; return 1; }
log "running $target"
"$runner"
}
cmd_sync_vendored() {
log "gates/vendored.txt declares:"
grep -vE '^\s*#|^\s*$' "${SCRIPT_DIR}/gates/vendored.txt" | sed 's/^/ /'
printf '\nMirrored into:\n'
printf ' ~/utils/file_length/_tables.py VENDORED_SUBPATHS["WUT_Computer_Science"]\n'
printf ' pyproject.toml [tool.ruff] exclude (Milestone 4)\n'
printf ' pyproject.toml [tool.coverage.run] omit (Milestone 4)\n'
}
main() {
case "${1:-}" in
check) shift; cmd_check "$@" ;;
install) shift; cmd_install "$@" ;;
status) shift; cmd_status "$@" ;;
run) shift; cmd_run "$@" ;;
sync-vendored) shift; cmd_sync_vendored "$@" ;;
-h | --help | "") usage ;;
*) err "unknown command: $1"; usage 1 ;;
esac
}
main "$@"