From 44e24ec31540e5449341f42cb29f7e60cf150ce3 Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Tue, 25 Aug 2026 02:52:56 -0700 Subject: [PATCH 1/9] ci(lint): fail on org repo literals not on a committed allowlist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AGENTS.md's first convention — never leak private caller names — was convention-enforced only. This makes it CI-enforced, the same move the workflow-pins lint made for the `workflows_ref` default rule. Allowlist, never denylist: a denylist grep would have to commit the private names into this public repo in order to match them, so the lint would BE the leak. `.github/lint/check-org-repo-literals.sh` instead fails on ANY org-prefixed repo literal in the tracked tree whose name is not on `.github/lint/org-repo-allowlist.txt`, where every entry carries a trailing comment saying why it is safe. Publishing a name becomes an allowlist edit that review sees. `test-org-repo-literals.yml` runs it with NO `paths:` filter — the leak surface is the whole tree, and gating it on workflow-file paths would exempt exactly the files most likely to carry one. Four smoke tests pin the CLI's exit status: an unlisted name fails, a lowercased org spelling fails (GitHub resolves owner names case-insensitively, so matching only the canonical spelling is a one-keystroke bypass), allowlisted names pass, and a bare `*` allowlist entry is a configuration error rather than a blanket clear. The allowlist is seeded from an actual scan of main rather than from a guess: real public repos, CODEOWNERS team handles, Hugging Face model namespaces, the deliberate fake-private fixtures the bump-callers and public-repo-hygiene suites commit, and the one human-ratified exception. main is clean against it as seeded — nothing had to be scrubbed. Stated limitation, also in the script header: the guard only catches org-prefixed literals. Bare names cannot be linted without a denylist that is itself the leak, so bare-name discipline stays with review and AGENTS.md. --- .github/lint/README.md | 65 ++++++ .github/lint/check-org-repo-literals.sh | 221 +++++++++++++++++++ .github/lint/org-repo-allowlist.txt | 97 ++++++++ .github/workflows/test-org-repo-literals.yml | 111 ++++++++++ AGENTS.md | 20 +- CONTRIBUTING.md | 4 + 6 files changed, 514 insertions(+), 4 deletions(-) create mode 100644 .github/lint/README.md create mode 100755 .github/lint/check-org-repo-literals.sh create mode 100644 .github/lint/org-repo-allowlist.txt create mode 100644 .github/workflows/test-org-repo-literals.yml diff --git a/.github/lint/README.md b/.github/lint/README.md new file mode 100644 index 0000000..9994c13 --- /dev/null +++ b/.github/lint/README.md @@ -0,0 +1,65 @@ +# lint — org repo literal allowlist + +`check-org-repo-literals.sh` + `org-repo-allowlist.txt` are the lint behind +[`test-org-repo-literals.yml`](../workflows/test-org-repo-literals.yml). They +make AGENTS.md's first convention — *never leak private caller names* — a CI +gate instead of something a reviewer has to remember, the same move +[`test-workflow-pins.yml`](../workflows/test-workflow-pins.yml) made for the +`workflows_ref` default rule. + +## The rule + +Every org-prefixed repo literal in this repo's **tracked** tree must have its +repo-name part on `org-repo-allowlist.txt`. Anything else fails the run, with +`file:line:match` for each hit. + +**Allowlist, never denylist.** A denylist grep would have to commit the private +names into this public repo in order to match them — the lint would *be* the +leak. Default-deny inverts that: the committed list holds publishable names +only, and publishing a new one is an allowlist edit that shows up in review. + +## Adding a name + +Add one line, lowest-friction spelling first, with a trailing `#` comment saying +why it is safe: + +``` +comfy-cli # public (also on the org-wide PUBLIC_COMFY_ORG_REPOS list) +``` + +Entries are shell globs, matched case-insensitively; a trailing `.git` and +trailing sentence punctuation are stripped from the literal before comparison. +For a **real** repo, "why it is safe" means you confirmed it is public. If it is +private, the fix is to remove the reference, not to add the line. + +## Why not `public-repo-hygiene`? + +[`.github/public-repo-hygiene/`](../public-repo-hygiene/) is the rigorous, +org-wide implementation of this same default-deny idea, and it is what every +*other* repo should adopt. This repo cannot adopt it as a caller: it is that +checker's own **home**, so its tests and docs deliberately commit fake-private +fixture names, quote internal collaboration-tool hosts, and reference ticket ids +by convention throughout — all three of that checker's categories, all +intentional. This lint is the one category this repo *can* enforce on itself +today. It is deliberately small; it is not a second copy of that checker, and +new detection belongs there rather than here. + +## Known limitations + +Run `bash check-org-repo-literals.sh --help` for the authoritative list. In +short: only **org-prefixed** literals are caught (a bare repo name cannot be +linted without a denylist that is itself the leak — that half stays with review +and AGENTS.md), and the name class is ASCII, so a name whose tail is non-ASCII +is read only as far as its ASCII prefix. The scan runs under `LC_ALL=C` so that +second limit is the same on every machine rather than a function of the runner's +locale. + +## Running it + +```bash +bash .github/lint/check-org-repo-literals.sh # scan the tracked tree +bash .github/lint/check-org-repo-literals.sh --root DIR +shellcheck -x .github/lint/check-org-repo-literals.sh +``` + +Exit `0` clean, `1` findings, `2` usage/setup error. diff --git a/.github/lint/check-org-repo-literals.sh b/.github/lint/check-org-repo-literals.sh new file mode 100755 index 0000000..521e989 --- /dev/null +++ b/.github/lint/check-org-repo-literals.sh @@ -0,0 +1,221 @@ +#!/usr/bin/env bash +# +# check-org-repo-literals.sh — allowlist lint for org-prefixed repo literals. +# +# This repo is PUBLIC, and AGENTS.md's first convention is "never leak private +# caller names". That rule was convention-enforced only. This makes it +# CI-enforced, the same move the workflow-pins lint made for the +# `workflows_ref` default rule. +# +# ALLOWLIST, NEVER DENYLIST. A denylist grep would have to commit the private +# names into this public repo to match them — the lint would BE the leak. So +# every `Comfy-Org/` literal in the tracked tree is a finding unless +# is on the committed allowlist. Adding a name is an allowlist edit, +# which review sees. +# +# KNOWN LIMITATIONS — read these before trusting a green run: +# 1. Only ORG-PREFIXED literals are caught. A bare repo name written without +# the `Comfy-Org/` prefix cannot be linted without committing a denylist +# that is itself the leak. Bare-name discipline stays with review and +# AGENTS.md. +# 2. The name class is ASCII and the scan runs in the C locale, so a name +# whose tail is non-ASCII -- a U+2010 homoglyph dash, a U+017F long s, CJK +# text -- is read only as far as its ASCII PREFIX, and a private name +# spelled that way can clear on an allowlisted prefix. Handling that needs +# the offset-aware scan `public-repo-hygiene` does; see limitation 3. +# 3. Category 3 only. `.github/public-repo-hygiene/` is the rigorous, +# org-wide implementation of this idea (it also covers ticket ids, +# internal collaboration-tool links and the homoglyph case above). This +# repo cannot adopt that caller as-is: it is that checker's own home, so +# its tests and docs are full of deliberate fake-private fixtures, and it +# references ticket ids by convention throughout. This lint is the subset +# that this repo CAN enforce on itself today. +# +# TAMPER BOUNDARY: unlike the reusable checkers this repo publishes, this lint +# runs from the PR's own checkout, so a PR here can edit both the script and the +# allowlist. That is the same boundary the workflow-pins lint has and it is the +# point -- widening the allowlist is a reviewed diff. It is NOT a control against +# a hostile committer. +# +# Usage: bash .github/lint/check-org-repo-literals.sh [--root DIR] +# [--allowlist REPO/REL/PATH] +# Exit: 0 clean, 1 findings, 2 usage/setup error. + +set -euo pipefail + +# Byte-wise, deterministic matching everywhere. `grep -i` case-folds according +# to the LOCALE, so in a UTF-8 locale it widens `[A-Za-z]` to reach U+017F and +# U+212A while in the POSIX locale it does not -- and the runner's locale is not +# something this lint should depend on. `LC_ALL=C` pins ASCII-only folding for +# both `grep` and the `tr` below, so a green run here means the same thing it +# means on CI. +export LC_ALL=C + +ORG='Comfy-Org' +# Deliberately assembled rather than written whole: a literal org-prefixed name +# anywhere in this file would be a finding against this very lint. +# +# Matched case-INSENSITIVELY (`grep -i`), because GitHub resolves owner names +# case-insensitively: `comfy-org/` reaches the same repository, so +# matching only the canonical spelling would leave a one-keystroke bypass of a +# default-deny control. The name class is already both cases, so `-i` widens +# nothing but the org segment. +PATTERN="${ORG}/[A-Za-z0-9_.-]+" + +root='.' +allowlist_rel='.github/lint/org-repo-allowlist.txt' + +while [ $# -gt 0 ]; do + case "$1" in + --root) + [ $# -ge 2 ] || { echo "error: --root needs a directory" >&2; exit 2; } + root="$2" + shift 2 + ;; + --allowlist) + [ $# -ge 2 ] || { echo "error: --allowlist needs a file" >&2; exit 2; } + allowlist_rel="$2" + shift 2 + ;; + -h|--help) + # The whole leading comment block, minus the shebang, is the help text. + sed -n '2,${/^[^#]/q;p;}' "$0" | sed 's/^# \{0,1\}//' + exit 0 + ;; + *) + echo "error: unknown argument '$1'" >&2 + exit 2 + ;; + esac +done + +[ -d "$root" ] || { echo "error: --root '$root' is not a directory" >&2; exit 2; } + +# The allowlist is resolved against the REPO being scanned when it has one, and +# otherwise against this script's own repo — that is what lets the smoke test +# point --root at a bare fixture directory and still get the real allowlist. +script_repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +if [ -f "$root/$allowlist_rel" ]; then + allowlist="$root/$allowlist_rel" +elif [ -f "$script_repo_root/$allowlist_rel" ]; then + allowlist="$script_repo_root/$allowlist_rel" +else + echo "error: allowlist '$allowlist_rel' not found under '$root' or '$script_repo_root'" >&2 + exit 2 +fi + +# Allowlist entries are shell globs, matched case-insensitively (GitHub +# resolves owner/repo names case-insensitively, so a case variant of an +# allowlisted PUBLIC name is the same repo — and no private name is on this +# list under any casing, so folding case cannot clear one). +patterns=() +pattern_count=0 +# `|| [ -n "$line" ]` so a final entry with no trailing newline is not dropped. +while IFS= read -r line || [ -n "$line" ]; do + line="${line%%#*}" # strip the trailing "why it's safe" comment + line="${line#"${line%%[![:space:]]*}"}" # ltrim + line="${line%"${line##*[![:space:]]}"}" # rtrim + [ -n "$line" ] || continue + # A bare `*` would clear every name and neuter the whole control. Reject it + # loudly rather than letting one character pass as an ordinary allowlist edit. + [ "$line" != '*' ] || { echo "error: allowlist '$allowlist' has a bare '*' entry, which allows every name" >&2; exit 2; } + patterns+=("$(printf '%s' "$line" | tr '[:upper:]' '[:lower:]')") + pattern_count=$((pattern_count + 1)) +done < "$allowlist" + +# Counted rather than `${#patterns[@]}`: bash 3.2 (still /bin/bash on macOS) +# treats an empty array as unset under `set -u`, so the guard would abort with +# `unbound variable` instead of this message. The `for` below is only reached +# with a non-empty array for the same reason. +if [ "$pattern_count" -eq 0 ]; then + echo "error: allowlist '$allowlist' has no entries" >&2 + exit 2 +fi + +# Tracked files only when there is a work tree (build output and anything +# untracked is out of scope by construction); a plain recursive grep otherwise, +# which is the path the smoke test's fixture directory takes. +hits='' +scan_status=0 +if git -C "$root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + hits="$(git -C "$root" grep -oiInE -- "$PATTERN" \ + -- . ":(exclude)$allowlist_rel")" || scan_status=$? +else + # `cd` + a `.` root rather than grepping "$root" and stripping the prefix + # afterwards: the strip would be a `sed` expression built from a path this + # script does not control. + hits="$(cd "$root" && grep -rIoiEn --exclude-dir=.git -- "$PATTERN" .)" || scan_status=$? +fi +# Both tools exit 1 for "no matches" and >1 for a real error. Swallowing the +# latter with `|| true` would report a clean tree because the scan never ran -- +# a guard that fails OPEN is worse than no guard, since the green run reads as +# coverage. +if [ "$scan_status" -gt 1 ]; then + echo "error: the scan failed (exit $scan_status) — refusing to report a clean tree" >&2 + exit 2 +fi + +# Trailing sentence punctuation the name class swallowed, then a `.git` suffix +# (`Foo.git` still references the repo `Foo`), then punctuation again -- +# `/foo.git.` needs both passes, and stripping `.git` first leaves the +# suffix stuck behind the period. A GitHub slug can end in none of these, so +# every pass can only narrow the name. +trim_trailing_punctuation() { + local value="$1" + while [ -n "$value" ]; do + case "$value" in + *.|*-|*_) value="${value%?}" ;; + *) break ;; + esac + done + printf '%s' "$value" +} + +findings=0 +while IFS= read -r hit; do + [ -n "$hit" ] || continue + # `file:line:match` — the match holds no colon, and neither does the line + # number, so both come off the RIGHT. Cutting the file off the LEFT instead + # would mangle any path that contains a colon. + match="${hit##*:}" + where="${hit%:*}" + file="${where%:*}" + file="${file#./}" + lineno="${where##*:}" + name="${match#*/}" + + lower="$(printf '%s' "$name" | tr '[:upper:]' '[:lower:]')" + lower="$(trim_trailing_punctuation "$lower")" + lower="${lower%.git}" + lower="$(trim_trailing_punctuation "$lower")" + # A reference left naming nothing (`/.`, `/.git`) is not a finding. + [ -n "$lower" ] || continue + + allowed=0 + for p in "${patterns[@]}"; do + # shellcheck disable=SC2254 # $p is intentionally a glob, not a literal + case "$lower" in + $p) allowed=1; break ;; + esac + done + + if [ "$allowed" -eq 0 ]; then + findings=$((findings + 1)) + echo "$file:$lineno: $match is not on $allowlist_rel" + echo "::error file=$file,line=$lineno::unapproved ${ORG} repo literal: $match" + fi +done <<< "$hits" + +if [ "$findings" -gt 0 ]; then + cat >&2 <`) ── +# Team handles are inherently public on a public repo: GitHub renders CODEOWNERS +# owners to anyone who can see the repo. Mirrors PUBLIC_COMFY_ORG_TEAMS. +comfy-cloud-team # team handle, org-wide PUBLIC_COMFY_ORG_TEAMS +core-engine-team # team handle, org-wide PUBLIC_COMFY_ORG_TEAMS +backend # team handle in the agents-md-integrity suite's CODEOWNERS fixture — a team slug, not a repo + +# ── Documentation examples — no such repo exists ── +github-workflows-tools # doc example in the bump-callers docs +github-workflows-actions # doc example in the bump-callers docs +example # generic doc/test placeholder +foo # generic doc/test placeholder +legit # bump-callers doc/test placeholder +x # single-letter placeholder in the public-repo-hygiene docs +notes # path-shaped example (`docs/v1.2//notes`) in the public-repo-hygiene docs + +# ── Hugging Face model/dataset namespaces, not GitHub repos ── +# Comfy-Org publishes weights under huggingface.co/Comfy-Org/, so these names +# have no GitHub existence at all. They are product content, not references. +qwen-image-edit_comfyui # public HF model, quoted in the public-repo-hygiene docs/tests +qwen-image_comfyui # public HF model, quoted in the public-repo-hygiene tests +ace_step_1.5_comfyui_files # public HF model, quoted in the public-repo-hygiene tests +stable-diffusion-v1-5-archive # public HF model, quoted in the public-repo-hygiene tests +a-model # HF-route fixture in the public-repo-hygiene tests +a-dataset # HF-route fixture in the public-repo-hygiene tests +some-model # HF-route fixture in the public-repo-hygiene tests +some-demo # HF-route fixture in the public-repo-hygiene tests +some-eval-set # HF-route fixture in the public-repo-hygiene tests +some-set-abc123 # HF-route fixture in the public-repo-hygiene tests + +# ── Test fixtures — invented names, no such repo ── +# These are the fake "private" names the bump-callers and public-repo-hygiene +# suites use to prove masking and default-deny. A NEW fixture name needs a line +# here, on purpose: that edit is where a reviewer confirms it is invented. +secret-* # bump-callers + public-repo-hygiene fixture family +secret_a.b-c # bump-callers fixture (dot/underscore name shape) +a.b.c # bump-callers fixture (dotted name shape) +comfyui2 # public-repo-hygiene fixture: invented near-miss of a real public name +some-team # public-repo-hygiene fixture team handle +not-a-public-team # public-repo-hygiene fixture team handle +not-a-public-repo # public-repo-hygiene fixture +private # public-repo-hygiene fixture +private-thing # public-repo-hygiene fixture +super-secret-repo # public-repo-hygiene fixture +some-internal-thing # public-repo-hygiene fixture +something-else # public-repo-hygiene fixture +definitely-not-public # public-repo-hygiene fixture +nope # public-repo-hygiene fixture +whatever # public-repo-hygiene fixture +bare-host-at-line-start # public-repo-hygiene model-host anchor fixture +host-case-and-port # public-repo-hygiene model-host anchor fixture +in-a-markdown-link # public-repo-hygiene model-host anchor fixture +in-angle-brackets # public-repo-hygiene model-host anchor fixture +in-double-quotes # public-repo-hygiene model-host anchor fixture +plain-http # public-repo-hygiene model-host anchor fixture +protocol-relative # public-repo-hygiene model-host anchor fixture +title-case-route # public-repo-hygiene model-host route fixture +upper-case-route # public-repo-hygiene model-host route fixture + +# ── ASCII prefixes of the public-repo-hygiene suite's homoglyph fixtures ── +# That suite deliberately commits names whose tail is non-ASCII, to prove its +# own homoglyph handling. This lint's name class is ASCII (limitation 2 in +# check-org-repo-literals.sh), so it reads only the prefix. Each of these is +# the prefix of an INVENTED fixture name, not of a real repo. +comfy-type # prefix of the `comfy-typecript-sdk` fixture +core-engine # prefix of the `core-engineteam` fixture +mod # prefix of the `model` HF-route fixture diff --git a/.github/workflows/test-org-repo-literals.yml b/.github/workflows/test-org-repo-literals.yml new file mode 100644 index 0000000..0b750c7 --- /dev/null +++ b/.github/workflows/test-org-repo-literals.yml @@ -0,0 +1,111 @@ +name: Test org repo literals + +# Regression lint (BE-8192): every org-prefixed repo literal in this public +# repo's tracked tree must be on `.github/lint/org-repo-allowlist.txt`. +# +# AGENTS.md's first convention is "never leak private caller names", and it was +# convention-enforced only. This is the same move test-workflow-pins.yml made +# for the `workflows_ref` default rule: take a rule a reviewer had to remember +# and make CI remember it instead. +# +# ALLOWLIST, NEVER DENYLIST — a denylist grep would have to commit the private +# names into this public repo to match them, so the lint would BE the leak. +# Adding a name is an allowlist edit, which review sees. +# +# NO `paths:` FILTER, deliberately. The leak surface is the whole tree (past +# leaks sat in `scripts/`, `.github/dependabot.yml` and a component README), so +# gating this on workflow-file paths would exempt exactly the files most likely +# to carry one. + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +# One lint run per ref: a push that supersedes an in-flight run should cancel +# it, not race it for a runner. +concurrency: + group: test-org-repo-literals-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: shellcheck + lint this repo + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: ShellCheck the lint script + run: shellcheck -x .github/lint/check-org-repo-literals.sh + + - name: Lint this repo's org repo literals + run: bash .github/lint/check-org-repo-literals.sh + + # Every fixture below assembles the literal with `printf` instead of + # writing it out. A literal org-prefixed name in this file would be + # scanned by the step above and would have to be allowlisted — which + # would defeat the fixture, since the point is that it is NOT allowlisted. + # (`%s/%s` is not a match: the name class cannot start with `%`.) + - name: Smoke test — an unlisted name fails the lint + run: | + # Proves the lint is wired to FAIL, not just to run: the step above + # exercises the clean path, this exercises the CLI's exit status. + dir="$(mktemp -d)" + printf 'see %s/%s for details\n' 'Comfy-Org' 'not-on-the-allowlist' > "$dir/fixture.md" + if bash .github/lint/check-org-repo-literals.sh --root "$dir"; then + echo "::error::lint passed an unallowlisted org repo literal — expected a non-zero exit" + exit 1 + fi + echo "lint correctly rejected the unallowlisted name" + + - name: Smoke test — a lowercased org spelling still fails + run: | + # GitHub resolves owner names case-insensitively, so `comfy-org/` + # reaches the same repository as the canonical spelling. Matching only + # the canonical one would be a one-keystroke bypass of a default-deny + # control (the same hole BE-8697 closed in public-repo-hygiene). + dir="$(mktemp -d)" + printf '%s/%s\n' 'comfy-org' 'Not-On-The-Allowlist' > "$dir/fixture.md" + if bash .github/lint/check-org-repo-literals.sh --root "$dir"; then + echo "::error::lint passed a lowercased org spelling — expected a non-zero exit" + exit 1 + fi + echo "lint correctly rejected the lowercased org spelling" + + - name: Smoke test — an allowlisted name passes + run: | + # The other half: a lint that always fails proves nothing either. This + # also pins the `.git` suffix and trailing-punctuation stripping, so a + # normalization regression cannot hide behind the clean-tree run above. + dir="$(mktemp -d)" + { + printf 'plain %s/%s\n' 'Comfy-Org' 'github-workflows' + printf 'clone %s/%s.\n' 'Comfy-Org' 'github-workflows.git' + printf 'glob %s/%s\n' 'Comfy-Org' 'secret-alpha' + printf 'empty %s/%s\n' 'Comfy-Org' '.git' + } > "$dir/fixture.md" + bash .github/lint/check-org-repo-literals.sh --root "$dir" + echo "lint correctly accepted the allowlisted names" + + - name: Smoke test — a bare '*' allowlist entry is rejected, not honoured + run: | + # The one edit that would silently neuter the whole control is a + # single '*' on an allowlist line. It must fail as a configuration + # error (exit 2), not clear every name. + dir="$(mktemp -d)" + mkdir -p "$dir/.github/lint" + printf '* # oops\n' > "$dir/.github/lint/org-repo-allowlist.txt" + printf '%s/%s\n' 'Comfy-Org' 'not-on-the-allowlist' > "$dir/fixture.md" + status=0 + bash .github/lint/check-org-repo-literals.sh --root "$dir" || status=$? + if [ "$status" -ne 2 ]; then + echo "::error::a bare '*' allowlist entry exited $status — expected 2 (configuration error)" + exit 1 + fi + echo "lint correctly rejected the wildcard allowlist entry" diff --git a/AGENTS.md b/AGENTS.md index bb33c6d..83c1e95 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,15 +48,19 @@ python3 -m unittest discover -s .github/workflow-pins/tests -p 'test_*.py' && py python3 -m pip install --require-hashes --only-binary=:all: -r .github/coderabbit-config/requirements.txt python3 -m unittest discover -s .github/coderabbit-config/tests -p 'test_*.py' -v +# org repo literal allowlist lint + its shellcheck (runs on EVERY change) +shellcheck -x .github/lint/check-org-repo-literals.sh && bash .github/lint/check-org-repo-literals.sh + # AGENTS.md integrity checker against any repo tree python3 .github/agents-md-integrity/check_agents_md.py --root . ``` There is no repo-wide formatter/linter config (no ruff/black/pyproject, no pre-commit). Shell is linted by `shellcheck` in CI; Python is guarded by the -`unittest` suites above. Every test workflow is **path-filtered**, so a change -that touches only the files under a given directory runs only that directory's -tests — run the matching command above for whatever you touched. +`unittest` suites above. Every test workflow is **path-filtered** EXCEPT +`test-org-repo-literals.yml` (the leak surface is the whole tree), so a change +that touches only the files under a given directory runs that directory's tests +plus the literal lint — run the matching commands above for whatever you touched. ## Layout @@ -110,6 +114,10 @@ tests — run the matching command above for whatever you touched. at every checkout. The `job.workflow_sha` self-pin is exempt from the `default:` half only — it stops the ref being MUTABLE, not being EMPTY, so it still has to carry a guard (BE-4169, corrected by BE-8077; see below). +- `.github/lint/` — `check-org-repo-literals.sh` + `org-repo-allowlist.txt`, the + repo-LOCAL lint behind `test-org-repo-literals.yml` (BE-8192): the repo-name + subset of `public-repo-hygiene`, which this repo cannot adopt as a caller + because it is that checker's HOME (its fake-private fixtures live here). - `.github/bump-callers/` — `bump-callers.sh`, the ONE fleet-agnostic script that opens SHA-bump PRs in consumer repos when a reusable workflow changes, plus `preflight.sh` (BE-6475), the ONE staleness/decommission guard that runs @@ -220,7 +228,10 @@ tests — run the matching command above for whatever you touched. unmasked in the env dump Actions emits *before* the step, so the bumper's own masking can never run early enough; a secret is masked there too. The bumper still masks each name. Keep private repo paths/detail out of workflow files, - commit messages, and PR text. + commit messages, and PR text. **CI-enforced (BE-8192)**: + `test-org-repo-literals.yml` fails any org-prefixed repo literal whose name is + not on `.github/lint/org-repo-allowlist.txt`, so publishing a name is an + allowlist edit review sees; BARE names stay with review (a denylist would leak). - **Pin everything by full commit SHA**, with a trailing `# v1` comment — both the `uses:` in callers and every third-party action here. Bare `@v1` fails the pin-validation (`pinact`, `zizmor`) that consumer CI runs. See README "Pinning". @@ -303,3 +314,4 @@ tests — run the matching command above for whatever you touched. - [`.github/agents-md-integrity/README.md`](.github/agents-md-integrity/README.md) — the checker + its knobs. - [`.github/public-repo-hygiene/README.md`](.github/public-repo-hygiene/README.md) — the leak guard, its tamper boundary + known limitations. - [`.github/bump-callers/README.md`](.github/bump-callers/README.md) — the shared bumper + its fleets. +- [`.github/lint/README.md`](.github/lint/README.md) — the org-repo-literal allowlist lint + how to add a name. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d46c328..1236781 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,6 +29,10 @@ python3 -m unittest discover -s .github/public-repo-hygiene/tests -p 'test_*.py' shellcheck -x .github/bump-callers/bump-callers.sh .github/bump-callers/tests/test_bump_callers.sh bash .github/bump-callers/tests/test_bump_callers.sh +# org repo literal allowlist lint (runs on EVERY change — no path filter) +shellcheck -x .github/lint/check-org-repo-literals.sh +bash .github/lint/check-org-repo-literals.sh + # PR-size classifier cd scripts/check-pr-size && go vet ./... && go test ./... ``` From 057f4882fa438bf69013c281b227713717d2ea16 Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Tue, 25 Aug 2026 03:35:24 -0700 Subject: [PATCH 2/9] fix(lint): close the normalization, glob and scope holes in the literal lint (BE-8192) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cursor-review panel findings on #224. Each one widened or fail-opened the default-deny control the lint exists to be: - `trim_trailing_punctuation` stripped `-` and `_` as well as `.`. Only a trailing `.` is illegal in a GitHub slug; `-`/`_` are legal last characters, so stripping them let `/-` normalize onto the allowlisted entry and clear. Now trims periods only, so normalization can only narrow. - The wildcard guard rejected the exact entry `*`, but `**`, `?*` and `[a-z]*` clear every name just as thoroughly while reading as ordinary allowlist lines. Replaced with a behavioural test: an entry that matches two unrelated probe names is a configuration error. - Team handles and repo names shared one namespace, so a slug allowlisted for a CODEOWNERS fixture also cleared a literal reference to a private REPO of that name. The optional `@` is now captured, and an `@`-prefixed allowlist entry clears only `@`-prefixed literals — the split `public-repo-hygiene` makes, including its npm/Packages crossing (a plain entry still clears either spelling, because `@/` is also a package scope). - `secret-*` pre-approved an unbounded family on a default-deny list. Enumerated instead, so a new fixture name is a reviewed line. - Two fail-open paths reported a clean tree when the scan never ran: a failed `cd` in the non-git path exited 1, which is grep's "no matches", and a `--root` inside a work tree but holding no tracked files matched zero files for the same reason. Both are configuration errors now, and the success message states the scope actually scanned rather than claiming the whole tracked tree. - Dropped the allowlist file's `:(exclude)` pathspec. Its entries are bare names that cannot match the pattern anyway, so the exclusion bought only one thing: letting its free-text rationale comments go unchecked. - Documented the `-I` binary-blob coverage hole, which a green run otherwise overstated. The smoke tests asserted "any non-zero" for the negative cases, so an exit 2 (nothing scanned) read as a correct rejection; they now assert the exact status, capture output so a successful run stops publishing `::error file=` annotations pointing at a temp file, and cover the new normalization, glob, team-namespace and scan-could-not-run cases. --- .github/lint/README.md | 46 +++++- .github/lint/check-org-repo-literals.sh | 163 +++++++++++++++---- .github/lint/org-repo-allowlist.txt | 116 +++++++++++-- .github/workflows/test-org-repo-literals.yml | 163 +++++++++++++------ 4 files changed, 378 insertions(+), 110 deletions(-) diff --git a/.github/lint/README.md b/.github/lint/README.md index 9994c13..01e4124 100644 --- a/.github/lint/README.md +++ b/.github/lint/README.md @@ -27,11 +27,28 @@ why it is safe: comfy-cli # public (also on the org-wide PUBLIC_COMFY_ORG_REPOS list) ``` -Entries are shell globs, matched case-insensitively; a trailing `.git` and -trailing sentence punctuation are stripped from the literal before comparison. -For a **real** repo, "why it is safe" means you confirmed it is public. If it is +Entries are shell globs, matched case-insensitively; a trailing `.git` suffix +and trailing periods are stripped from the literal before comparison. A trailing +`-` or `_` is **not** stripped — both are legal at the end of a real slug, so +stripping them would let `-` clear on ``. For a +**real** repo, "why it is safe" means you confirmed it is public. If it is private, the fix is to remove the reference, not to add the line. +**Team handles are a separate namespace.** An entry written with a leading `@` +clears only an `@`-prefixed literal — a CODEOWNERS team handle, or an npm / +GitHub Packages scope. A plain entry clears either spelling, because +`@/` is also how a package scope for a repo of that name is written. +Scoping team slugs this way is what stops a CODEOWNERS fixture's team name from +also clearing a literal reference to a *private repo* of the same name; the +org-wide checker splits the two lists for the same reason. + +**No entry may match an arbitrary name.** An entry that clears two unrelated +probe names is a configuration error (exit `2`), not an allowlist line: `*`, +`**`, `?*` and `[a-z]*` all read as ordinary edits while switching the whole +control off. For the same reason, prefer enumerating a test-fixture family over +globbing it — a `secret-*` line pre-approves an unbounded set of names on a +list whose entire value is that each name was reviewed once. + ## Why not `public-repo-hygiene`? [`.github/public-repo-hygiene/`](../public-repo-hygiene/) is the rigorous, @@ -47,12 +64,23 @@ new detection belongs there rather than here. ## Known limitations Run `bash check-org-repo-literals.sh --help` for the authoritative list. In -short: only **org-prefixed** literals are caught (a bare repo name cannot be -linted without a denylist that is itself the leak — that half stays with review -and AGENTS.md), and the name class is ASCII, so a name whose tail is non-ASCII -is read only as far as its ASCII prefix. The scan runs under `LC_ALL=C` so that -second limit is the same on every machine rather than a function of the runner's -locale. +short: + +- Only **org-prefixed** literals are caught. A bare repo name cannot be linted + without a denylist that is itself the leak — that half stays with review and + AGENTS.md. +- The name class is **ASCII**, so a name whose tail is non-ASCII (a U+2010 + homoglyph dash, a U+017F long s, CJK text) is read only as far as its ASCII + prefix. The class cannot simply be widened to high bytes: this tree + legitimately writes an ellipsis, an em dash and CJK text flush against a real + name in prose, so widening turns those into findings. The scan runs under + `LC_ALL=C` so this limit is the same on every machine rather than a function + of the runner's locale. Closing it properly needs the offset-aware scan + [`public-repo-hygiene`](../public-repo-hygiene/) already implements. +- **Binary blobs are never read.** Both scan paths pass `-I`, so a UTF-16 file, + a file carrying a stray NUL, or one `.gitattributes` line (`*.md binary`) + takes whole file types out of the scan with no signal in the run output. Text + is the only surface this lint claims. ## Running it diff --git a/.github/lint/check-org-repo-literals.sh b/.github/lint/check-org-repo-literals.sh index 521e989..9df36db 100755 --- a/.github/lint/check-org-repo-literals.sh +++ b/.github/lint/check-org-repo-literals.sh @@ -9,21 +9,28 @@ # # ALLOWLIST, NEVER DENYLIST. A denylist grep would have to commit the private # names into this public repo to match them — the lint would BE the leak. So -# every `Comfy-Org/` literal in the tracked tree is a finding unless -# is on the committed allowlist. Adding a name is an allowlist edit, +# every org-prefixed repo literal in the tracked tree is a finding unless the +# name is on the committed allowlist. Adding a name is an allowlist edit, # which review sees. # # KNOWN LIMITATIONS — read these before trusting a green run: # 1. Only ORG-PREFIXED literals are caught. A bare repo name written without -# the `Comfy-Org/` prefix cannot be linted without committing a denylist -# that is itself the leak. Bare-name discipline stays with review and -# AGENTS.md. +# the org prefix cannot be linted without committing a denylist that is +# itself the leak. Bare-name discipline stays with review and AGENTS.md. # 2. The name class is ASCII and the scan runs in the C locale, so a name # whose tail is non-ASCII -- a U+2010 homoglyph dash, a U+017F long s, CJK # text -- is read only as far as its ASCII PREFIX, and a private name -# spelled that way can clear on an allowlisted prefix. Handling that needs -# the offset-aware scan `public-repo-hygiene` does; see limitation 3. -# 3. Category 3 only. `.github/public-repo-hygiene/` is the rigorous, +# spelled that way can clear on an allowlisted prefix. The class cannot +# simply be widened to high bytes: this tree legitimately writes an +# ellipsis, an em dash and CJK text flush against a real name in prose, so +# widening turns those into findings. Handling it properly needs the +# offset-aware scan `public-repo-hygiene` does; see limitation 4. +# 3. Both scan paths pass `-I`, so a tracked blob git classifies as BINARY is +# never read: a UTF-16 file, a file carrying a stray NUL, or one +# `.gitattributes` line (`*.md binary`) removes whole file types from the +# scan and a green run says nothing about them. Text is the only surface +# this lint claims. +# 4. Category 3 only. `.github/public-repo-hygiene/` is the rigorous, # org-wide implementation of this idea (it also covers ticket ids, # internal collaboration-tool links and the homoglyph case above). This # repo cannot adopt that caller as-is: it is that checker's own home, so @@ -56,11 +63,18 @@ ORG='Comfy-Org' # anywhere in this file would be a finding against this very lint. # # Matched case-INSENSITIVELY (`grep -i`), because GitHub resolves owner names -# case-insensitively: `comfy-org/` reaches the same repository, so +# case-insensitively: a lowercased org spelling reaches the same repository, so # matching only the canonical spelling would leave a one-keystroke bypass of a # default-deny control. The name class is already both cases, so `-i` widens # nothing but the org segment. -PATTERN="${ORG}/[A-Za-z0-9_.-]+" +# +# The optional leading `@` is CAPTURED, not skipped: an `@`-prefixed match is a +# CODEOWNERS team handle or an npm/GitHub Packages scope, and team handles are +# allowlisted separately from repo names (see `@`-entries below). Without the +# capture the two share one namespace, so a team slug allowlisted for a +# CODEOWNERS fixture would also clear a literal reference to a private REPO of +# the same name. `public-repo-hygiene` splits the two for the same reason. +PATTERN="@?${ORG}/[A-Za-z0-9_.-]+" root='.' allowlist_rel='.github/lint/org-repo-allowlist.txt' @@ -104,30 +118,66 @@ else exit 2 fi +# Two names no allowlist entry has any business matching. An entry that clears +# BOTH is not an allowlist line, it is an off switch: `*`, `**`, `?*` and +# `[a-z]*` all read as ordinary-looking edits while clearing every name that +# reaches the `case` below. Testing behaviour rather than spelling is what makes +# the guard cover the spellings nobody thought to enumerate; requiring BOTH +# probes keeps a narrow-but-odd glob from tripping it. +OVERBROAD_PROBE_A='zzq0probe-not-a-real-name-9f3d' +OVERBROAD_PROBE_B='qx7probe.not-a-real-name-2b8' + # Allowlist entries are shell globs, matched case-insensitively (GitHub # resolves owner/repo names case-insensitively, so a case variant of an # allowlisted PUBLIC name is the same repo — and no private name is on this # list under any casing, so folding case cannot clear one). +# +# An entry written with a leading `@` is a TEAM/scope entry: it clears only an +# `@`-prefixed literal. A plain entry clears either spelling, because +# `@/` is also how npm and GitHub Packages write a package scope for +# a repo of that name — the same asymmetry `public-repo-hygiene` documents. patterns=() pattern_count=0 +team_patterns=() +team_pattern_count=0 # `|| [ -n "$line" ]` so a final entry with no trailing newline is not dropped. while IFS= read -r line || [ -n "$line" ]; do line="${line%%#*}" # strip the trailing "why it's safe" comment line="${line#"${line%%[![:space:]]*}"}" # ltrim line="${line%"${line##*[![:space:]]}"}" # rtrim [ -n "$line" ] || continue - # A bare `*` would clear every name and neuter the whole control. Reject it - # loudly rather than letting one character pass as an ordinary allowlist edit. - [ "$line" != '*' ] || { echo "error: allowlist '$allowlist' has a bare '*' entry, which allows every name" >&2; exit 2; } - patterns+=("$(printf '%s' "$line" | tr '[:upper:]' '[:lower:]')") - pattern_count=$((pattern_count + 1)) + entry="$(printf '%s' "$line" | tr '[:upper:]' '[:lower:]')" + is_team=0 + case "$entry" in + @*) is_team=1; entry="${entry#@}" ;; + esac + [ -n "$entry" ] || { echo "error: allowlist '$allowlist' has an entry that is a bare '@'" >&2; exit 2; } + # shellcheck disable=SC2254 # $entry is intentionally a glob, not a literal + case "$OVERBROAD_PROBE_A" in + $entry) + # shellcheck disable=SC2254 + case "$OVERBROAD_PROBE_B" in + $entry) + echo "error: allowlist '$allowlist' entry '$line' matches arbitrary names, which allows every name" >&2 + exit 2 + ;; + esac + ;; + esac + if [ "$is_team" -eq 1 ]; then + team_patterns+=("$entry") + team_pattern_count=$((team_pattern_count + 1)) + else + patterns+=("$entry") + pattern_count=$((pattern_count + 1)) + fi done < "$allowlist" # Counted rather than `${#patterns[@]}`: bash 3.2 (still /bin/bash on macOS) # treats an empty array as unset under `set -u`, so the guard would abort with -# `unbound variable` instead of this message. The `for` below is only reached -# with a non-empty array for the same reason. -if [ "$pattern_count" -eq 0 ]; then +# `unbound variable` instead of this message. The `for` loops below are only +# reached with a non-empty array for the same reason. +if [ "$((pattern_count + team_pattern_count))" -eq 0 ]; then echo "error: allowlist '$allowlist' has no entries" >&2 exit 2 fi @@ -138,13 +188,35 @@ fi hits='' scan_status=0 if git -C "$root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then - hits="$(git -C "$root" grep -oiInE -- "$PATTERN" \ - -- . ":(exclude)$allowlist_rel")" || scan_status=$? + # `--is-inside-work-tree` is true for any directory NESTED under a work tree, + # not just its root, and the `.` pathspec resolves relative to `$root`. So a + # `--root` below the top scans only that subtree — and if the subtree holds no + # tracked files, `git grep` matches nothing, exits 1, and that is + # indistinguishable from a clean tree. Fail closed instead, and report the + # scope actually scanned rather than claiming the whole tracked tree. + scope="the tracked files under '$root'" + first_tracked="$(git -C "$root" ls-files -- . 2>/dev/null | head -n 1 || true)" + if [ -z "$first_tracked" ]; then + echo "error: no tracked files under '$root' — refusing to report a clean tree" >&2 + exit 2 + fi + # The allowlist file itself is NOT excluded. Its entries are bare names that + # cannot match PATTERN anyway, so an exclusion would buy only one thing: + # letting its free-text rationale comments go unchecked, so that a line like + # `foo # mirrors /` would publish an unapproved name with CI + # green. The non-git path below never excluded it either. + hits="$(git -C "$root" grep -oiInE -- "$PATTERN" -- .)" || scan_status=$? else # `cd` + a `.` root rather than grepping "$root" and stripping the prefix # afterwards: the strip would be a `sed` expression built from a path this # script does not control. - hits="$(cd "$root" && grep -rIoiEn --exclude-dir=.git -- "$PATTERN" .)" || scan_status=$? + # + # `|| exit 3` rather than `&&`: a failed `cd` would otherwise exit 1, which is + # grep's "no matches" status, and the guard below would wave it through as a + # clean tree. `[ -d "$root" ]` above does not cover it (a directory with no + # execute bit, or a TOCTOU replacement between the two). + scope="'$root'" + hits="$(cd -- "$root" || exit 3; grep -rIoiEn --exclude-dir=.git -- "$PATTERN" .)" || scan_status=$? fi # Both tools exit 1 for "no matches" and >1 for a real error. Swallowing the # latter with `|| true` would report a clean tree because the scan never ran -- @@ -158,13 +230,18 @@ fi # Trailing sentence punctuation the name class swallowed, then a `.git` suffix # (`Foo.git` still references the repo `Foo`), then punctuation again -- # `/foo.git.` needs both passes, and stripping `.git` first leaves the -# suffix stuck behind the period. A GitHub slug can end in none of these, so -# every pass can only narrow the name. -trim_trailing_punctuation() { +# suffix stuck behind the period. +# +# ONLY `.` is stripped. A GitHub slug may not END in a period, so dropping one +# can only narrow the name. `-` and `_` are different: both are LEGAL as a +# slug's last character, so stripping them would WIDEN the match — a literal for +# a private repo named `-` or `_` would normalize onto +# the allowlisted entry and clear this default-deny check. +trim_trailing_dots() { local value="$1" while [ -n "$value" ]; do case "$value" in - *.|*-|*_) value="${value%?}" ;; + *.) value="${value%?}" ;; *) break ;; esac done @@ -182,22 +259,38 @@ while IFS= read -r hit; do file="${where%:*}" file="${file#./}" lineno="${where##*:}" - name="${match#*/}" + # An `@`-prefixed literal may additionally draw on the team/scope entries. + is_at=0 + qualified="$match" + case "$qualified" in + @*) is_at=1; qualified="${qualified#@}" ;; + esac + name="${qualified#*/}" lower="$(printf '%s' "$name" | tr '[:upper:]' '[:lower:]')" - lower="$(trim_trailing_punctuation "$lower")" + lower="$(trim_trailing_dots "$lower")" lower="${lower%.git}" - lower="$(trim_trailing_punctuation "$lower")" + lower="$(trim_trailing_dots "$lower")" # A reference left naming nothing (`/.`, `/.git`) is not a finding. [ -n "$lower" ] || continue allowed=0 - for p in "${patterns[@]}"; do - # shellcheck disable=SC2254 # $p is intentionally a glob, not a literal - case "$lower" in - $p) allowed=1; break ;; - esac - done + if [ "$pattern_count" -gt 0 ]; then + for p in "${patterns[@]}"; do + # shellcheck disable=SC2254 # $p is intentionally a glob, not a literal + case "$lower" in + $p) allowed=1; break ;; + esac + done + fi + if [ "$allowed" -eq 0 ] && [ "$is_at" -eq 1 ] && [ "$team_pattern_count" -gt 0 ]; then + for p in "${team_patterns[@]}"; do + # shellcheck disable=SC2254 # $p is intentionally a glob, not a literal + case "$lower" in + $p) allowed=1; break ;; + esac + done + fi if [ "$allowed" -eq 0 ]; then findings=$((findings + 1)) @@ -218,4 +311,4 @@ MSG exit 1 fi -echo "OK: every ${ORG} repo literal in the tracked tree is on $allowlist_rel" +echo "OK: every ${ORG} repo literal in $scope is on $allowlist_rel" diff --git a/.github/lint/org-repo-allowlist.txt b/.github/lint/org-repo-allowlist.txt index 72ddbfc..7c9b810 100644 --- a/.github/lint/org-repo-allowlist.txt +++ b/.github/lint/org-repo-allowlist.txt @@ -3,8 +3,21 @@ # # One entry per line, each with a trailing `#` comment saying WHY it is safe. # Entries are shell globs and are matched case-insensitively (GitHub resolves -# owner/repo names case-insensitively). A trailing `.git` and trailing sentence -# punctuation are stripped from the literal before it is compared. +# owner/repo names case-insensitively). A trailing `.git` suffix and trailing +# periods are stripped from the literal before it is compared; a trailing `-` +# or `_` is NOT, because both are legal at the end of a real slug and +# stripping them would let `-` clear on ``. +# +# An entry written with a leading `@` is a TEAM/scope entry: it clears only an +# `@`-prefixed literal (a CODEOWNERS team handle, or an npm / GitHub Packages +# scope). A plain entry clears either spelling. Keeping team slugs out of the +# repo namespace is why a CODEOWNERS fixture's team name does not also clear a +# literal reference to a private REPO of that name. +# +# No entry may match an arbitrary name: an entry that clears two unrelated probe +# names is rejected as a configuration error (exit 2), because `*`, `**`, +# `?*` and `[a-z]*` all read as ordinary allowlist edits while switching the +# whole control off. # # Adding a line here is the ratification step: it is what makes "this name is # safe to publish" a reviewed decision instead of an assumption. Never add a @@ -15,8 +28,11 @@ # .github/public-repo-hygiene/check_public_repo_hygiene.py and is the source of # truth for other repos; this file is repo-local because this repo additionally # carries that checker's own fake-private test fixtures. +# +# This file is scanned like every other tracked file — the comments above and +# below are NOT exempt, so a rationale that quotes a private name is a finding. -# ── Real Comfy-Org repos, verified public ── +# ── Real repos in this org, verified public ── github-workflows # this repo, public .github # org meta repo, public comfy-cli # public (also on the org-wide PUBLIC_COMFY_ORG_REPOS list) @@ -28,12 +44,20 @@ comfyui_frontend # public (org-wide list: ComfyUI_frontend) # ── Documented exceptions ── unreviewed-merges # private, but deliberately published in README/docs as the SOC-2 tracker destination — human-ratified exception -# ── CODEOWNERS team handles (`@Comfy-Org/`) ── +# ── CODEOWNERS team handles / package scopes (`@`-prefixed literals only) ── # Team handles are inherently public on a public repo: GitHub renders CODEOWNERS -# owners to anyone who can see the repo. Mirrors PUBLIC_COMFY_ORG_TEAMS. -comfy-cloud-team # team handle, org-wide PUBLIC_COMFY_ORG_TEAMS -core-engine-team # team handle, org-wide PUBLIC_COMFY_ORG_TEAMS -backend # team handle in the agents-md-integrity suite's CODEOWNERS fixture — a team slug, not a repo +# owners to anyone who can see the repo. Mirrors PUBLIC_COMFY_ORG_TEAMS. These +# are `@`-scoped so none of them also clears a bare repo reference. +@core-engine-team # team handle, org-wide PUBLIC_COMFY_ORG_TEAMS +@backend # team handle in the agents-md-integrity suite's CODEOWNERS fixture — a team slug, not a repo +@not-a-public-team # fixture team handle in the public-repo-hygiene suite +@core-engine # ASCII prefix of the public-repo-hygiene suite's `core-engineteam` fixture handle (see limitation 2) + +# ── Team handles that ALSO appear bare, as fixtures ── +# The public-repo-hygiene suite writes these without the `@` on purpose, to +# prove the bare path is checked against the repo list. Both are invented. +comfy-cloud-team # team handle, org-wide PUBLIC_COMFY_ORG_TEAMS; also written bare in a public-repo-hygiene fixture +some-team # fixture team handle, also written bare in a public-repo-hygiene HF-route fixture # ── Documentation examples — no such repo exists ── github-workflows-tools # doc example in the bump-callers docs @@ -45,8 +69,9 @@ x # single-letter placeholder in the public-repo-hygie notes # path-shaped example (`docs/v1.2//notes`) in the public-repo-hygiene docs # ── Hugging Face model/dataset namespaces, not GitHub repos ── -# Comfy-Org publishes weights under huggingface.co/Comfy-Org/, so these names -# have no GitHub existence at all. They are product content, not references. +# This org publishes weights under a Hugging Face namespace of the same name, so +# these names have no GitHub existence at all. They are product content, not +# references. qwen-image-edit_comfyui # public HF model, quoted in the public-repo-hygiene docs/tests qwen-image_comfyui # public HF model, quoted in the public-repo-hygiene tests ace_step_1.5_comfyui_files # public HF model, quoted in the public-repo-hygiene tests @@ -60,14 +85,13 @@ some-set-abc123 # HF-route fixture in the public-repo-hygiene tests # ── Test fixtures — invented names, no such repo ── # These are the fake "private" names the bump-callers and public-repo-hygiene -# suites use to prove masking and default-deny. A NEW fixture name needs a line -# here, on purpose: that edit is where a reviewer confirms it is invented. -secret-* # bump-callers + public-repo-hygiene fixture family -secret_a.b-c # bump-callers fixture (dot/underscore name shape) -a.b.c # bump-callers fixture (dotted name shape) +# suites use to prove masking and default-deny. Enumerated rather than globbed: +# a `secret-*` glob would pre-approve an unbounded family on a default-deny +# list, so a real private repo whose name began `secret-` would ship without +# tripping the lint. A NEW fixture name needs a line here, on purpose: that edit +# is where a reviewer confirms it is invented. comfyui2 # public-repo-hygiene fixture: invented near-miss of a real public name -some-team # public-repo-hygiene fixture team handle -not-a-public-team # public-repo-hygiene fixture team handle +a.b.c # bump-callers fixture (dotted name shape) not-a-public-repo # public-repo-hygiene fixture private # public-repo-hygiene fixture private-thing # public-repo-hygiene fixture @@ -86,6 +110,63 @@ plain-http # public-repo-hygiene model-host anchor fixture protocol-relative # public-repo-hygiene model-host anchor fixture title-case-route # public-repo-hygiene model-host route fixture upper-case-route # public-repo-hygiene model-host route fixture +secret-a # fixture: bump-callers / public-repo-hygiene +secret-alpha # fixture: bump-callers / public-repo-hygiene +secret-anchor # fixture: bump-callers / public-repo-hygiene +secret-b # fixture: bump-callers / public-repo-hygiene +secret-bad # fixture: bump-callers / public-repo-hygiene +secret-beta # fixture: bump-callers / public-repo-hygiene +secret-both # fixture: bump-callers / public-repo-hygiene +secret-branch # fixture: bump-callers / public-repo-hygiene +secret-c # fixture: bump-callers / public-repo-hygiene +secret-case # fixture: bump-callers / public-repo-hygiene +secret-coloc # fixture: bump-callers / public-repo-hygiene +secret-conv # fixture: bump-callers / public-repo-hygiene +secret-converged # fixture: bump-callers / public-repo-hygiene +secret-converted # fixture: bump-callers / public-repo-hygiene +secret-deriskonly # fixture: bump-callers / public-repo-hygiene +secret-dup # fixture: bump-callers / public-repo-hygiene +secret-dupnopin # fixture: bump-callers / public-repo-hygiene +secret-emptyref # fixture: bump-callers / public-repo-hygiene +secret-expr # fixture: bump-callers / public-repo-hygiene +secret-floating # fixture: bump-callers / public-repo-hygiene +secret-flow # fixture: bump-callers / public-repo-hygiene +secret-gamma # fixture: bump-callers / public-repo-hygiene +secret-good # fixture: bump-callers / public-repo-hygiene +secret-groomed # fixture: bump-callers / public-repo-hygiene +secret-half # fixture: bump-callers / public-repo-hygiene +secret-hashref # fixture: bump-callers / public-repo-hygiene +secret-lbl # fixture: bump-callers / public-repo-hygiene +secret-legacy # fixture: bump-callers / public-repo-hygiene +secret-longkey # fixture: bump-callers / public-repo-hygiene +secret-lowercase # fixture: bump-callers / public-repo-hygiene +secret-mention # fixture: bump-callers / public-repo-hygiene +secret-mono # fixture: bump-callers / public-repo-hygiene +secret-multi # fixture: bump-callers / public-repo-hygiene +secret-nopin # fixture: bump-callers / public-repo-hygiene +secret-nopin-two # fixture: bump-callers / public-repo-hygiene +secret-noscript # fixture: bump-callers / public-repo-hygiene +secret-notuses # fixture: bump-callers / public-repo-hygiene +secret-ok # fixture: bump-callers / public-repo-hygiene +secret-one # fixture: bump-callers / public-repo-hygiene +secret-pinned # fixture: bump-callers / public-repo-hygiene +secret-pkg # fixture: bump-callers / public-repo-hygiene +secret-prose # fixture: bump-callers / public-repo-hygiene +secret-quoted # fixture: bump-callers / public-repo-hygiene +secret-riskonly # fixture: bump-callers / public-repo-hygiene +secret-shortref # fixture: bump-callers / public-repo-hygiene +secret-sibling # fixture: bump-callers / public-repo-hygiene +secret-sibref # fixture: bump-callers / public-repo-hygiene +secret-squad # fixture: bump-callers / public-repo-hygiene +secret-tagref # fixture: bump-callers / public-repo-hygiene +secret-three # fixture: bump-callers / public-repo-hygiene +secret-two # fixture: bump-callers / public-repo-hygiene +secret-unwired # fixture: bump-callers / public-repo-hygiene +secret-wdup # fixture: bump-callers / public-repo-hygiene +secret-wired # fixture: bump-callers / public-repo-hygiene +secret-wref # fixture: bump-callers / public-repo-hygiene +secret-wrongfleet # fixture: bump-callers / public-repo-hygiene +secret_a.b-c # fixture: bump-callers / public-repo-hygiene # ── ASCII prefixes of the public-repo-hygiene suite's homoglyph fixtures ── # That suite deliberately commits names whose tail is non-ASCII, to prove its @@ -93,5 +174,4 @@ upper-case-route # public-repo-hygiene model-host route fixture # check-org-repo-literals.sh), so it reads only the prefix. Each of these is # the prefix of an INVENTED fixture name, not of a real repo. comfy-type # prefix of the `comfy-typecript-sdk` fixture -core-engine # prefix of the `core-engineteam` fixture mod # prefix of the `model` HF-route fixture diff --git a/.github/workflows/test-org-repo-literals.yml b/.github/workflows/test-org-repo-literals.yml index 0b750c7..aa0b155 100644 --- a/.github/workflows/test-org-repo-literals.yml +++ b/.github/workflows/test-org-repo-literals.yml @@ -52,60 +52,127 @@ jobs: # scanned by the step above and would have to be allowlisted — which # would defeat the fixture, since the point is that it is NOT allowlisted. # (`%s/%s` is not a match: the name class cannot start with `%`.) - - name: Smoke test — an unlisted name fails the lint + # + # Each negative case asserts the EXACT status. "any non-zero" would let a + # lint that died with exit 2 (renamed allowlist, unreadable root, an + # argument rejected after a refactor) read as "correctly rejected the + # name" while nothing was scanned. Output is captured and echoed only on + # failure, because a finding prints a `::error file=…` workflow command + # and a SUCCESSFUL run must not publish annotations pointing at a temp + # file that is not in the repo. + - name: Smoke test — findings exit 1, allowlisted names exit 0 run: | + fail=0 + expect() { # expect