From a4844eb77fdcb9541e5c0a2cbfcaed27742e5dce Mon Sep 17 00:00:00 2001 From: Joe Petrucelli Date: Sun, 9 Aug 2026 19:24:02 -0400 Subject: [PATCH 1/3] feat(leak-scan): reusable workflow, allowlist contract, and docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One 'uses:' line is all a caller needs — no pattern list, no copy of the script, and no setting naming the calling repo. The allowlist requires rule, path, match and a reason of at least 20 characters, enforced by exiting non-zero on a malformed file rather than warning. An entry suppresses a finding only where rule, path glob and matched text all line up, so it cannot widen into a blanket exemption. fetch-depth uses the quoted '0'/'1' form: Actions treats the number 0 as falsy, so `inputs.history && 0 || 1` would always evaluate to 1 and history mode would silently scan a shallow clone. --- .github/workflows/leak-scan.yml | 56 +++++++++++ README.md | 95 +++++++++++++++++- scripts/__pycache__/leak_scan.cpython-312.pyc | Bin 0 -> 15807 bytes ...est_leak_scan.cpython-312-pytest-9.1.1.pyc | Bin 0 -> 14839 bytes scripts/test_leak_scan.py | 55 +++++++++- 5 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/leak-scan.yml create mode 100644 scripts/__pycache__/leak_scan.cpython-312.pyc create mode 100644 scripts/__pycache__/test_leak_scan.cpython-312-pytest-9.1.1.pyc diff --git a/.github/workflows/leak-scan.yml b/.github/workflows/leak-scan.yml new file mode 100644 index 0000000..dd0a504 --- /dev/null +++ b/.github/workflows/leak-scan.yml @@ -0,0 +1,56 @@ +# Reusable leak scan. One source of truth for every public repo in this org. +# +# Callers add a four-line workflow and nothing else — no pattern list, no copy of +# the script, and in particular no setting that names the calling repo. Twelve +# vendored copies would drift, and the repo whose copy drifted would be the repo +# that stopped being checked. +# +# Call it with: +# jobs: +# leak-scan: +# uses: Back-Road-Creative/.github/.github/workflows/leak-scan.yml@main +name: leak-scan + +on: + workflow_call: + inputs: + history: + description: >- + Also scan every blob reachable from every ref. Off for the pull-request + gate: history cannot be changed by a pull request, so a finding there + would be permanently red and the gate would get switched off. Turn it on + for a scheduled audit. + type: boolean + default: false + +permissions: + contents: read + +jobs: + scan: + runs-on: ubuntu-latest + steps: + - name: Check out the repository being scanned + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # History mode needs the objects; the worktree gate does not. + fetch-depth: ${{ inputs.history && '0' || '1' }} + + - name: Check out the scanner + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: Back-Road-Creative/.github + # Untracked in the scanned repo, so `git ls-files` never sees it and the + # scanner cannot report its own pattern list as a finding. + path: .leak-scan-tool + + - name: Scan + env: + # Only used to raise the API rate limit. The org's public-repo listing is + # a public endpoint, so no extra scope is required. + GH_TOKEN: ${{ github.token }} + run: | + python3 .leak-scan-tool/scripts/leak_scan.py \ + --repo-root . \ + --self-name '${{ github.repository }}' \ + ${{ inputs.history && '--history' || '' }} diff --git a/README.md b/README.md index 316fc50..cb2da9c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,95 @@ # .github -Organization profile README + +Organization profile README, and the shared CI this org's public repositories call. + +## leak-scan + +`scripts/leak_scan.py` checks that a public repository publishes its own work and +nothing else: no sibling project's name, no client's name, no layout convention +from the private monorepo several of these repos were extracted from, and no +absolute path from an authoring machine. + +It lives here once and is called by every repo, rather than vendored into each. +Twelve copies would drift, and the repo whose copy drifted would be the repo that +quietly stopped being checked. + +### Calling it + +Add this and nothing else. There is no pattern list to copy and no setting that +names the calling repo: + +```yaml +name: leak-scan +on: [pull_request, push] +jobs: + leak-scan: + uses: Back-Road-Creative/.github/.github/workflows/leak-scan.yml@main +``` + +Pass `with: {history: true}` for an audit run that also reads every blob in +history. The pull-request gate deliberately does not: only a force-push removes a +published blob, so a history finding would be permanently red on something no pull +request can fix, and a permanently red gate gets switched off. + +### Two things it will never do + +**Flag a repo for naming itself.** The name comes from `${{ github.repository }}`, +never from configuration, so there is no per-repo setting to get wrong and no repo +left holding a stale one. The lookahead ends the name rather than using `\b`: a +word boundary sits between `driftless` and the `-` of `driftless-archive`, so an +earlier version of this rule could not see the one private sibling it existed to +catch. A trailing `.git` passes, because a clone URL carries it and still names the +repo itself. + +**Flag a public sibling.** Naming a repository anyone can already open discloses +nothing. The public set is read from the org API at scan time rather than listed in +the script, because a hand-kept list goes stale the day a repo is published and +then fails every scan that mentions it. If that lookup fails the scan stops rather +than guess — mistaking a private repo for a public one is the error that matters. + +### What it looks for + +| Rule | Catches | +| --- | --- | +| `foreign-repo` | A repo in this org that is neither this one nor public. | +| `client-name` | A third-party client engagement. | +| `machine-path` | `/home/joe`, `/home/dev` — the two authoring accounts. | +| `private-project` | A project that exists only in private repos. | +| `workspace-convention` | `_active/`, `.data/`, `baton`. | + +Every pattern was validated against fresh clones of all thirteen public repos +before it was kept. Candidates that fired on legitimate content were dropped +rather than tightened: `GMS` (a test fixture name, and the industry term +"grant-management-software"), `gomoveshift` (a public brand), `workspaces` +(ordinary English), `openclaw` (a third-party product these tools support), and a +general `/home//` (documentation examples). An over-eager scanner gets +switched off, and a switched-off scanner protects nothing. + +### The allowlist + +A repo with a genuine exception adds `.github/leak-scan-allowlist.json`: + +```json +{ + "allow": [ + { + "rule": "workspace-convention", + "path": "CHANGELOG.md", + "match": ".data/plans", + "reason": "Records that this build plan stayed in the monorepo at extraction; names no file that ships here.", + "scope": "both" + } + ] +} +``` + +`rule`, `path`, `match` and `reason` are all required, and a `reason` under 20 +characters is rejected. This is enforced, not advised: the scan exits non-zero on a +malformed allowlist, so an unexplained exemption cannot be merged. An entry +suppresses a finding only where all three of rule, path glob and matched substring +line up, so it cannot silently widen. `scope` is `worktree`, `history` or `both` +(default). + +An exemption whose reason has been falsified by later code is the failure mode +here, and nothing rechecks a reason automatically. Re-derive each entry when you +touch the file it covers. diff --git a/scripts/__pycache__/leak_scan.cpython-312.pyc b/scripts/__pycache__/leak_scan.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c4f1d9ac991a55eda5812792a7a232c58043a6b9 GIT binary patch literal 15807 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 literal 0 HcmV?d00001 diff --git a/scripts/__pycache__/test_leak_scan.cpython-312-pytest-9.1.1.pyc b/scripts/__pycache__/test_leak_scan.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4398a2660e829e2e501540a51eded5209250705b GIT binary patch literal 14839 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 literal 0 HcmV?d00001 diff --git a/scripts/test_leak_scan.py b/scripts/test_leak_scan.py index c7cb9fc..36cef64 100644 --- a/scripts/test_leak_scan.py +++ b/scripts/test_leak_scan.py @@ -4,10 +4,19 @@ from __future__ import annotations +import json +from dataclasses import replace + import pytest -from leak_scan import build_rules +from leak_scan import Finding, build_rules, is_allowed, load_allowlist ORG = "Back-Road-Creative" +FULL_ENTRY = { + "rule": "workspace-convention", + "path": "CHANGELOG.md", + "match": ".data/plans", + "reason": "Records that this plan stayed in the monorepo at extraction; ships no file.", +} def hits(text: str, self_name: str = "driftless") -> set[str]: @@ -55,3 +64,47 @@ def test_self_name_accepts_the_owner_slash_name_form() -> None: """The workflow feeds ${{ github.repository }}. Misread that and every repo reports itself.""" assert not hits(f"{ORG}/headlessmode", self_name=f"{ORG}/headlessmode") + + +@pytest.mark.parametrize( + "entry", + [ + {"rule": "client-name", "path": "a.md", "match": "x"}, # no reason + {"rule": "client-name", "path": "a.md", "match": "x", "reason": "legacy"}, # too short + {"rule": "client-name", "path": "a.md", "reason": "a" * 30}, # no match string + {"path": "a.md", "match": "x", "reason": "a" * 30}, # no rule + ], +) +def test_allowlist_entry_without_a_real_reason_is_fatal(tmp_path, entry) -> None: + """An unexplained exemption is how a guard quietly stops guarding, so this is a + hard error rather than a warning — the entry cannot be merged.""" + f = tmp_path / "allow.json" + f.write_text(json.dumps({"allow": [entry]})) + with pytest.raises(SystemExit): + load_allowlist(f) + + +def test_allowlist_accepts_a_fully_explained_entry(tmp_path) -> None: + f = tmp_path / "allow.json" + f.write_text(json.dumps({"allow": [FULL_ENTRY]})) + assert load_allowlist(f) == [FULL_ENTRY] + + +def test_absent_allowlist_is_empty_not_an_error(tmp_path) -> None: + assert load_allowlist(tmp_path / "nope.json") == [] + + +def test_entry_suppresses_only_its_own_rule_path_and_text() -> None: + """All three must line up, so an entry cannot silently widen into a blanket.""" + f = Finding("workspace-convention", "worktree", "CHANGELOG.md", 9, "see .data/plans/x.md") + assert is_allowed(f, [FULL_ENTRY]) + assert not is_allowed(replace(f, rule="client-name"), [FULL_ENTRY]) + assert not is_allowed(replace(f, path="OTHER.md"), [FULL_ENTRY]) + assert not is_allowed(replace(f, text="see _active/x"), [FULL_ENTRY]) + + +def test_scope_limits_an_entry_to_one_side_of_the_scan() -> None: + entry = {**FULL_ENTRY, "scope": "history"} + f = Finding("workspace-convention", "worktree", "CHANGELOG.md", 9, "see .data/plans/x.md") + assert not is_allowed(f, [entry]) + assert is_allowed(replace(f, where="history"), [entry]) From f654053bf2314ccfede1fd757eb10886b39f9b2d Mon Sep 17 00:00:00 2001 From: Joe Petrucelli Date: Sun, 9 Aug 2026 19:25:25 -0400 Subject: [PATCH 2/3] ci: hold this repo to the rule it enforces elsewhere --- .github/workflows/leak-scan-self.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/workflows/leak-scan-self.yml diff --git a/.github/workflows/leak-scan-self.yml b/.github/workflows/leak-scan-self.yml new file mode 100644 index 0000000..7f0b0cd --- /dev/null +++ b/.github/workflows/leak-scan-self.yml @@ -0,0 +1,9 @@ +# This repo is public too, so it holds itself to the rule it enforces elsewhere. +name: leak-scan +on: + pull_request: + push: + branches: [main] +jobs: + leak-scan: + uses: Back-Road-Creative/.github/.github/workflows/leak-scan.yml@main From 1cf428c38e606456de76a0889b8b93d29800fedf Mon Sep 17 00:00:00 2001 From: Joe Petrucelli Date: Sun, 9 Aug 2026 19:27:30 -0400 Subject: [PATCH 3/3] ci: move this repo's own caller out of the scanner PR A caller cannot be green before the reusable workflow it calls exists on main, and a workflow-not-found run is still a red run on the PR that has to merge first. It ships with the other callers instead. --- .github/workflows/leak-scan-self.yml | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 .github/workflows/leak-scan-self.yml diff --git a/.github/workflows/leak-scan-self.yml b/.github/workflows/leak-scan-self.yml deleted file mode 100644 index 7f0b0cd..0000000 --- a/.github/workflows/leak-scan-self.yml +++ /dev/null @@ -1,9 +0,0 @@ -# This repo is public too, so it holds itself to the rule it enforces elsewhere. -name: leak-scan -on: - pull_request: - push: - branches: [main] -jobs: - leak-scan: - uses: Back-Road-Creative/.github/.github/workflows/leak-scan.yml@main