Two things kept CI from being green, and neither was the code.
First, GitHub Actions was DISABLED at the repository level. The workflows
registered fine and simply never ran -- `gh api .../actions/permissions`
returned {"enabled": false}. Now enabled.
Second, once it ran, shellcheck failed on a file that is clean locally:
SC2236 in PSD/zin3/python/run_temperature_system.sh. `language: system` hooks
use whatever is on PATH, and Ubuntu's packaged shellcheck is not the 0.11.0
this machine has, so the build went red for a difference in tool version
rather than in the code. That is the failure mode testsAndMisc pins tools to
avoid, so the workflow now installs shellcheck v0.11.0 and verifies its
SHA256 -- checked here first: the digest matches and the archive really does
unpack to shellcheck-v0.11.0/shellcheck.
The `! -z` -> `-n` fix is applied too. Pinning stops the next version drift;
fixing the code means this instance is right under either version.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172dj34FDpao6wsix8ZuCzE
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
`pre-commit run --all-files` is green. The config only carries hooks whose
subject is already clean, so it stays green at every commit -- file-length,
ruff/mypy and dependency-freshness are listed as blocked, each on its own
cleanup, and the file-REWRITING hooks (trailing-whitespace, end-of-file-fixer)
are held back for their own reviewed commit.
Turning it on found four real things:
1. A committed private key: ERSMS-project/nginx/ssl/nginx.key. It is a
self-signed CN=localhost cert that had already expired (notAfter
2025-06-16), so the risk is nil -- but it is regenerable in one openssl
call, so it is replaced by generate-cert.sh rather than excluded from the
hook. The script says plainly that removing it from HEAD does NOT
unpublish it: history is deliberately not rewritten, which is acceptable
only because nothing ever trusted this key.
2. Four files with unresolved merge conflict markers committed, left over
from the subtree merges. EARIN/.gitignore and .vscode/extensions.json are
resolved as the union of both sides. The two Unity folder .meta files each
had two candidate GUIDs; grepping showed neither is referenced anywhere
(folder GUIDs rarely are), so either is safe and the HEAD side is kept --
an arbitrary choice, but a checked one rather than a guess.
3. 62 shellcheck findings across 11 scripts, all fixed, no disables added.
The interesting pattern was `cd X; cmd &; PID=$!; cd ..`, which trips both
SC2164 and SC2103 and cannot use a plain subshell without losing the PID.
`( cd X && exec cmd ) &` keeps the directory, the PID and the semantics.
EOPSY lab1's submitted solution was verified by RUNNING it afterwards:
a->A, B->b, foldera->FOLDERA, FOLDERB->folderb, exactly as before.
4. check-json failing on 11 files, which was the wrong parser rather than a
defect -- tsconfig*.json and .vscode/*.json are JSONC, where TypeScript
and VS Code both allow comments. Those two path classes are excluded and
every other .json is still checked. Excluding them is what surfaced the
merge conflict in (2) as the one genuine failure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172dj34FDpao6wsix8ZuCzE
Five files were outside README / CLAUDE* / DOCS* / TODO*, and two were over
the 250-line cap. Both gates now pass repo-wide.
Renames: the two lowercase readme.md files, and three documents into DOCS-*.
The AIS requirements doc split at its own boundary into functional (101) and
non-functional (174). The WDWR report split into six numbered sections behind
an index that keeps the original filename, 38-235 lines each; 753 lines of
body plus the 18-line header account for all 771 original lines, so nothing
was dropped.
That WDWR file needed checking before splitting: it sits next to main.tex and
main.pdf and looked like a conversion of them. It is not -- main.tex is
project 25406 and the markdown is project 18402, a different document. So it
is authored content and the split is real work, not churn on a build artifact.
Filled the three empty READMEs and replaced ECOTE's, which was a stale copy of
the root README still claiming the group projects were excluded -- they were
merged in by subtree some time ago.
EOPSY/lab1 turned out not to be the code-free directory the survey suggested:
it has modify.sh and modify_examples.sh. The oddly-named empty files beside
them are the exercise's OUTPUT, committed so the before/after state is
visible, and its README now says so rather than leaving them looking like junk.
CLAUDE.md follows the testsAndMisc structure. Writing it caught a nice one:
the md-naming marker is a biconditional, so quoting the literal removal string
in a non-TODO file fails the gate. The gate was right; the text is rephrased.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172dj34FDpao6wsix8ZuCzE
621 binaries are tracked here and they are supposed to be: 155 PDFs, 275
PNGs, 68 JPGs, the submitted reports and figures. Decision 3 keeps them and
skips the history rewrite, so a blanket block would be wrong. What the gate
is for is stopping NEW junk.
So the allowlist is scoped by path rather than by extension alone. Documents,
figures and datasets are allowed under Programming/ and NotProgramming/;
compiled output (.o .so .a .exe .dll .dylib .class .pyc) is allowed nowhere
and has no entry at all; and the repo root, gates/, scripts/ and .github/
have no entry either, so a binary dropped in the tooling directories is
blocked. The .tgz entries are load-bearing -- EOPSY ships its simulators as
ftp/{sched,memory}.tgz and `make setup` extracts them.
Verified three ways, because a gate that passes everything is vacuous:
- all 2749 tracked files -> exit 0
- probe.exe at root, probe.png in gates/ -> BLOCKED, exit 1
- a real submitted PDF -> exit 0
plus shellcheck clean.
The script is a copy of testsAndMisc's, which is where this gate lives today
-- it is not in the shared ~/utils set. The header says so and says to move
it to ~/utils rather than let the two drift, since a hand-copied gate is what
produced four forks of the 250-line cap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172dj34FDpao6wsix8ZuCzE
Untracked (--cached, so they stay on disk): 76 LaTeX artifacts, 7 .xdv XeTeX
intermediates, 18 -eps-converted-to.pdf from epstopdf, 32 files of .NET
obj/Debug/net6.0 output including three copies of lab2.dll, 16 .class/.o, the
9 MB Nx cache with its native .node, and an entire committed Python
virtualenv at ECOTE/program/ecote (python 3.10.10, 18 files).
The existing 407-line LaTeX .gitignore was NOT unused as I first assumed --
it already covered *.aux and *.class, and the artifacts simply predate it. So
this appends the categories it missed (*.o, obj/, .venv/, .nx/, target/)
rather than rewriting something that was working.
The second half is the more interesting bug. Auditing with
git ls-files | git check-ignore --stdin --no-index
found 130 files that were tracked AND ignored. That combination fails
silently: `git add` on such a file is not an error, the edit just never
lands. Causes were dist/ swallowing the vendored jQuery/Bootstrap bundle,
.vscode/ and .idea/, and -- the good one -- the LaTeX template's man-page
patterns *.[1-9] and *.[1-9][0-9] matching EPFU's date-named report
directories, so doc/21.11/ read as a man page section and took report.pdf
and seven screenshots with it.
32 of those 130 turned out to be more build output and were untracked too;
the rest got targeted ! overrides. The first attempt used a blanket
!NotProgramming/EMISY/** which re-enabled that course's own LaTeX artifacts,
so it is narrowed to the *.1 schematic exports that actually needed it.
TRAK's nested .gitignore excluded the .vscode DIRECTORY, which stops git
descending, so no root-level ! can reach inside -- that one had to be fixed
at its source.
Both invariants now verified: 0 tracked files ignored, and 0 of the 189
untracked artifacts leaked back into view.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172dj34FDpao6wsix8ZuCzE
First step of bringing this archive up to the fleet standard: nothing can be
linted until the gates know what is not kuhy's code.
gates/vendored.txt is the source of truth, one path per line with the reason
AND how it was verified, because an unverified entry is a suppression wearing
a costume. Seven subtrees: the ASP.NET scaffolder's jQuery/Bootstrap bundle
(~98% of all JS and CSS here), stock Unity package and template assets,
manufacturer-supplied Altium/PADS component libraries for the OLED and
HMC5883L parts, and the two EOPSY simulator trees.
The EOPSY entries were the one judgement call, and they turned out not to be:
work/ is the extraction target of `make setup`, and extracting the committed
ftp/*.tgz and diffing shows 14/14 java files byte-identical to the department
originals. ControlPanel.java (1852 lines) and Kernel.java (511) are therefore
not student work and are not split.
Deliberately NOT vendored: EMISY's README, declaration and report are kuhy's
own writing; E-PSYOPS Assets/Scripts (12 .cs) is authored; and the Unity Input
System binding is already exempt via its auto-generated banner, so it needs no
entry. Narrow entries beat one blanket directory.
TODO-file-length-250.md parks the one thing this mechanism cannot express: 27
data .txt files whose lines are long enough that the cap reads them as prose.
They resist directory-prefix exemption because the SemEval corpus dirs mix the
official distribution with kuhy's own GPT outputs, and WDWR/projekt/R holds
both scenarios.txt and the authored model.R. That blocks only the installation
of the file-length gate, which is several milestones away.
Mirrored into the shared gate as kuhyx/utils@a4a3e0a.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172dj34FDpao6wsix8ZuCzE