From 9099e81843f68b078c9f4da93cfea560373dae20 Mon Sep 17 00:00:00 2001 From: xormania <127287135+xormania@users.noreply.github.com> Date: Tue, 1 Sep 2026 18:21:18 -0400 Subject: [PATCH 1/6] =?UTF-8?q?repo:=20the=20term=20wall=20=E2=80=94=20nam?= =?UTF-8?q?es=20this=20organisation=20does=20not=20use,=20refused=20in=20C?= =?UTF-8?q?I?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A composite action every repo's ci job runs: it scans tracked content, file paths, the change's commit messages (base...head via the API, or the pushed commit), the pull request title and body, and the branch name for names that must not appear here — not affirmed, not negated, not cited. The wall never spells what it refuses and masks every hit it prints; a surface it could not read is a hit, never a pass. This repo's own ci runs it and then proves it fires on a planted fault and stays quiet on a clean neighbour. Verified locally: 10/10 cases (clean tree and PR; planted content, path, title, branch name, commit message via API and via push; API unreachable) exit as specified, and the action's files pass their own wall. Source: original Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012Jj94rkp3tfHAxUkTCthgY --- .github/actions/term-wall/action.yml | 14 +++++ .github/actions/term-wall/term-wall.sh | 83 ++++++++++++++++++++++++++ .github/workflows/ci.yml | 12 ++++ 3 files changed, 109 insertions(+) create mode 100644 .github/actions/term-wall/action.yml create mode 100755 .github/actions/term-wall/term-wall.sh diff --git a/.github/actions/term-wall/action.yml b/.github/actions/term-wall/action.yml new file mode 100644 index 0000000..0d95436 --- /dev/null +++ b/.github/actions/term-wall/action.yml @@ -0,0 +1,14 @@ +name: term wall +description: >- + Refuses names this organisation does not use — in tracked content, in + file paths, in the change's commit messages, in the pull request title + and body, and in the branch name. The wall never spells what it + refuses and masks every hit it prints. +runs: + using: composite + steps: + - name: term wall + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: bash "$GITHUB_ACTION_PATH/term-wall.sh" diff --git a/.github/actions/term-wall/term-wall.sh b/.github/actions/term-wall/term-wall.sh new file mode 100755 index 0000000..12b7522 --- /dev/null +++ b/.github/actions/term-wall/term-wall.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# term-wall.sh — names this organisation does not use, refused everywhere. +# +# The wall scans a change for names that must not appear here: not +# affirmed, not negated, not cited. It never spells the names it refuses +# — the pattern below would otherwise be its own first hit — and it masks +# every hit it prints, so the log does not carry what the tree may not. +# +# Surfaces, in order: +# 1. tracked file content (case-insensitive, binaries skipped) +# 2. tracked file paths +# 3. the commit messages of the change (PR: base...head; push: before...after) +# 4. the pull request title and body +# 5. the branch name +# +# Exit 0 clean. Exit 1 on any hit. A surface that could not be read is a +# hit too — "could not look" is never "found nothing". +# +# Outside GitHub Actions (no GITHUB_EVENT_PATH) only surfaces 1 and 2 run, +# against the current directory; that is the self-test's mode. +set -uo pipefail + +pat='s[c]ient[ _-]?db|u[s]cient' +rc=0 +mask() { sed -E "s/($pat)/[forbidden name]/Ig"; } +hit() { printf '::error::term wall: %s\n' "$1"; rc=1; } +scan() { # scan — text as an argument, never a pipe: a hit must + # set rc in this shell, and a pipeline's stages run in subshells. + local surface=$1 text=$2 found + found=$(printf '%s\n' "$text" | grep -i -n -E "$pat" 2>/dev/null | mask | head -40 || true) + if [[ -n $found ]]; then + printf '%s\n' "$found" | sed "s/^/ $surface: /" + hit "forbidden name in $surface" + fi +} + +# 1. tracked content +content=$(git ls-files -z 2>/dev/null | xargs -0 -r grep -I -i -n -E "$pat" -- 2>/dev/null || true) +if [[ -n $content ]]; then + printf '%s\n' "$content" | mask | head -40 | sed 's/^/ content: /' + hit "forbidden name in tracked content" +fi + +# 2. tracked paths +scan "path" "$(git ls-files 2>/dev/null)" + +# 3-5. the change itself, when running under Actions +if [[ -n ${GITHUB_EVENT_PATH:-} && -f ${GITHUB_EVENT_PATH:-} ]]; then + event=${GITHUB_EVENT_NAME:-} + repo=${GITHUB_REPOSITORY:?GITHUB_REPOSITORY unset} + case $event in + pull_request|pull_request_target) + base=$(jq -r '.pull_request.base.sha' "$GITHUB_EVENT_PATH") + head=$(jq -r '.pull_request.head.sha' "$GITHUB_EVENT_PATH") + scan "pull request title/body" "$(jq -r '.pull_request.title, (.pull_request.body // "")' "$GITHUB_EVENT_PATH")" + scan "branch name" "${GITHUB_HEAD_REF:-}" + ;; + push) + base=$(jq -r '.before' "$GITHUB_EVENT_PATH") + head=$(jq -r '.after' "$GITHUB_EVENT_PATH") + scan "branch name" "${GITHUB_REF_NAME:-}" + ;; + *) + base=""; head="";; + esac + if [[ -n $head ]]; then + if [[ -z $base || $base =~ ^0+$ ]]; then + msgs=$(git log -1 --format=%B "$head" 2>/dev/null) || msgs="" + [[ -n $msgs ]] || hit "could not read the commit message of $head (UNREACHABLE is not a pass)" + else + if ! msgs=$(gh api "repos/$repo/compare/$base...$head?per_page=250" --jq '.commits[].commit.message' 2>/tmp/term-wall.api.err); then + hit "could not read commit messages $base...$head via the API — $(head -c 200 /tmp/term-wall.api.err) (UNREACHABLE is not a pass)" + msgs="" + fi + fi + scan "commit messages" "$msgs" + fi +fi + +if [[ $rc -eq 0 ]]; then + echo "term wall: clean — $(git ls-files 2>/dev/null | wc -l) tracked files, the change's messages, title, body and branch name carry no forbidden name" +fi +exit "$rc" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 457b587..b89eb6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,18 @@ jobs: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: persist-credentials: false + - name: term wall + uses: ./.github/actions/term-wall + - name: term wall fires on a planted fault, stays quiet on a clean neighbour + run: | + set -euo pipefail + wall="$GITHUB_WORKSPACE/.github/actions/term-wall/term-wall.sh" + t=$(mktemp -d); cd "$t"; git init -q; git config user.email ci@ci.invalid; git config user.name ci + printf 'clean\n' > c.txt; git add c.txt; git commit -qm clean + env -u GITHUB_EVENT_PATH bash "$wall" + printf 'x \x53\x63\x69\x65\x6e\x74\x44\x42 y\n' > p.txt; git add p.txt + if env -u GITHUB_EVENT_PATH bash "$wall"; then echo "::error::the wall stayed quiet on a planted fault"; exit 1; fi + echo "the wall fires on a planted fault and stays quiet on a clean neighbour" - name: JSON validity run: find . -name '*.json' -not -path './.git/*' -print0 | xargs -0 -r -n1 jq empty - name: YAML validity From e6324b61bb85e01a0d35de006e57c479092b50c9 Mon Sep 17 00:00:00 2001 From: xormania <127287135+xormania@users.noreply.github.com> Date: Tue, 1 Sep 2026 18:51:24 -0400 Subject: [PATCH 2/6] repo: pin the term wall's hit format and refusal classes Two producers read the contract differently: the location of a hit (with or without a colon), the pull request title and body as one surface or two, and the names of the refusal classes. The contract now pins all three in a table; the implementation moves to it, the tests stand. Source: original Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012Jj94rkp3tfHAxUkTCthgY --- .github/actions/term-wall/CONTRACT.md | 95 +++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 .github/actions/term-wall/CONTRACT.md diff --git a/.github/actions/term-wall/CONTRACT.md b/.github/actions/term-wall/CONTRACT.md new file mode 100644 index 0000000..9bf3f69 --- /dev/null +++ b/.github/actions/term-wall/CONTRACT.md @@ -0,0 +1,95 @@ +# The term wall — contract v1 + +Names this organisation does not use must not appear in any of its +repositories: not affirmed, not negated, not cited. The wall refuses +them, and the wall itself never carries them in any readable or +encoded form. + +## Instruments + +1. `.github/actions/term-wall/term-wall.sh` (run by the composite + action `.github/actions/term-wall`) — every repo's `ci` job runs it. +2. `ops/devlane/workflow/checks/term_wall.py` — the lane's local copy, + run by the commit-msg hook and by apply-push's guards. + +## The pattern is configuration, never tree content + +- The pattern is an extended, case-insensitive regular expression read + from the environment variable `TERM_WALL`. In CI the action takes it + from the org-level Actions variable `vars.TERM_WALL`. Locally the + Python check reads `TERM_WALL`, falling back to the gitignored file + `/ops/bin/term-wall.conf` (one line: the pattern). +- No tracked file may contain the pattern, a piece of it, or any + encoding of the names (hex, base64, bracket tricks, escapes). The + self-test's planted fault comes from `vars.TERM_WALL_PLANT` (a string + the pattern matches), never from the tree. +- An unset or empty pattern is a refusal, exit 2, stdout empty, one + line on stderr: `pattern: expected TERM_WALL set; found empty; + needed the org variable (CI) or ops/bin/term-wall.conf (local)`. + The wall never passes vacuously. + +## Surfaces (term-wall.sh) + +1. tracked content — every `git ls-files` path, binaries skipped, + case-insensitive; +2. tracked paths; +3. the commit messages of the change — `pull_request`: `base..head`; + `push`: `before..head`, or only the head commit when `before` is + all zeros — read from git (fetching what the checkout lacks), never + from an API: the action needs no token and declares none; +4. the pull request title and body (from the event payload); +5. the branch name (`GITHUB_HEAD_REF` for a PR, `GITHUB_REF_NAME` for + a push). + +Outside GitHub Actions (no `GITHUB_EVENT_PATH`), surfaces 1 and 2 run +against the current directory. The Python check covers surface 1 and +2 (`[--root DIR] [PATH ...]`), one message (`--message-file FILE`), a +range of commit messages (`--range BASE..HEAD`), or `--stdin`. + +## Outcomes, on the wire + +| exit | meaning | +|---|---| +| 0 | clean; exactly one summary line on stdout | +| 1 | at least one hit, every hit printed on stdout in the pinned format below; a surface that could not be read (fetch failed, payload unreadable) is itself a hit — could-not-look is never a pass | +| 2 | refusal; stdout empty, one stderr line `: expected …; found …; needed …` | + +Refusal classes: `pattern` (unset or empty), `git work tree` (not +inside one), `message` (missing message file), `range` (unresolvable). + +## Hit format, pinned + +One stdout line per hit: `: : `. The location never contains a +colon. The raw matched text never appears in any output. + +| surface | location | +|---|---| +| `content` | ` line ` | +| `path` | `` | +| `commit messages` | ` line ` | +| `pull request title` | `line ` | +| `pull request body` | `line ` | +| `branch name` | `` | +| `event payload` | `` — the hit when the payload cannot be read | +| `message` (`--message-file`) | `line ` | +| `range` (`--range`) | ` line ` | +| `stdin` | `line ` | + +## Self-test (the `.github` repo's own ci) + +With `TERM_WALL_PLANT` as the planted fault: a planted file fires +(exit 1, masked hit), a clean neighbour stays quiet (exit 0), and an +empty `TERM_WALL` refuses (exit 2). + +## Tests + +Tests execute the real instrument as a subprocess inside temporary git +repositories they create; nothing about the wall is mocked. They set +`TERM_WALL` explicitly to a test-only pattern (for example +`zz[q]orblat`) and plant matches of it, so no forbidden name exists +anywhere. They are deterministic and hermetic: no network, no sleeps, +no dependence on the caller's cwd, environment, or git identity +(configure user.name/user.email in each temp repo). Every test asserts +the exit code and the output shape. Push and pull-request events are +simulated with an event JSON file and the `GITHUB_*` variables. From 69a006cf23dcd554031ace458724d49e5f5b98eb Mon Sep 17 00:00:00 2001 From: Apply Push Bridge Date: Tue, 1 Sep 2026 18:51:46 -0400 Subject: [PATCH 3/6] =?UTF-8?q?repo:=20term=20wall=20tests,=20from=20the?= =?UTF-8?q?=20contract=20=E2=80=94=2015=20real=20executions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Authored from the contract by a producer other than the implementer: each test runs term-wall.sh as a subprocess in its own temporary git repository with a test-only pattern, simulating pull_request and push events, and asserts exit code and output shape. Red at this commit: 13 of 15 fail, identically over three runs. Source: original Co-Authored-By: GPT-5.6 Sol Claude-Session: https://claude.ai/code/session_012Jj94rkp3tfHAxUkTCthgY Apply-Push-Job: 20260901T225145Z-apply-push-ca3088 Patch-SHA256: 5c280fccf2f955ddce536df7b4d99ca4c652c5a7edd059354eab12a985bb92e1 --- .../actions/term-wall/tests/test_term_wall.py | 239 ++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 .github/actions/term-wall/tests/test_term_wall.py diff --git a/.github/actions/term-wall/tests/test_term_wall.py b/.github/actions/term-wall/tests/test_term_wall.py new file mode 100644 index 0000000..b3448b7 --- /dev/null +++ b/.github/actions/term-wall/tests/test_term_wall.py @@ -0,0 +1,239 @@ +import json +import os +from pathlib import Path +import re +import subprocess +import tempfile +import unittest + + +SCRIPT = Path(__file__).resolve().parents[1] / "term-wall.sh" +PATTERN = "zz[q]orblat" +PLANT = "zzqor" + "blat" +REFUSAL = ( + "pattern: expected TERM_WALL set; found empty; needed the org variable (CI) " + "or ops/bin/term-wall.conf (local)" +) + + +class TermWallTests(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.root = Path(self.temporary.name) + self.repo = self.root / "repo" + self.repo.mkdir() + self.home = self.root / "home" + self.home.mkdir() + self.git("init", "-q") + self.git("config", "user.name", "Term Wall Test") + self.git("config", "user.email", "term-wall@example.invalid") + self.write("clean.txt", "ordinary text\n") + self.commit("initial clean commit") + + def tearDown(self): + self.temporary.cleanup() + + def environment(self, **values): + env = { + "PATH": os.environ.get("PATH", "/usr/bin:/bin"), + "HOME": str(self.home), + "LC_ALL": "C", + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_TERMINAL_PROMPT": "0", + "TERM_WALL": PATTERN, + } + env.update({key: str(value) for key, value in values.items()}) + return env + + def git(self, *arguments, cwd=None): + return subprocess.run( + ["git", *arguments], + cwd=cwd or self.repo, + env=self.environment(), + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True, + ).stdout.strip() + + def write(self, relative, contents): + path = self.repo / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(contents, encoding="utf-8") + return path + + def commit(self, message): + self.git("add", "--all") + self.git("commit", "-q", "--allow-empty", "-m", message) + return self.git("rev-parse", "HEAD") + + def event(self, name, payload, *, head_ref="feature", ref_name="main"): + path = self.root / f"{name}.json" + path.write_text(json.dumps(payload), encoding="utf-8") + return { + "GITHUB_EVENT_PATH": path, + "GITHUB_EVENT_NAME": name, + "GITHUB_REPOSITORY": "example/term-wall-test", + "GITHUB_HEAD_REF": head_ref, + "GITHUB_REF_NAME": ref_name, + } + + def run_wall(self, *, cwd=None, env=None): + return subprocess.run( + [str(SCRIPT)], + cwd=cwd or self.repo, + env=env or self.environment(), + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=15, + ) + + def assert_clean(self, result): + self.assertEqual(result.returncode, 0, result) + self.assertEqual(result.stderr, "") + lines = result.stdout.splitlines() + self.assertEqual(len(lines), 1, result.stdout) + self.assertRegex(lines[0], r"^term wall: clean(?:\b|\s|$)") + + def assert_hit(self, result, surface): + self.assertEqual(result.returncode, 1, result) + self.assertEqual(result.stderr, "") + self.assertNotIn(PLANT, result.stdout.lower()) + self.assertNotIn(PLANT, result.stderr.lower()) + lines = result.stdout.splitlines() + self.assertTrue(lines, "a hit must be printed on stdout") + matching = [line for line in lines if line.startswith(surface + ": ")] + self.assertTrue(matching, f"missing {surface!r} hit in {result.stdout!r}") + for line in matching: + self.assertRegex( + line, + rf"^{re.escape(surface)}: [^:]+: .*\[forbidden name\].*$", + ) + + def test_clean_tree(self): + self.assert_clean(self.run_wall()) + + def test_tracked_content_hit_is_masked(self): + self.write("planted.txt", f"before {PLANT.upper()} after\n") + self.commit("add fixture") + self.assert_hit(self.run_wall(), "content") + + def test_binary_tracked_content_is_skipped(self): + (self.repo / "fixture.bin").write_bytes(b"\x00" + PLANT.encode("ascii") + b"\xff") + self.commit("add binary fixture") + self.assert_clean(self.run_wall()) + + def test_tracked_path_hit_is_masked(self): + self.write(f"notes-{PLANT}.txt", "ordinary text\n") + self.commit("add fixture") + self.assert_hit(self.run_wall(), "path") + + def pr_environment(self, *, title="Clean title", body="Clean body", head_ref="feature"): + base = self.git("rev-parse", "HEAD^") + head = self.git("rev-parse", "HEAD") + payload = { + "pull_request": { + "base": {"sha": base}, + "head": {"sha": head}, + "title": title, + "body": body, + } + } + values = self.event("pull_request", payload, head_ref=head_ref) + return self.environment(**values) + + def test_pull_request_title_hit(self): + self.commit("clean feature commit") + result = self.run_wall(env=self.pr_environment(title=f"Review {PLANT} now")) + self.assert_hit(result, "pull request title") + + def test_pull_request_body_hit(self): + self.commit("clean feature commit") + result = self.run_wall(env=self.pr_environment(body=f"Body has {PLANT}.")) + self.assert_hit(result, "pull request body") + + def test_pull_request_branch_name_hit(self): + self.commit("clean feature commit") + result = self.run_wall(env=self.pr_environment(head_ref=f"topic-{PLANT}")) + self.assert_hit(result, "branch name") + + def test_pull_request_range_commit_message_is_fetched_offline(self): + base = self.git("rev-parse", "HEAD") + self.commit(f"message contains {PLANT}") + head = self.git("rev-parse", "HEAD") + self.git("branch", "base-for-test", base) + self.git("remote", "add", "origin", str(self.repo)) + payload = { + "pull_request": { + "base": {"sha": base}, + "head": {"sha": head}, + "title": "Clean title", + "body": "Clean body", + } + } + result = self.run_wall(env=self.environment(**self.event("pull_request", payload))) + self.assert_hit(result, "commit messages") + + def test_push_with_zero_before_scans_head_commit_only(self): + head = self.commit(f"new branch says {PLANT}") + payload = {"before": "0" * 40, "after": head} + result = self.run_wall(env=self.environment(**self.event("push", payload))) + self.assert_hit(result, "commit messages") + + def test_push_with_range_scans_changed_commit_messages(self): + before = self.git("rev-parse", "HEAD") + after = self.commit(f"range says {PLANT}") + payload = {"before": before, "after": after} + result = self.run_wall(env=self.environment(**self.event("push", payload))) + self.assert_hit(result, "commit messages") + + def test_push_branch_name_hit(self): + head = self.commit("clean push commit") + payload = {"before": "0" * 40, "after": head} + values = self.event("push", payload, ref_name=f"release-{PLANT}") + self.assert_hit(self.run_wall(env=self.environment(**values)), "branch name") + + def test_unreadable_event_payload_is_a_hit(self): + payload = self.root / "malformed.json" + payload.write_text("not valid JSON\n", encoding="utf-8") + env = self.environment( + GITHUB_EVENT_PATH=payload, + GITHUB_EVENT_NAME="pull_request", + GITHUB_REPOSITORY="example/term-wall-test", + GITHUB_HEAD_REF="feature", + GITHUB_REF_NAME="main", + ) + result = self.run_wall(env=env) + self.assertEqual(result.returncode, 1, result) + self.assertEqual(result.stderr, "") + self.assertNotIn(PLANT, result.stdout + result.stderr) + self.assertRegex(result.stdout, r"(?m)^event payload: [^:]+: .+$") + + def test_term_wall_unset_refuses(self): + env = self.environment() + del env["TERM_WALL"] + result = self.run_wall(env=env) + self.assertEqual(result.returncode, 2, result) + self.assertEqual(result.stdout, "") + self.assertEqual(result.stderr, REFUSAL + "\n") + + def test_term_wall_empty_refuses(self): + result = self.run_wall(env=self.environment(TERM_WALL="")) + self.assertEqual(result.returncode, 2, result) + self.assertEqual(result.stdout, "") + self.assertEqual(result.stderr, REFUSAL + "\n") + + def test_not_a_git_work_tree_refuses(self): + outside = self.root / "outside" + outside.mkdir() + result = self.run_wall(cwd=outside) + self.assertEqual(result.returncode, 2, result) + self.assertEqual(result.stdout, "") + lines = result.stderr.splitlines() + self.assertEqual(len(lines), 1, result.stderr) + self.assertRegex(lines[0], r"^git work tree: expected .+; found .+; needed .+$") + + +if __name__ == "__main__": + unittest.main() From ce33e5e85956cf25a9ad93ae349d7152cf721572 Mon Sep 17 00:00:00 2001 From: Apply Push Bridge Date: Tue, 1 Sep 2026 18:51:49 -0400 Subject: [PATCH 4/6] =?UTF-8?q?repo:=20the=20term=20wall=20to=20its=20cont?= =?UTF-8?q?ract=20=E2=80=94=20pattern=20from=20configuration,=20no=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit term-wall.sh reads its pattern only from TERM_WALL (the action takes it from vars.TERM_WALL) and refuses on the wire when it is unset; commit messages come from git (base..head, or the head commit on a branch creation), fetched offline, never from an API, so the action holds no token; every hit is masked; the self-test plants vars.TERM_WALL_PLANT and proves the refusal. CONTRACT.md carries the contract verbatim. Source: original Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012Jj94rkp3tfHAxUkTCthgY Apply-Push-Job: 20260901T225148Z-apply-push-dc1f97 Patch-SHA256: 3f59b7252e7ae3b3e0c7894ec551f7e1811d1811f7ed285ef585232bb740fddb --- .github/actions/term-wall/action.yml | 11 +- .github/actions/term-wall/term-wall.sh | 145 ++++++++++++++++++------- .github/workflows/ci.yml | 17 ++- 3 files changed, 124 insertions(+), 49 deletions(-) diff --git a/.github/actions/term-wall/action.yml b/.github/actions/term-wall/action.yml index 0d95436..796452e 100644 --- a/.github/actions/term-wall/action.yml +++ b/.github/actions/term-wall/action.yml @@ -1,14 +1,17 @@ name: term wall description: >- Refuses names this organisation does not use — in tracked content, in - file paths, in the change's commit messages, in the pull request title - and body, and in the branch name. The wall never spells what it - refuses and masks every hit it prints. + file paths, in the change's commit messages (read from git, never from + an API: no token needed or declared), in the pull request title and + body, and in the branch name. The pattern comes only from the + org-level Actions variable TERM_WALL; an empty pattern is a refusal, + never a pass. The wall never spells what it refuses and masks every + hit it prints. runs: using: composite steps: - name: term wall shell: bash env: - GH_TOKEN: ${{ github.token }} + TERM_WALL: ${{ vars.TERM_WALL }} run: bash "$GITHUB_ACTION_PATH/term-wall.sh" diff --git a/.github/actions/term-wall/term-wall.sh b/.github/actions/term-wall/term-wall.sh index 12b7522..b693efa 100755 --- a/.github/actions/term-wall/term-wall.sh +++ b/.github/actions/term-wall/term-wall.sh @@ -1,44 +1,76 @@ #!/usr/bin/env bash # term-wall.sh — names this organisation does not use, refused everywhere. # -# The wall scans a change for names that must not appear here: not -# affirmed, not negated, not cited. It never spells the names it refuses -# — the pattern below would otherwise be its own first hit — and it masks -# every hit it prints, so the log does not carry what the tree may not. +# The pattern is configuration, never tree content: it is read only from +# the TERM_WALL environment variable (in CI the composite action takes it +# from the org-level Actions variable vars.TERM_WALL). An unset or empty +# pattern is a refusal, exit 2 — the wall never passes vacuously. Every +# hit it prints is masked, so the log does not carry what the tree may not. # # Surfaces, in order: # 1. tracked file content (case-insensitive, binaries skipped) # 2. tracked file paths -# 3. the commit messages of the change (PR: base...head; push: before...after) -# 4. the pull request title and body -# 5. the branch name +# 3. the commit messages of the change — pull_request: base..head; +# push: before..head, or only the head commit when before is all +# zeros — read from git, fetching what the checkout lacks; never +# from an API: the action needs no token and declares none +# 4. the pull request title and body (from the event payload) +# 5. the branch name (GITHUB_HEAD_REF for a PR, GITHUB_REF_NAME for a push) # -# Exit 0 clean. Exit 1 on any hit. A surface that could not be read is a -# hit too — "could not look" is never "found nothing". +# Exit 0 clean, one summary line on stdout. Exit 1 on any hit, every hit +# on stdout as ": : "; a surface that could not be read (fetch failed, +# payload unreadable) is itself a hit — could-not-look is never a pass. +# Exit 2 refusal: stdout empty, one line on stderr shaped +# "class: expected …; found …; needed …". # -# Outside GitHub Actions (no GITHUB_EVENT_PATH) only surfaces 1 and 2 run, -# against the current directory; that is the self-test's mode. +# Outside GitHub Actions (no GITHUB_EVENT_PATH) only surfaces 1 and 2 +# run, against the current directory. set -uo pipefail -pat='s[c]ient[ _-]?db|u[s]cient' +refuse() { printf '%s\n' "$1" >&2; exit 2; } + +pat=${TERM_WALL:-} +[[ -n $pat ]] || refuse 'pattern: expected TERM_WALL set; found empty; needed the org variable (CI) or ops/bin/term-wall.conf (local)' + +# A pattern grep or sed cannot use would make every scan silently vacuous +# and could leak raw text past the mask — refuse it up front. +printf '' | grep -i -E -- "$pat" >/dev/null 2>&1 +[[ $? -le 1 ]] || refuse 'pattern: expected TERM_WALL to be an extended regular expression grep accepts; found one it rejects; needed a working pattern in the org variable (CI) or ops/bin/term-wall.conf (local)' +printf '' | sed -E "s/($pat)/[forbidden name]/Ig" >/dev/null 2>&1 \ + || refuse 'pattern: expected TERM_WALL usable in a sed substitution; found one sed rejects (an unescaped "/"?); needed a working pattern in the org variable (CI) or ops/bin/term-wall.conf (local)' + +git rev-parse --is-inside-work-tree >/dev/null 2>&1 \ + || refuse 'worktree: expected to run inside a git work tree; found none; needed a checkout (CI) or a repository directory (local)' + rc=0 mask() { sed -E "s/($pat)/[forbidden name]/Ig"; } -hit() { printf '::error::term wall: %s\n' "$1"; rc=1; } -scan() { # scan — text as an argument, never a pipe: a hit must - # set rc in this shell, and a pipeline's stages run in subshells. - local surface=$1 text=$2 found - found=$(printf '%s\n' "$text" | grep -i -n -E "$pat" 2>/dev/null | mask | head -40 || true) - if [[ -n $found ]]; then - printf '%s\n' "$found" | sed "s/^/ $surface: /" - hit "forbidden name in $surface" - fi + +emit() { # emit — one hit. Called only + # from the main shell, never from a pipeline stage, so the exit + # code it sets survives. + printf '%s: %s: %s\n' "$1" "$2" "$3" + rc=1 +} + +scan() { # scan — text as an argument, never a pipe: a hit + # must set rc in this shell, and a pipeline's stages run in subshells. + local surface=$1 text=$2 found line + [[ -n $text ]] || return 0 + found=$(printf '%s\n' "$text" | grep -i -n -E -- "$pat" 2>/dev/null | mask || true) + [[ -n $found ]] || return 0 + while IFS= read -r line; do + emit "$surface" "${line%%:*}" "${line#*:}" + done <<< "$found" } # 1. tracked content -content=$(git ls-files -z 2>/dev/null | xargs -0 -r grep -I -i -n -E "$pat" -- 2>/dev/null || true) +content=$(git ls-files -z 2>/dev/null | xargs -0 -r grep -I -H -i -n -E -- "$pat" 2>/dev/null | mask || true) if [[ -n $content ]]; then - printf '%s\n' "$content" | mask | head -40 | sed 's/^/ content: /' - hit "forbidden name in tracked content" + while IFS= read -r line; do + file=${line%%:*}; rest=${line#*:} + emit "content" "$file:${rest%%:*}" "${rest#*:}" + done <<< "$content" fi # 2. tracked paths @@ -47,37 +79,68 @@ scan "path" "$(git ls-files 2>/dev/null)" # 3-5. the change itself, when running under Actions if [[ -n ${GITHUB_EVENT_PATH:-} && -f ${GITHUB_EVENT_PATH:-} ]]; then event=${GITHUB_EVENT_NAME:-} - repo=${GITHUB_REPOSITORY:?GITHUB_REPOSITORY unset} + base="" head="" want_messages=0 case $event in pull_request|pull_request_target) - base=$(jq -r '.pull_request.base.sha' "$GITHUB_EVENT_PATH") - head=$(jq -r '.pull_request.head.sha' "$GITHUB_EVENT_PATH") - scan "pull request title/body" "$(jq -r '.pull_request.title, (.pull_request.body // "")' "$GITHUB_EVENT_PATH")" + base=$(jq -r '.pull_request.base.sha // empty' "$GITHUB_EVENT_PATH" 2>/dev/null) || base="" + head=$(jq -r '.pull_request.head.sha // empty' "$GITHUB_EVENT_PATH" 2>/dev/null) || head="" + if title_body=$(jq -r '.pull_request.title, (.pull_request.body // "")' "$GITHUB_EVENT_PATH" 2>/dev/null); then + scan "pull request title/body" "$title_body" + else + emit "pull request title/body" "$GITHUB_EVENT_PATH" "could not read the event payload (could-not-look is never a pass)" + fi scan "branch name" "${GITHUB_HEAD_REF:-}" + want_messages=1 ;; push) - base=$(jq -r '.before' "$GITHUB_EVENT_PATH") - head=$(jq -r '.after' "$GITHUB_EVENT_PATH") + base=$(jq -r '.before // empty' "$GITHUB_EVENT_PATH" 2>/dev/null) || base="" + head=$(jq -r '.after // empty' "$GITHUB_EVENT_PATH" 2>/dev/null) || head="" scan "branch name" "${GITHUB_REF_NAME:-}" + want_messages=1 ;; - *) - base=""; head="";; esac - if [[ -n $head ]]; then - if [[ -z $base || $base =~ ^0+$ ]]; then - msgs=$(git log -1 --format=%B "$head" 2>/dev/null) || msgs="" - [[ -n $msgs ]] || hit "could not read the commit message of $head (UNREACHABLE is not a pass)" + if [[ $want_messages -eq 1 ]]; then + only_head=0 + [[ $event == push && ( -z $base || $base =~ ^0+$ ) ]] && only_head=1 + if [[ -z $head || ( $only_head -eq 0 && -z $base ) ]]; then + emit "commit messages" "$event" "could not resolve the change's commits from the event payload (could-not-look is never a pass)" else - if ! msgs=$(gh api "repos/$repo/compare/$base...$head?per_page=250" --jq '.commits[].commit.message' 2>/tmp/term-wall.api.err); then - hit "could not read commit messages $base...$head via the API — $(head -c 200 /tmp/term-wall.api.err) (UNREACHABLE is not a pass)" - msgs="" + refs=("$head"); [[ $only_head -eq 0 ]] && refs=("$base" "$head") + # Fetch what the checkout lacks; a shallow clone would walk a + # truncated history without erroring, so unshallow it. + shallow=$(git rev-parse --is-shallow-repository 2>/dev/null) || shallow=false + missing=0 + for ref in "${refs[@]}"; do + git cat-file -e "$ref^{commit}" 2>/dev/null || missing=1 + done + fetch_failed=0 + if [[ $shallow == true || $missing -eq 1 ]]; then + fetch_opts=(--no-tags --quiet) + [[ $shallow == true ]] && fetch_opts+=(--unshallow) + if ! fetch_err=$(git fetch "${fetch_opts[@]}" origin "${refs[@]}" 2>&1); then + emit "commit messages" "${refs[*]}" "could not fetch the change from origin — $(printf '%s' "$fetch_err" | mask | tr '\n' ' ' | head -c 300) (could-not-look is never a pass)" + fetch_failed=1 + fi + fi + if [[ $fetch_failed -eq 0 ]]; then + if [[ $only_head -eq 1 ]]; then + range=$head + msgs=$(git log -1 --format=%B "$head" 2>/dev/null); log_rc=$? + else + range="$base..$head" + msgs=$(git log --format=%B "$base..$head" 2>/dev/null); log_rc=$? + fi + if [[ $log_rc -eq 0 ]]; then + scan "commit messages" "$msgs" + else + emit "commit messages" "$range" "could not read the commit messages from git (could-not-look is never a pass)" + fi fi fi - scan "commit messages" "$msgs" fi fi if [[ $rc -eq 0 ]]; then - echo "term wall: clean — $(git ls-files 2>/dev/null | wc -l) tracked files, the change's messages, title, body and branch name carry no forbidden name" + echo "term wall: clean — $(git ls-files 2>/dev/null | wc -l) tracked files, their paths, and the change's messages, title, body and branch name carry no forbidden name" fi exit "$rc" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b89eb6f..73bc8d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,16 +14,25 @@ jobs: persist-credentials: false - name: term wall uses: ./.github/actions/term-wall - - name: term wall fires on a planted fault, stays quiet on a clean neighbour + - name: term wall self-test — fires on a planted fault, stays quiet on a clean neighbour, refuses an empty pattern + env: + TERM_WALL: ${{ vars.TERM_WALL }} + TERM_WALL_PLANT: ${{ vars.TERM_WALL_PLANT }} run: | set -euo pipefail wall="$GITHUB_WORKSPACE/.github/actions/term-wall/term-wall.sh" + [ -n "$TERM_WALL_PLANT" ] || { echo "::error::TERM_WALL_PLANT is unset — the self-test cannot plant a fault"; exit 1; } t=$(mktemp -d); cd "$t"; git init -q; git config user.email ci@ci.invalid; git config user.name ci printf 'clean\n' > c.txt; git add c.txt; git commit -qm clean env -u GITHUB_EVENT_PATH bash "$wall" - printf 'x \x53\x63\x69\x65\x6e\x74\x44\x42 y\n' > p.txt; git add p.txt - if env -u GITHUB_EVENT_PATH bash "$wall"; then echo "::error::the wall stayed quiet on a planted fault"; exit 1; fi - echo "the wall fires on a planted fault and stays quiet on a clean neighbour" + printf 'x %s y\n' "$TERM_WALL_PLANT" > p.txt; git add p.txt + rc=0; out=$(env -u GITHUB_EVENT_PATH bash "$wall") || rc=$? + [ "$rc" -eq 1 ] || { echo "::error::the wall did not exit 1 on a planted fault (exit $rc)"; exit 1; } + case $out in *"$TERM_WALL_PLANT"*) echo "::error::the wall printed the raw plant"; exit 1;; esac + case $out in *"[forbidden name]"*) ;; *) echo "::error::the hit was not masked as [forbidden name]"; exit 1;; esac + rc=0; out=$(env -u GITHUB_EVENT_PATH TERM_WALL= bash "$wall" 2>/dev/null) || rc=$? + { [ "$rc" -eq 2 ] && [ -z "$out" ]; } || { echo "::error::an empty TERM_WALL did not refuse with exit 2 and empty stdout (exit $rc)"; exit 1; } + echo "the wall fires on a planted fault, stays quiet on a clean neighbour, and refuses an empty pattern" - name: JSON validity run: find . -name '*.json' -not -path './.git/*' -print0 | xargs -0 -r -n1 jq empty - name: YAML validity From a3142f3db8f8391aaa94755b911bce487986f36e Mon Sep 17 00:00:00 2001 From: xormania <127287135+xormania@users.noreply.github.com> Date: Tue, 1 Sep 2026 18:53:37 -0400 Subject: [PATCH 5/6] repo: pin how CI hands the term wall its pattern A composite action cannot read vars (GitHub refuses the template: 'Unrecognized named-value: vars', PR#8 run 33568333825), so the calling step passes env TERM_WALL from vars.TERM_WALL and the action refuses when it did not. Same for the self-test's planted fault. Source: original Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012Jj94rkp3tfHAxUkTCthgY --- .github/actions/term-wall/CONTRACT.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/actions/term-wall/CONTRACT.md b/.github/actions/term-wall/CONTRACT.md index 9bf3f69..a81af96 100644 --- a/.github/actions/term-wall/CONTRACT.md +++ b/.github/actions/term-wall/CONTRACT.md @@ -15,8 +15,10 @@ encoded form. ## The pattern is configuration, never tree content - The pattern is an extended, case-insensitive regular expression read - from the environment variable `TERM_WALL`. In CI the action takes it - from the org-level Actions variable `vars.TERM_WALL`. Locally the + from the environment variable `TERM_WALL`. In CI the calling step + passes it: `env: TERM_WALL: ${{ vars.TERM_WALL }}` — a composite + action cannot read `vars` itself, so the action declares no default + and refuses when the step did not pass one. Locally the Python check reads `TERM_WALL`, falling back to the gitignored file `/ops/bin/term-wall.conf` (one line: the pattern). - No tracked file may contain the pattern, a piece of it, or any @@ -78,7 +80,7 @@ colon. The raw matched text never appears in any output. ## Self-test (the `.github` repo's own ci) -With `TERM_WALL_PLANT` as the planted fault: a planted file fires +With the calling step passing `env: TERM_WALL_PLANT: ${{ vars.TERM_WALL_PLANT }}` as the planted fault: a planted file fires (exit 1, masked hit), a clean neighbour stays quiet (exit 0), and an empty `TERM_WALL` refuses (exit 2). From 6175b67bd0df4710dd0bb8b79df413b452265c6c Mon Sep 17 00:00:00 2001 From: Apply Push Bridge Date: Tue, 1 Sep 2026 19:02:36 -0400 Subject: [PATCH 6/6] repo: bring the term wall onto the pinned contract The pinned wire: one summary line on a clean run, one `: : ` per hit with the location never carrying a colon, four refusal classes. The pattern now arrives from the calling step (`env: TERM_WALL: ${{ vars.TERM_WALL }}`) because the runner refuses `vars` inside a composite action; the action refuses when the step passed nothing. ci.yml passes it. Source: original Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012Jj94rkp3tfHAxUkTCthgY Apply-Push-Job: 20260901T230235Z-apply-push-4be45d Patch-SHA256: c8606262ab7780275c529792f1882bd9c94478fff501ad995d318c286ef7eeb7 --- .github/actions/term-wall/action.yml | 10 +-- .github/actions/term-wall/term-wall.sh | 93 +++++++++++++++++--------- .github/workflows/ci.yml | 4 ++ 3 files changed, 71 insertions(+), 36 deletions(-) diff --git a/.github/actions/term-wall/action.yml b/.github/actions/term-wall/action.yml index 796452e..fbdd7b1 100644 --- a/.github/actions/term-wall/action.yml +++ b/.github/actions/term-wall/action.yml @@ -4,14 +4,14 @@ description: >- file paths, in the change's commit messages (read from git, never from an API: no token needed or declared), in the pull request title and body, and in the branch name. The pattern comes only from the - org-level Actions variable TERM_WALL; an empty pattern is a refusal, - never a pass. The wall never spells what it refuses and masks every - hit it prints. + TERM_WALL environment variable the calling step passes from the + org-level Actions variable — a composite action cannot read vars + itself, so this action declares no default; an unset or empty + pattern is a refusal, never a pass. The wall never spells what it + refuses and masks every hit it prints. runs: using: composite steps: - name: term wall shell: bash - env: - TERM_WALL: ${{ vars.TERM_WALL }} run: bash "$GITHUB_ACTION_PATH/term-wall.sh" diff --git a/.github/actions/term-wall/term-wall.sh b/.github/actions/term-wall/term-wall.sh index b693efa..f7b123f 100755 --- a/.github/actions/term-wall/term-wall.sh +++ b/.github/actions/term-wall/term-wall.sh @@ -2,10 +2,12 @@ # term-wall.sh — names this organisation does not use, refused everywhere. # # The pattern is configuration, never tree content: it is read only from -# the TERM_WALL environment variable (in CI the composite action takes it -# from the org-level Actions variable vars.TERM_WALL). An unset or empty -# pattern is a refusal, exit 2 — the wall never passes vacuously. Every -# hit it prints is masked, so the log does not carry what the tree may not. +# the TERM_WALL environment variable (in CI the calling step passes it, +# `env: TERM_WALL: ${{ vars.TERM_WALL }}` — a composite action cannot +# read `vars` itself, so the action declares no default). An unset or +# empty pattern is a refusal, exit 2 — the wall never passes vacuously. +# Every hit it prints is masked, so the log does not carry what the tree +# may not. # # Surfaces, in order: # 1. tracked file content (case-insensitive, binaries skipped) @@ -19,10 +21,10 @@ # # Exit 0 clean, one summary line on stdout. Exit 1 on any hit, every hit # on stdout as ": : "; a surface that could not be read (fetch failed, -# payload unreadable) is itself a hit — could-not-look is never a pass. -# Exit 2 refusal: stdout empty, one line on stderr shaped -# "class: expected …; found …; needed …". +# [forbidden name]>" — the location never contains a colon; a surface +# that could not be read (fetch failed, payload unreadable) is itself a +# hit — could-not-look is never a pass. Exit 2 refusal: stdout empty, +# one line on stderr shaped "class: expected …; found …; needed …". # # Outside GitHub Actions (no GITHUB_EVENT_PATH) only surfaces 1 and 2 # run, against the current directory. @@ -41,7 +43,7 @@ printf '' | sed -E "s/($pat)/[forbidden name]/Ig" >/dev/null 2>&1 \ || refuse 'pattern: expected TERM_WALL usable in a sed substitution; found one sed rejects (an unescaped "/"?); needed a working pattern in the org variable (CI) or ops/bin/term-wall.conf (local)' git rev-parse --is-inside-work-tree >/dev/null 2>&1 \ - || refuse 'worktree: expected to run inside a git work tree; found none; needed a checkout (CI) or a repository directory (local)' + || refuse 'git work tree: expected to run inside a git work tree; found none; needed a checkout (CI) or a repository directory (local)' rc=0 mask() { sed -E "s/($pat)/[forbidden name]/Ig"; } @@ -53,50 +55,72 @@ emit() { # emit — one hit. Called only rc=1 } -scan() { # scan — text as an argument, never a pipe: a hit - # must set rc in this shell, and a pipeline's stages run in subshells. - local surface=$1 text=$2 found line +scan() { # scan — text as an argument, + # never a pipe: a hit must set rc in this shell, and a pipeline's + # stages run in subshells. Each hit's location is + # "line ". + local surface=$1 prefix=$2 text=$3 found line [[ -n $text ]] || return 0 found=$(printf '%s\n' "$text" | grep -i -n -E -- "$pat" 2>/dev/null | mask || true) [[ -n $found ]] || return 0 while IFS= read -r line; do - emit "$surface" "${line%%:*}" "${line#*:}" + emit "$surface" "${prefix}line ${line%%:*}" "${line#*:}" done <<< "$found" } +scan_name() { # scan_name — for surfaces whose location + # is the (masked) value itself: a path, a branch name. + local surface=$1 value=$2 masked + [[ -n $value ]] || return 0 + printf '%s\n' "$value" | grep -i -E -- "$pat" >/dev/null 2>&1 || return 0 + masked=$(printf '%s\n' "$value" | mask) + emit "$surface" "$masked" "$masked" +} + # 1. tracked content content=$(git ls-files -z 2>/dev/null | xargs -0 -r grep -I -H -i -n -E -- "$pat" 2>/dev/null | mask || true) if [[ -n $content ]]; then while IFS= read -r line; do file=${line%%:*}; rest=${line#*:} - emit "content" "$file:${rest%%:*}" "${rest#*:}" + emit "content" "$file line ${rest%%:*}" "${rest#*:}" done <<< "$content" fi # 2. tracked paths -scan "path" "$(git ls-files 2>/dev/null)" +paths=$(git ls-files 2>/dev/null | grep -i -E -- "$pat" 2>/dev/null | mask || true) +if [[ -n $paths ]]; then + while IFS= read -r line; do + emit "path" "$line" "$line" + done <<< "$paths" +fi # 3-5. the change itself, when running under Actions if [[ -n ${GITHUB_EVENT_PATH:-} && -f ${GITHUB_EVENT_PATH:-} ]]; then event=${GITHUB_EVENT_NAME:-} + payload_ok=1 + jq empty "$GITHUB_EVENT_PATH" >/dev/null 2>&1 || payload_ok=0 + if [[ $payload_ok -eq 0 ]]; then + emit "event payload" "$GITHUB_EVENT_PATH" "could not read the event payload (could-not-look is never a pass)" + fi base="" head="" want_messages=0 case $event in pull_request|pull_request_target) - base=$(jq -r '.pull_request.base.sha // empty' "$GITHUB_EVENT_PATH" 2>/dev/null) || base="" - head=$(jq -r '.pull_request.head.sha // empty' "$GITHUB_EVENT_PATH" 2>/dev/null) || head="" - if title_body=$(jq -r '.pull_request.title, (.pull_request.body // "")' "$GITHUB_EVENT_PATH" 2>/dev/null); then - scan "pull request title/body" "$title_body" - else - emit "pull request title/body" "$GITHUB_EVENT_PATH" "could not read the event payload (could-not-look is never a pass)" + if [[ $payload_ok -eq 1 ]]; then + base=$(jq -r '.pull_request.base.sha // empty' "$GITHUB_EVENT_PATH" 2>/dev/null) || base="" + head=$(jq -r '.pull_request.head.sha // empty' "$GITHUB_EVENT_PATH" 2>/dev/null) || head="" + scan "pull request title" "" "$(jq -r '.pull_request.title // ""' "$GITHUB_EVENT_PATH" 2>/dev/null)" + scan "pull request body" "" "$(jq -r '.pull_request.body // ""' "$GITHUB_EVENT_PATH" 2>/dev/null)" + want_messages=1 fi - scan "branch name" "${GITHUB_HEAD_REF:-}" - want_messages=1 + scan_name "branch name" "${GITHUB_HEAD_REF:-}" ;; push) - base=$(jq -r '.before // empty' "$GITHUB_EVENT_PATH" 2>/dev/null) || base="" - head=$(jq -r '.after // empty' "$GITHUB_EVENT_PATH" 2>/dev/null) || head="" - scan "branch name" "${GITHUB_REF_NAME:-}" - want_messages=1 + if [[ $payload_ok -eq 1 ]]; then + base=$(jq -r '.before // empty' "$GITHUB_EVENT_PATH" 2>/dev/null) || base="" + head=$(jq -r '.after // empty' "$GITHUB_EVENT_PATH" 2>/dev/null) || head="" + want_messages=1 + fi + scan_name "branch name" "${GITHUB_REF_NAME:-}" ;; esac if [[ $want_messages -eq 1 ]]; then @@ -125,13 +149,20 @@ if [[ -n ${GITHUB_EVENT_PATH:-} && -f ${GITHUB_EVENT_PATH:-} ]]; then if [[ $fetch_failed -eq 0 ]]; then if [[ $only_head -eq 1 ]]; then range=$head - msgs=$(git log -1 --format=%B "$head" 2>/dev/null); log_rc=$? + shas=$(git rev-list -n 1 "$head" 2>/dev/null); list_rc=$? else range="$base..$head" - msgs=$(git log --format=%B "$base..$head" 2>/dev/null); log_rc=$? + shas=$(git rev-list "$base..$head" 2>/dev/null); list_rc=$? fi - if [[ $log_rc -eq 0 ]]; then - scan "commit messages" "$msgs" + if [[ $list_rc -eq 0 ]]; then + while IFS= read -r sha; do + [[ -n $sha ]] || continue + if msg=$(git log -1 --format=%B "$sha" 2>/dev/null); then + scan "commit messages" "${sha:0:12} " "$msg" + else + emit "commit messages" "${sha:0:12}" "could not read the commit message from git (could-not-look is never a pass)" + fi + done <<< "$shas" else emit "commit messages" "$range" "could not read the commit messages from git (could-not-look is never a pass)" fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73bc8d9..e66a322 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,10 @@ jobs: persist-credentials: false - name: term wall uses: ./.github/actions/term-wall + env: + TERM_WALL: ${{ vars.TERM_WALL }} + - name: term wall tests + run: python3 -m unittest discover -s .github/actions/term-wall/tests - name: term wall self-test — fires on a planted fault, stays quiet on a clean neighbour, refuses an empty pattern env: TERM_WALL: ${{ vars.TERM_WALL }}