Gate the invariants, fix ECOTE's unrunnable tests, contain build output

Two invariants had been established by hand and left ungated, which is the
"a remedy that only works if someone remembers" pattern:

  - scripts/check_ignore_consistency.sh fails the commit if any TRACKED file
    is also gitignored. .binary-allowlist used to *instruct* the reader to
    re-run that check; now it is enforced. Verified both ways -- passes on the
    tree, and fails when an ignored file is force-added.

DOCS-runnability.md now really has two columns. Each environment writes only
its own gates/runnability-<env>.tsv and the doc renders from both, so a CI run
cannot clobber the local results. The ci tsv is gitignored: committing a copy
generated here would label this machine's capabilities as the runner's.
Rendering it surfaced a classic bug -- awk's NR==FNR idiom silently swallows
the SECOND file when the first is empty, so the table came out with a header
and no rows. It compares FILENAME instead.

runnability.yml deliberately does NOT diff against a committed baseline. A
status is a property of the machine, so `runnable` locally and `blocked` on a
runner is correct rather than drift, and diffing would make the workflow red
by construction. It fails on `error` instead -- a runner that crashes or
reports an unknown kind.

Then five more courses were actually run, because EADS alone had produced four
real bugs and one sample proves nothing:

  - ECOAR and EPFU build (nasm + 32-bit gcc, and gcc respectively).
  - ECOTE's 17 test modules could not run AT ALL. Its package is named `code/`,
    which shadows the standard library: pytest's debugging plugin imports pdb,
    pdb imports code, and it got this directory instead, so the suite died
    with an INTERNALERROR before collecting a single test. Renamed to
    translator/ with all 17 imports updated -- 26 tests now pass.
  - A LaTeX report builds. That one exposed a hazard rather than a bug:
    latexmk wrote its PDF beside the source, and in a course where the built
    name matches the submitted one it would have silently overwritten a
    tracked deliverable. Both the LaTeX and C++ runners now build into a
    gitignored build/, and rebuilding ECOGR leaves `git status` empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172dj34FDpao6wsix8ZuCzE
This commit is contained in:
Krzysztof Rudnicki 2026-08-28 18:43:13 +02:00
parent badc21d653
commit e512bde26f
44 changed files with 268 additions and 87 deletions

View File

