mirror of
https://github.com/kuhyx/WUT_Computer_Science.git
synced 2026-11-10 19:07:11 +01:00
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
This commit is contained in:
parent
50263f075a
commit
7515bd0dc2
30
.github/workflows/md-naming.yml
vendored
Normal file
30
.github/workflows/md-naming.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
name: md-naming
|
||||
|
||||
# Deliberately a SEPARATE workflow from pre-commit.yml: one workflow per gate,
|
||||
# so a naming failure is legible on its own rather than buried in a suite.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
md-naming:
|
||||
name: markdown naming convention
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
# The gate and its exemption list live in the shared utils repo so every
|
||||
# repo enforces the same rule. Checking it out beats vendoring a copy,
|
||||
# which is how a gate and a survey start disagreeing.
|
||||
- name: Check out the shared gate
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: kuhyx/utils
|
||||
path: .utils
|
||||
|
||||
- name: Enforce the naming convention
|
||||
run: bash .utils/scripts/check_md_naming.sh --all
|
||||
48
.github/workflows/pre-commit.yml
vendored
Normal file
48
.github/workflows/pre-commit.yml
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
name: pre-commit
|
||||
|
||||
# The local hooks can be skipped -- a fresh clone that never ran ./install.sh
|
||||
# has no hooks at all -- so the gate needs a second home that nobody can
|
||||
# forget to install.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# scripts/check_md_naming.sh is a thin shim that execs the shared gate
|
||||
# out of the utils repo. A runner has no ~/utils, so without this the
|
||||
# hook fails with "shared gate not found".
|
||||
UTILS_ROOT: ${{ github.workspace }}/.utils
|
||||
steps:
|
||||
# fetch-depth 1: the history here is ~494 MB and no gate reads it.
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Check out the shared gates
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: kuhyx/utils
|
||||
path: .utils
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
- name: Install shellcheck
|
||||
run: sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pre-commit
|
||||
key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
|
||||
- name: Install pre-commit
|
||||
run: python -m pip install --quiet pre-commit
|
||||
|
||||
- name: Run the hooks
|
||||
run: pre-commit run --all-files --show-diff-on-failure
|
||||
54
DOCS-runnability.md
Normal file
54
DOCS-runnability.md
Normal file
@ -0,0 +1,54 @@
|
||||
# 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.
|
||||
|
||||
| Course | local | Detail |
|
||||
|---|---|---|
|
||||
| `NotProgramming/DPZ` | unprobed | no run.sh yet |
|
||||
| `NotProgramming/ECOGR` | unprobed | no run.sh yet |
|
||||
| `NotProgramming/EDABA` | unprobed | no run.sh yet |
|
||||
| `NotProgramming/EDABA-LAB` | unprobed | no run.sh yet |
|
||||
| `NotProgramming/EDISE` | unprobed | no run.sh yet |
|
||||
| `NotProgramming/EDISP` | unprobed | no run.sh yet |
|
||||
| `NotProgramming/ELAC` | unprobed | no run.sh yet |
|
||||
| `NotProgramming/EMISY` | unprobed | no run.sh yet |
|
||||
| `NotProgramming/ENUME` | unprobed | no run.sh yet |
|
||||
| `NotProgramming/EPHY2` | unprobed | no run.sh yet |
|
||||
| `NotProgramming/EPOGAI` | unprobed | no run.sh yet |
|
||||
| `NotProgramming/ERPM` | unprobed | no run.sh yet |
|
||||
| `NotProgramming/ESDP1` | unprobed | no run.sh yet |
|
||||
| `NotProgramming/ESOEN` | unprobed | no run.sh yet |
|
||||
| `NotProgramming/MOM` | unprobed | no run.sh yet |
|
||||
| `NotProgramming/SDM` | unprobed | no run.sh yet |
|
||||
| `NotProgramming/SDM2` | unprobed | no run.sh yet |
|
||||
| `NotProgramming/SPD` | unprobed | no run.sh yet |
|
||||
| `NotProgramming/STUP` | unprobed | no run.sh yet |
|
||||
| `Programming/AIS` | unprobed | no run.sh yet |
|
||||
| `Programming/BD2` | unprobed | no run.sh yet |
|
||||
| `Programming/BSS` | unprobed | no run.sh yet |
|
||||
| `Programming/EADS` | unprobed | no run.sh yet |
|
||||
| `Programming/EARIN` | unprobed | no run.sh yet |
|
||||
| `Programming/ECOAR` | unprobed | no run.sh yet |
|
||||
| `Programming/ECOTE` | unprobed | no run.sh yet |
|
||||
| `Programming/ECRYPT` | unprobed | no run.sh yet |
|
||||
| `Programming/ECRYPT_PROJECT` | unprobed | no run.sh yet |
|
||||
| `Programming/EGUI` | unprobed | no run.sh yet |
|
||||
| `Programming/EOOP` | unprobed | no run.sh yet |
|
||||
| `Programming/EOPSY` | unprobed | no run.sh yet |
|
||||
| `Programming/EPFU` | unprobed | no run.sh yet |
|
||||
| `Programming/E-PSYOPS` | unprobed | no run.sh yet |
|
||||
| `Programming/ERSMS-project` | unprobed | no run.sh yet |
|
||||
| `Programming/NLP` | unprobed | no run.sh yet |
|
||||
| `Programming/PBAD` | unprobed | no run.sh yet |
|
||||
| `Programming/PORR` | unprobed | no run.sh yet |
|
||||
| `Programming/PSD` | unprobed | pre-existing run.sh, no --probe support |
|
||||
| `Programming/psd_project` | unprobed | no run.sh yet |
|
||||
| `Programming/TRAK` | unprobed | no run.sh yet |
|
||||
| `Programming/twm_4` | unprobed | no run.sh yet |
|
||||
| `Programming/USD` | unprobed | no run.sh yet |
|
||||
| `Programming/WDWR` | unprobed | no run.sh yet |
|
||||
| `Programming/ZPOB` | unprobed | no run.sh yet |
|
||||
114
install.sh
Executable file
114
install.sh
Executable file
@ -0,0 +1,114 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ============================================================================
|
||||
# Set this repository up for development.
|
||||
#
|
||||
# Usage:
|
||||
# ./install.sh install tools and wire the git hooks
|
||||
# ./install.sh --check verify only; exit 1 if anything is missing
|
||||
#
|
||||
# Idempotent: a second run reports what is already present and changes nothing.
|
||||
#
|
||||
# It ENDS by verifying, and fails loudly if the gate is not actually runnable.
|
||||
# A warning here would just recreate the bug it exists to prevent: the hooks
|
||||
# were absent from this repo entirely, so every "pre-commit passed" claim
|
||||
# before now was vacuous.
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly SCRIPT_DIR
|
||||
readonly UTILS_ROOT="${UTILS_ROOT:-$HOME/utils}"
|
||||
|
||||
CHECK_ONLY=0
|
||||
|
||||
log() { printf '==> %s\n' "$*"; }
|
||||
err() { printf 'error: %s\n' "$*" >&2; }
|
||||
|
||||
usage() {
|
||||
grep -E '^# \./install\.sh' "${BASH_SOURCE[0]}" | sed 's/^# //'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# The shared gates are not vendored here on purpose -- they live in one repo so
|
||||
# that every repo enforces the same rule. Without them the hooks cannot run.
|
||||
check_shared_gates() {
|
||||
if [[ ! -d "$UTILS_ROOT" ]]; then
|
||||
err "shared gates not found at $UTILS_ROOT"
|
||||
err " git clone https://github.com/kuhyx/utils \"$UTILS_ROOT\""
|
||||
err " (or set UTILS_ROOT to where it lives)"
|
||||
return 1
|
||||
fi
|
||||
log "shared gates: $UTILS_ROOT"
|
||||
}
|
||||
|
||||
install_tools() {
|
||||
local missing=()
|
||||
command -v shellcheck > /dev/null 2>&1 || missing+=(shellcheck)
|
||||
command -v pre-commit > /dev/null 2>&1 || missing+=(pre-commit)
|
||||
|
||||
if [[ ${#missing[@]} -eq 0 ]]; then
|
||||
log "tools already present: shellcheck, pre-commit"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ $CHECK_ONLY -eq 1 ]]; then
|
||||
err "missing tools: ${missing[*]}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "installing: ${missing[*]}"
|
||||
if command -v pacman > /dev/null 2>&1; then
|
||||
sudo pacman -S --needed --noconfirm "${missing[@]}"
|
||||
elif command -v apt-get > /dev/null 2>&1; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq "${missing[@]}"
|
||||
else
|
||||
err "no supported package manager; install manually: ${missing[*]}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
install_hooks() {
|
||||
if [[ $CHECK_ONLY -eq 1 ]]; then
|
||||
[[ -f "${SCRIPT_DIR}/.git/hooks/pre-commit" ]] || {
|
||||
err "git hooks are not installed"
|
||||
return 1
|
||||
}
|
||||
log "git hooks installed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "wiring git hooks (pre-commit and pre-push)"
|
||||
( cd "$SCRIPT_DIR" \
|
||||
&& pre-commit install > /dev/null \
|
||||
&& pre-commit install --hook-type pre-push > /dev/null )
|
||||
}
|
||||
|
||||
# The last word. If the gate cannot actually run, say so and exit non-zero.
|
||||
verify() {
|
||||
log "verifying the gate runs"
|
||||
if ! ( cd "$SCRIPT_DIR" && pre-commit run --all-files > /dev/null 2>&1 ); then
|
||||
err "pre-commit run --all-files failed -- the repo is not clean"
|
||||
err "run it directly to see why: pre-commit run --all-files"
|
||||
return 1
|
||||
fi
|
||||
log "OK: all hooks pass"
|
||||
}
|
||||
|
||||
main() {
|
||||
check_shared_gates
|
||||
install_tools
|
||||
install_hooks
|
||||
verify
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--check) CHECK_ONLY=1; shift ;;
|
||||
-h | --help) usage ;;
|
||||
*) err "unknown option: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
main "$@"
|
||||
146
run.sh
Executable file
146
run.sh
Executable file
@ -0,0 +1,146 @@
|
||||
#!/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 "$@"
|
||||
Loading…
Reference in New Issue
Block a user