diff --git a/.github/workflows/md-naming.yml b/.github/workflows/md-naming.yml new file mode 100644 index 00000000..ce96d06a --- /dev/null +++ b/.github/workflows/md-naming.yml @@ -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 diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 00000000..5bb2345a --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -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 diff --git a/DOCS-runnability.md b/DOCS-runnability.md new file mode 100644 index 00000000..365edbfb --- /dev/null +++ b/DOCS-runnability.md @@ -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 | diff --git a/install.sh b/install.sh new file mode 100755 index 00000000..acbe6489 --- /dev/null +++ b/install.sh @@ -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 "$@" diff --git a/run.sh b/run.sh new file mode 100755 index 00000000..b24d0cdd --- /dev/null +++ b/run.sh @@ -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