@ -18,9 +18,10 @@
# - the repo root, gates/, scripts/ and .github/ have no entry either, so a # - the repo root, gates/, scripts/ and .github/ have no entry either, so a
# binary added there is blocked. # binary added there is blocked.
# #
# Every pattern here must also survive `git ls-files | git check-ignore # An allowlisted file that .gitignore ALSO matches fails silently on
# --stdin --no-index` printing nothing -- an allowlisted file that .gitignore # `git add` rather than erroring. That is enforced, not remembered: the
# also matches fails silently on `git add` rather than erroring. # ignore-consistency pre-commit hook (scripts/check_ignore_consistency.sh)
# fails the commit if any tracked file is gitignored.
# --- Reports and presentations (155 .pdf, 3 .docx, 1 .doc, 1 .pptx) --------- # --- Reports and presentations (155 .pdf, 3 .docx, 1 .doc, 1 .pptx) ---------
Programming/*.pdf Programming/*.pdf

40
.github/workflows/runnability.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: runnability
# Probes every course directory and fails if any RUNNER is broken.
#
# Deliberately NOT a diff against a committed baseline. A runner's status is a
# property of the machine, not of the commit: MATLAB, Unity and the .NET SDK
# are absent here and present locally, so `runnable` on a laptop and `blocked`
# on ubuntu-latest is correct rather than drift. Diffing the two would make
# this workflow red by construction, which is the one outcome the whole
# exercise is meant to avoid.
#
# What IS a defect, and what this catches: a run.sh that crashes, hangs, or
# reports an unknown kind. Those are `error`, and any `error` fails the job.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
jobs:
runnability:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Probe every course directory
run: ./run.sh status
- name: Fail on any broken runner
run: |
set -euo pipefail
if grep -qP '\terror\t' gates/runnability-ci.tsv; then
echo "::error::a course run.sh failed to probe"
grep -P '\terror\t' gates/runnability-ci.tsv
exit 1
fi
echo "all runners probed cleanly"
cut -f2 gates/runnability-ci.tsv | sort | uniq -c

4
.gitignore vendored
View File

@ -482,3 +482,7 @@ Programming/ERSMS-project/nginx/ssl/nginx.crt
# Output of ./run.sh run <dir> for the C++ courses. # Output of ./run.sh run <dir> for the C++ courses.
build/ build/
# Generated fresh by the runner on each CI run. Committing a local copy
# would label this machine capabilities as the runner is, which is a lie.
gates/runnability-ci.tsv

View File

@ -56,6 +56,15 @@ repos:
language: script language: script
pass_filenames: true pass_filenames: true
# A tracked file that .gitignore also matches fails silently on
# `git add`. 130 of those were fixed by hand; this keeps them fixed.
- id: ignore-consistency
name: no tracked file is gitignored
entry: scripts/check_ignore_consistency.sh
language: script
pass_filenames: false
always_run: true
# Thin shim into ~/utils, which owns the four markdown namespaces. # Thin shim into ~/utils, which owns the four markdown namespaces.
- id: md-naming - id: md-naming
name: markdown naming convention name: markdown naming convention

View File

@ -2,53 +2,57 @@
Generated by `./run.sh status`. Do not edit by hand. Generated by `./run.sh status`. Do not edit by hand.
Status is recorded PER ENVIRONMENT. A directory that is `runnable` locally and Status is recorded PER ENVIRONMENT, from `gates/runnability-local.tsv` and
`gates/runnability-ci.tsv`. A directory that is `runnable` locally and
`blocked` in CI is the normal case, not drift: the runner has no MATLAB, no `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. Unity and no .NET SDK. Each environment rewrites only its own file, so a CI
run cannot clobber the local column.
| Course | local | Detail | `-` means that environment has not probed yet.
|---|---|---|
| `NotProgramming/DPZ` | runnable | builds the report with latexmk | | Course | local | ci | Detail (local) |
| `NotProgramming/ECOGR` | runnable | builds the report with latexmk | |---|---|---|---|
| `NotProgramming/EDABA` | runnable | builds the report with latexmk | | `NotProgramming/DPZ` | runnable | - | builds the report with latexmk |
| `NotProgramming/EDABA-LAB` | runnable | builds the report with latexmk | | `NotProgramming/ECOGR` | runnable | - | builds the report with latexmk |
| `NotProgramming/EDISE` | runnable | builds the report with latexmk | | `NotProgramming/EDABA` | runnable | - | builds the report with latexmk |
| `NotProgramming/EDISP` | no-code | submitted lab reports as PDFs only; no source was produced | | `NotProgramming/EDABA-LAB` | runnable | - | builds the report with latexmk |
| `NotProgramming/ELAC` | runnable | builds the report with latexmk | | `NotProgramming/EDISE` | runnable | - | builds the report with latexmk |
| `NotProgramming/EMISY` | runnable | builds the report with latexmk | | `NotProgramming/EDISP` | no-code | - | submitted lab reports as PDFs only; no source was produced |
| `NotProgramming/ENUME` | blocked | needs MATLAB or Octave (none of: matlab octave) | | `NotProgramming/ELAC` | runnable | - | builds the report with latexmk |
| `NotProgramming/EPHY2` | no-code | submitted lab reports as PDFs only; no source was produced | | `NotProgramming/EMISY` | runnable | - | builds the report with latexmk |
| `NotProgramming/EPOGAI` | no-code | a presentation only; no source was produced | | `NotProgramming/ENUME` | blocked | - | needs MATLAB or Octave (none of: matlab octave) |
| `NotProgramming/ERPM` | runnable | builds the report with latexmk | | `NotProgramming/EPHY2` | no-code | - | submitted lab reports as PDFs only; no source was produced |
| `NotProgramming/ESDP1` | runnable | builds the report with latexmk | | `NotProgramming/EPOGAI` | no-code | - | a presentation only; no source was produced |
| `NotProgramming/ESOEN` | runnable | builds the report with latexmk | | `NotProgramming/ERPM` | runnable | - | builds the report with latexmk |
| `NotProgramming/MOM` | runnable | builds the report with latexmk | | `NotProgramming/ESDP1` | runnable | - | builds the report with latexmk |
| `NotProgramming/SDM` | runnable | builds the report with latexmk | | `NotProgramming/ESOEN` | runnable | - | builds the report with latexmk |
| `NotProgramming/SDM2` | runnable | builds the report with latexmk | | `NotProgramming/MOM` | runnable | - | builds the report with latexmk |
| `NotProgramming/SPD` | runnable | builds the report with latexmk | | `NotProgramming/SDM` | runnable | - | builds the report with latexmk |
| `NotProgramming/STUP` | runnable | npm install && npm start | | `NotProgramming/SDM2` | runnable | - | builds the report with latexmk |
| `Programming/AIS` | runnable | builds the report with latexmk | | `NotProgramming/SPD` | runnable | - | builds the report with latexmk |
| `Programming/BD2` | runnable | npm install && npm start | | `NotProgramming/STUP` | runnable | - | npm install && npm start |
| `Programming/BSS` | runnable | builds the report with latexmk | | `Programming/AIS` | runnable | - | builds the report with latexmk |
| `Programming/EADS` | runnable | compiles each standalone .cpp | | `Programming/BD2` | runnable | - | npm install && npm start |
| `Programming/EARIN` | runnable | python3 the entry script | | `Programming/BSS` | runnable | - | builds the report with latexmk |
| `Programming/ECOAR` | runnable | make | | `Programming/EADS` | runnable | - | compiles each standalone .cpp |
| `Programming/ECOTE` | runnable | python3 /home/kuhy/WUT_Computer_Science/Programming/ECOTE/program/code/main.py | | `Programming/EARIN` | runnable | - | python3 the entry script |
| `Programming/ECRYPT` | no-code | test1.py is a 0-byte stub; this course produced no submitted code | | `Programming/ECOAR` | runnable | - | make |
| `Programming/ECRYPT_PROJECT` | runnable | python3 /home/kuhy/WUT_Computer_Science/Programming/ECRYPT_PROJECT/code | | `Programming/ECOTE` | runnable | - | python3 /home/kuhy/WUT_Computer_Science/Programming/ECOTE/program/translator/main.py |
| `Programming/EGUI` | runnable | dotnet build | | `Programming/ECRYPT` | no-code | - | test1.py is a 0-byte stub; this course produced no submitted code |
| `Programming/EOOP` | runnable | compiles each standalone .cpp | | `Programming/ECRYPT_PROJECT` | runnable | - | python3 /home/kuhy/WUT_Computer_Science/Programming/ECRYPT_PROJECT/code |
| `Programming/EOPSY` | runnable | make | | `Programming/EGUI` | runnable | - | dotnet build |
| `Programming/EPFU` | runnable | make | | `Programming/EOOP` | runnable | - | compiles each standalone .cpp |
| `Programming/E-PSYOPS` | runnable | opens in the Unity editor | | `Programming/EOPSY` | runnable | - | make |
| `Programming/ERSMS-project` | runnable | docker compose up | | `Programming/EPFU` | runnable | - | make |
| `Programming/NLP` | runnable | python3 the entry script | | `Programming/E-PSYOPS` | runnable | - | opens in the Unity editor |
| `Programming/PBAD` | runnable | jupyter nbconvert --execute | | `Programming/ERSMS-project` | runnable | - | docker compose up |
| `Programming/PORR` | runnable | python3 /home/kuhy/WUT_Computer_Science/Programming/PORR/code | | `Programming/NLP` | runnable | - | python3 the entry script |
| `Programming/PSD` | runnable | starts the Kafka/Flink stack (installs packages with sudo) | | `Programming/PBAD` | runnable | - | jupyter nbconvert --execute |
| `Programming/psd_project` | runnable | mvn package | | `Programming/PORR` | runnable | - | python3 /home/kuhy/WUT_Computer_Science/Programming/PORR/code |
| `Programming/TRAK` | runnable | python3 the entry script | | `Programming/PSD` | runnable | - | starts the Kafka/Flink stack (installs packages with sudo) |
| `Programming/twm_4` | runnable | jupyter nbconvert --execute | | `Programming/psd_project` | runnable | - | mvn package |
| `Programming/USD` | no-code | README describes a MetaDrive RL assignment; no implementation was submitted | | `Programming/TRAK` | runnable | - | python3 the entry script |
| `Programming/WDWR` | runnable | python3 the entry script | | `Programming/twm_4` | runnable | - | jupyter nbconvert --execute |
| `Programming/ZPOB` | runnable | builds the report with latexmk | | `Programming/USD` | no-code | - | README describes a MetaDrive RL assignment; no implementation was submitted |
| `Programming/WDWR` | runnable | - | python3 the entry script |
| `Programming/ZPOB` | runnable | - | builds the report with latexmk |

View File

@ -1,9 +1,21 @@
To run unit tests: # ECOTE translator
Enter project environment:
source /ecote/bin/activate Translates LaTeX `tabular` environments into HTML tables.
Run tests: | Path | What it is |
|---|---|
| `translator/` | The implementation. Entry point `translator/main.py`. |
| `translator/functional_tests/` | End-to-end fixtures. |
| `unit_tests/test_code/` | 17 pytest modules. |
## Run the tests
```bash
python -m pytest unit_tests python -m pytest unit_tests
```
> The package used to be called `code/`, which shadows Python's standard
> library `code` module. That is not cosmetic: pytest's debugging plugin
> imports `pdb`, `pdb` imports `code`, and it got this directory instead, so
> the whole suite died with an INTERNALERROR before collecting a single test.
> Renaming it is what makes these tests runnable at all.

View File

@ -1,7 +1,7 @@
""" """
Tests begin document function Tests begin document function
""" """
from code.main import begin_document from translator.main import begin_document
# Write python tests for a function translating LaTeX documentclass to html # Write python tests for a function translating LaTeX documentclass to html
def given_empty_then_error(): def given_empty_then_error():

View File

@ -1,7 +1,7 @@
""" """
Tests begin document function Tests begin document function
""" """
from code.main import begin_tabular from translator.main import begin_tabular
# Write python tests for a function translating LaTeX documentclass to html # Write python tests for a function translating LaTeX documentclass to html
def given_empty_then_error(): def given_empty_then_error():

View File

@ -1,4 +1,4 @@
from code.main import command_name_check from translator.main import command_name_check
def given_valid_then_empty(): def given_valid_then_empty():

View File

@ -1,7 +1,7 @@
""" """
Tests document class function Tests document class function
""" """
from code.main import document_class from translator.main import document_class
# Write python tests for a function translating LaTeX documentclass to html # Write python tests for a function translating LaTeX documentclass to html

View File

@ -1,4 +1,4 @@
from code.main import document_class_only_checks from translator.main import document_class_only_checks
def test_missing_curly_bracket(): def test_missing_curly_bracket():

View File

@ -1,4 +1,4 @@
from code.main import generic_checks from translator.main import generic_checks
def test_empty_string(): def test_empty_string():

View File

@ -1,4 +1,4 @@
from code.main import generic_checks_command from translator.main import generic_checks_command
def test_empty_string(): def test_empty_string():

View File

@ -1,7 +1,7 @@
""" """
Tests begin document function Tests begin document function
""" """
from code.main import length_conversions from translator.main import length_conversions
# Write python tests for a function translating LaTeX documentclass to html # Write python tests for a function translating LaTeX documentclass to html

View File

@ -1,4 +1,4 @@
from code.main import only_pipes_and_space from translator.main import only_pipes_and_space
def given_empty_then_true(): def given_empty_then_true():

View File

@ -1,4 +1,4 @@
from code.main import split_columns from translator.main import split_columns
def given_empty_then_correct(): def given_empty_then_correct():

View File

@ -1,4 +1,4 @@
from code.main import split_rows from translator.main import split_rows
def given_empty_then_correct(): def given_empty_then_correct():

View File

@ -1,4 +1,4 @@
from code.main import tabular_columns_parameters from translator.main import tabular_columns_parameters
def given_empty_then_error(): def given_empty_then_error():

View File

@ -2,7 +2,7 @@
Tests tabular_parameters function Tests tabular_parameters function
""" """
from code.main import tabular_parameters from translator.main import tabular_parameters
def given_empty_then_empty(): def given_empty_then_empty():
""" """

View File

@ -2,7 +2,7 @@
Tests tabular_parameters function Tests tabular_parameters function
""" """
from code.main import tabular_required_parameters from translator.main import tabular_required_parameters
def given_empty_then_error(): def given_empty_then_error():

View File

@ -1,4 +1,4 @@
from code.main import translate_column from translator.main import translate_column
def given_empty_then_correct(): def given_empty_then_correct():

View File

@ -1,5 +1,5 @@
from code.main import translate_inside_to_html from translator.main import translate_inside_to_html
from code.main import tabular_required_parameters from translator.main import tabular_required_parameters
def given_correct_then_correct(): def given_correct_then_correct():

View File

@ -8,5 +8,5 @@ set -euo pipefail
. "$(dirname "$(readlink -f "$0")")/../../scripts/course_lib.sh" . "$(dirname "$(readlink -f "$0")")/../../scripts/course_lib.sh"
course_main --kind python \ course_main --kind python \
--entry "$(dirname "$(readlink -f "$0")")/program/code/main.py" \ --entry "$(dirname "$(readlink -f "$0")")/program/translator/main.py" \
-- "$@" -- "$@"

View File

@ -0,0 +1,44 @@
NotProgramming/DPZ runnable builds the report with latexmk
NotProgramming/ECOGR runnable builds the report with latexmk
NotProgramming/EDABA runnable builds the report with latexmk
NotProgramming/EDABA-LAB runnable builds the report with latexmk
NotProgramming/EDISE runnable builds the report with latexmk
NotProgramming/EDISP no-code submitted lab reports as PDFs only; no source was produced
NotProgramming/ELAC runnable builds the report with latexmk
NotProgramming/EMISY runnable builds the report with latexmk
NotProgramming/ENUME blocked needs MATLAB or Octave (none of: matlab octave)
NotProgramming/EPHY2 no-code submitted lab reports as PDFs only; no source was produced
NotProgramming/EPOGAI no-code a presentation only; no source was produced
NotProgramming/ERPM runnable builds the report with latexmk
NotProgramming/ESDP1 runnable builds the report with latexmk
NotProgramming/ESOEN runnable builds the report with latexmk
NotProgramming/MOM runnable builds the report with latexmk
NotProgramming/SDM runnable builds the report with latexmk
NotProgramming/SDM2 runnable builds the report with latexmk
NotProgramming/SPD runnable builds the report with latexmk
NotProgramming/STUP runnable npm install && npm start
Programming/AIS runnable builds the report with latexmk
Programming/BD2 runnable npm install && npm start
Programming/BSS runnable builds the report with latexmk
Programming/EADS runnable compiles each standalone .cpp
Programming/EARIN runnable python3 the entry script
Programming/ECOAR runnable make
Programming/ECOTE runnable python3 /home/kuhy/WUT_Computer_Science/Programming/ECOTE/program/translator/main.py
Programming/ECRYPT no-code test1.py is a 0-byte stub; this course produced no submitted code
Programming/ECRYPT_PROJECT runnable python3 /home/kuhy/WUT_Computer_Science/Programming/ECRYPT_PROJECT/code
Programming/EGUI runnable dotnet build
Programming/EOOP runnable compiles each standalone .cpp
Programming/EOPSY runnable make
Programming/EPFU runnable make
Programming/E-PSYOPS runnable opens in the Unity editor
Programming/ERSMS-project runnable docker compose up
Programming/NLP runnable python3 the entry script
Programming/PBAD runnable jupyter nbconvert --execute
Programming/PORR runnable python3 /home/kuhy/WUT_Computer_Science/Programming/PORR/code
Programming/PSD runnable starts the Kafka/Flink stack (installs packages with sudo)
Programming/psd_project runnable mvn package
Programming/TRAK runnable python3 the entry script
Programming/twm_4 runnable jupyter nbconvert --execute
Programming/USD no-code README describes a MetaDrive RL assignment; no implementation was submitted
Programming/WDWR runnable python3 the entry script
Programming/ZPOB runnable builds the report with latexmk
1 NotProgramming/DPZ runnable builds the report with latexmk
2 NotProgramming/ECOGR runnable builds the report with latexmk
3 NotProgramming/EDABA runnable builds the report with latexmk
4 NotProgramming/EDABA-LAB runnable builds the report with latexmk
5 NotProgramming/EDISE runnable builds the report with latexmk
6 NotProgramming/EDISP no-code submitted lab reports as PDFs only; no source was produced
7 NotProgramming/ELAC runnable builds the report with latexmk
8 NotProgramming/EMISY runnable builds the report with latexmk
9 NotProgramming/ENUME blocked needs MATLAB or Octave (none of: matlab octave)
10 NotProgramming/EPHY2 no-code submitted lab reports as PDFs only; no source was produced
11 NotProgramming/EPOGAI no-code a presentation only; no source was produced
12 NotProgramming/ERPM runnable builds the report with latexmk
13 NotProgramming/ESDP1 runnable builds the report with latexmk
14 NotProgramming/ESOEN runnable builds the report with latexmk
15 NotProgramming/MOM runnable builds the report with latexmk
16 NotProgramming/SDM runnable builds the report with latexmk
17 NotProgramming/SDM2 runnable builds the report with latexmk
18 NotProgramming/SPD runnable builds the report with latexmk
19 NotProgramming/STUP runnable npm install && npm start
20 Programming/AIS runnable builds the report with latexmk
21 Programming/BD2 runnable npm install && npm start
22 Programming/BSS runnable builds the report with latexmk
23 Programming/EADS runnable compiles each standalone .cpp
24 Programming/EARIN runnable python3 the entry script
25 Programming/ECOAR runnable make
26 Programming/ECOTE runnable python3 /home/kuhy/WUT_Computer_Science/Programming/ECOTE/program/translator/main.py
27 Programming/ECRYPT no-code test1.py is a 0-byte stub; this course produced no submitted code
28 Programming/ECRYPT_PROJECT runnable python3 /home/kuhy/WUT_Computer_Science/Programming/ECRYPT_PROJECT/code
29 Programming/EGUI runnable dotnet build
30 Programming/EOOP runnable compiles each standalone .cpp
31 Programming/EOPSY runnable make
32 Programming/EPFU runnable make
33 Programming/E-PSYOPS runnable opens in the Unity editor
34 Programming/ERSMS-project runnable docker compose up
35 Programming/NLP runnable python3 the entry script
36 Programming/PBAD runnable jupyter nbconvert --execute
37 Programming/PORR runnable python3 /home/kuhy/WUT_Computer_Science/Programming/PORR/code
38 Programming/PSD runnable starts the Kafka/Flink stack (installs packages with sudo)
39 Programming/psd_project runnable mvn package
40 Programming/TRAK runnable python3 the entry script
41 Programming/twm_4 runnable jupyter nbconvert --execute
42 Programming/USD no-code README describes a MetaDrive RL assignment; no implementation was submitted
43 Programming/WDWR runnable python3 the entry script
44 Programming/ZPOB runnable builds the report with latexmk

41
run.sh
View File

@ -84,39 +84,56 @@ probe_one() {
cmd_status() { cmd_status() {
local env_label="${CI:+ci}" local env_label="${CI:+ci}"
env_label="${env_label:-local}" env_label="${env_label:-local}"
local store="${SCRIPT_DIR}/gates/runnability-${env_label}.tsv"
log "probing course directories (${env_label})" log "probing course directories (${env_label})"
local tmp
tmp="$(mktemp)"
trap 'rm -f "${tmp:-}"' RETURN
local dir rel line local dir rel line
: > "$store"
while IFS= read -r dir; do while IFS= read -r dir; do
rel="${dir#"${SCRIPT_DIR}"/}" rel="${dir#"${SCRIPT_DIR}"/}"
line="$(probe_one "$dir")" line="$(probe_one "$dir")"
printf '%s\t%s\n' "$rel" "$line" >> "$tmp" printf '%s\t%s\n' "$rel" "$line" >> "$store"
done < <(course_dirs) done < <(course_dirs)
write_runnability_doc "$tmp" "$env_label" # Each environment writes only its OWN file, then the doc is rendered from
# both. Writing a single shared column would mean a CI run silently
# overwrote the local results with its own -- and the whole point of
# recording per environment is that "runnable here, blocked on a runner"
# is the normal case rather than drift.
write_runnability_doc
log "wrote ${RUNNABILITY_DOC##*/}" log "wrote ${RUNNABILITY_DOC##*/}"
awk -F'\t' '{ n[$2]++ } END { for (k in n) printf " %-10s %d\n", k, n[k] }' "$tmp" awk -F'\t' '{ n[$2]++ } END { for (k in n) printf " %-10s %d\n", k, n[k] }' "$store"
} }
write_runnability_doc() { write_runnability_doc() {
local data="$1" env_label="$2" local local_tsv="${SCRIPT_DIR}/gates/runnability-local.tsv"
local ci_tsv="${SCRIPT_DIR}/gates/runnability-ci.tsv"
[[ -f "$local_tsv" ]] || : > "$local_tsv"
[[ -f "$ci_tsv" ]] || : > "$ci_tsv"
{ {
cat <<'MD' cat <<'MD'
# Runnability # Runnability
Generated by `./run.sh status`. Do not edit by hand. Generated by `./run.sh status`. Do not edit by hand.
Status is recorded PER ENVIRONMENT. A directory that is `runnable` locally and Status is recorded PER ENVIRONMENT, from `gates/runnability-local.tsv` and
`gates/runnability-ci.tsv`. A directory that is `runnable` locally and
`blocked` in CI is the normal case, not drift: the runner has no MATLAB, no `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. Unity and no .NET SDK. Each environment rewrites only its own file, so a CI
run cannot clobber the local column.
`-` means that environment has not probed yet.
MD MD
printf '| Course | %s | Detail |\n|---|---|---|\n' "$env_label" printf '| Course | local | ci | Detail (local) |\n|---|---|---|---|\n'
awk -F'\t' '{ printf "| `%s` | %s | %s |\n", $1, $2, $3 }' "$data" # Compare FILENAME rather than using the NR==FNR idiom: with an EMPTY
# first file awk never reads it, so NR==FNR stays true for every row
# of the second file and the whole table comes out blank.
awk -F'\t' -v cifile="$ci_tsv" '
FILENAME == cifile { ci[$1] = $2; next }
{ printf "| `%s` | %s | %s | %s |\n", $1, $2, ($1 in ci ? ci[$1] : "-"), $3 }
' "$ci_tsv" "$local_tsv"
} > "$RUNNABILITY_DOC" } > "$RUNNABILITY_DOC"
} }

View File

@ -0,0 +1,42 @@
#!/bin/bash
# ============================================================================
# Fail if any TRACKED file is also matched by a .gitignore rule.
#
# That combination fails silently: `git add` on such a file is not an error,
# the edit simply never lands. This repo had 130 of them -- dist/ swallowing
# the vendored jQuery bundle, and the LaTeX template's man-page patterns
# (*.[1-9]) eating EPFU's date-named doc/21.11/ directory along with its
# report.pdf.
#
# They were fixed by hand once. This is the gate that keeps them fixed, so
# that .binary-allowlist can stop *instructing* the reader to re-run a command
# and just have it enforced instead.
#
# Usage: scripts/check_ignore_consistency.sh
# ============================================================================
set -euo pipefail
main() {
local offenders
offenders="$(git ls-files | git check-ignore --stdin --no-index 2>/dev/null || true)"
if [[ -z "$offenders" ]]; then
exit 0
fi
echo "Tracked files that .gitignore also matches:" >&2
printf '%s\n' "$offenders" | sed 's/^/ /' >&2
cat >&2 <<'MSG'
Each of these fails SILENTLY on `git add` -- not an error, the edit just
never lands. Fix by either:
1. adding a "!" override in .gitignore (if the file belongs in the repo), or
2. `git rm --cached` it (if it is build output that should never have been
tracked; --cached keeps it on disk).
MSG
exit 1
}
main "$@"

View File

@ -140,11 +140,19 @@ _run() {
[[ -n "$tex" ]] || _no_code "no .tex found" [[ -n "$tex" ]] || _no_code "no .tex found"
local tool local tool
tool="$(_require_any 'a LaTeX toolchain' latexmk pdflatex xelatex)" tool="$(_require_any 'a LaTeX toolchain' latexmk pdflatex xelatex)"
# Build into build/, never beside the source. A submitted report.pdf
# is a tracked deliverable, and a rebuild whose output name happens
# to match it would silently overwrite what was handed in.
local dir base
dir="$(dirname "$tex")"
base="$(basename "$tex")"
if [[ $tool == latexmk ]]; then if [[ $tool == latexmk ]]; then
( cd "$(dirname "$tex")" && latexmk -pdf -interaction=nonstopmode "$(basename "$tex")" ) ( cd "$dir" && latexmk -pdf -interaction=nonstopmode -outdir=build "$base" )
else else
( cd "$(dirname "$tex")" && "$tool" -interaction=nonstopmode "$(basename "$tex")" ) ( cd "$dir" && mkdir -p build \
&& "$tool" -interaction=nonstopmode -output-directory=build "$base" )
fi fi
printf 'output in %s/build\n' "$dir"
;; ;;
python) python)
local entry="${COURSE_ENTRY:-}" local entry="${COURSE_ENTRY:-}"