mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #206] [MERGED] feat(public-repo-hygiene): centralize the public-repo leak guard as a reusable workflow (BE-8654) #215
Labels
No labels
cursor-review-catalog-drift
groom
groom
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/github-workflows#215
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/Comfy-Org/github-workflows/pull/206
Author: @mattmillerai
Created: 8/22/2026
Status: ✅ Merged
Merged: 8/22/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-8654-public-repo-hygiene📝 Commits (6)
e6b5b72feat(public-repo-hygiene): centralize the public-repo leak guard as a reusable workflow (BE-8654)b804098fix(public-repo-hygiene): make every "declined to look" route fail loudly (BE-8654)1b93baffix(public-repo-hygiene): close the coverage and log-volume holes the panel found (BE-8654)b0ea12dfix(public-repo-hygiene): case-insensitive matching and sentence-period stripping (BE-8697) (#207)8af49e6fix(public-repo-hygiene): close the work-tree-encoding bypass and the remaining coverage-accounting holes (BE-8654)982f5dcfix(public-repo-hygiene): close the LFS/homoglyph/encoding bypasses the panel found (BE-8654)📊 Changes
13 files changed (+3841 additions, -18 deletions)
View changed files
📝
.github/bump-callers/README.md(+1 -0)➕
.github/public-repo-hygiene/README.md(+190 -0)➕
.github/public-repo-hygiene/check_public_repo_hygiene.py(+1232 -0)➕
.github/public-repo-hygiene/tests/test_check_public_repo_hygiene.py(+1326 -0)📝
.github/reviewers.yml(+7 -1)➕
.github/workflows/bump-public-repo-hygiene-callers.yml(+127 -0)➕
.github/workflows/public-repo-hygiene.yml(+273 -0)➕
.github/workflows/test-public-repo-hygiene.yml(+436 -0)📝
AGENTS.md(+28 -4)📝
CONTRIBUTING.md(+3 -0)📝
README.md(+11 -4)📝
docs/callers/README.md(+24 -9)➕
docs/callers/public-repo-hygiene.md(+183 -0)📄 Description
ELI-5
There is a check that stops internal-only things — ticket ids, Datadog/Linear/Notion links, private repo names — from being published in a public repo. It existed in exactly two repos, and in both of them it ran out of the pull request's own checkout, which means a PR could edit or delete the check that was judging it. This moves the check (and, more importantly, the list of which repos are public) here, where a caller pins it by SHA and cannot touch it.
What changed
public-repo-hygiene.yml— the reusable workflow, modelled onagents-md-integrity.yml.workflows_refis required with no default, and the checkout's own job runs the empty-ref guard..github/public-repo-hygiene/— one merged checker replacing two divergent copies (a 178-line Python one and a 187-line JavaScript one), plus the org-wide known-public repo/team allowlist, plus a README covering the tamper boundary and the known limitations.test-public-repo-hygiene.yml— unit tests (40) plus seven end-to-end smoke steps.bump-public-repo-hygiene-callers.yml— the SHA-propagation fleet, over the sharedbump-callers.sh.README.md,docs/callers/(index + new setup guide),AGENTS.md,CONTRIBUTING.md,.github/bump-callers/README.md,.github/reviewers.yml.The tamper boundary, stated plainly
The known-public repo and team allowlists are not a workflow input and are not readable from the caller's tree. An allowlist a caller can pass is an allowlist a PR in the caller repo can widen, which is the hole being closed. The two knobs that are inputs are scoped so they cannot loosen the rule:
ticket_allowlistexclude_pathsBoth are visible in the caller's workflow diff and in the run log. That is the honest boundary: the rule is untouchable from a caller repo, and narrowing the scope is loud.
Asserted, not just asserted-in-prose:
TamperResistanceTestplants a wide-open copy of the checker at both the historical per-repo path and the centralized one, a config file, and environment variables named after the module constants — and the verdict does not move.test-public-repo-hygiene.ymlruns the same fixture end to end.Parity, proven before anything is deleted
Both SDK checkers and this one were run over the same seeded trees (each SDK repo at its default branch tip plus a corpus exercising every category and shape boundary), and the normalized findings diffed.
Comfy-Org/github-workflowsno longer flagged — it is now allowlisted, which is the point of the ticket.Comfy-Org/comfy-cli.gitno longer flagged — the trailing.gitstrip, which the JavaScript copy already had and the Python one did not. Where the two copies disagreed, the stricter/more-correct one won; the choice is documented rather than silent.Nothing has been deleted. See "not done" below.
That proof describes
e6b5b72, not the branch tip. Two later commits changed detection onpurpose, each in its own reviewed change, so the 18/18 diff no longer describes this branch
byte-for-byte. Both divergences are strictly in the stricter direction and are pinned by unit
tests:
b0ea12d(#207, BE-8697) — the org segment and allowlist membership are now matchedcase-insensitively, and a sentence-final period is stripped. That closes the two carried-over
defects described below and makes
comfy-org/<private-repo>a finding where it previously wasnot.
8af49e6— no category, regex or allowlist entry changed. What changed is coverageaccounting: an LFS pointer stub and a submodule gitlink no longer count as scanned, and a
working-tree-encodinggitattribute is refused outright.Re-running the parity diff is the first thing the SDK-conversion PRs should do, since it is what
licenses deleting those two copies.
Carried-over quirks — carried over, then fixed on their own PR
A parity proof is only worth something if the migration changed nothing, so two real defects in the original scripts were deliberately carried over rather than fixed in the move:
…built on Comfy-Org/ComfyUI.was flagged asComfyUI.. This fires on ordinary English.comfy-org/<private-repo>reference was not flagged, though GitHub resolves owner names case-insensitively. That is a one-keystroke bypass of a default-deny control.Both are now fixed, in
b0ea12d(#207, BE-8697) — its own reviewable change, exactly as intended, rather than bundled into a move. That is where detection parity with the two per-repo scripts deliberately ended; the checker README says so, and unit tests pin both behaviours. The remaining detection findings from the review rounds below are still tracked as follow-ups.Coverage: the half this does not fix
Sized with the same sweep, pointed at the remainder:
comfy-python-sdkandcomfy-typescript-sdk. Confirmed org-wide by code search, not by inference.Not done, and why
Comfy-Org/comfy-python-sdkandComfy-Org/comfy-typescript-sdk, outside this repo. Parity — the half the acceptance criteria puts before deletion — is proven above and is what makes that conversion safe to do next; the conversion itself needs a PR in each of those repos.PUBLIC_REPO_HYGIENE_CALLERSroster is not seeded, since there are no callers yet.ALLOW_EMPTYistrueand the fleet is a clean no-op; flip it tofalsewith the first enrolment. Enrolling a caller is two steps — merge the caller and add it to the roster — and skipping the second is the most repeated mistake in this repo.github-workflows. Pre-blessing 60-odd names nobody reviewed would trade the default-deny posture for convenience, and a repo that later goes private would silently stop being flagged. Adding one is now a single reviewed edit here instead of one per consumer.(BE-####)in commit messages.detect-unreviewed-merge.ymlis un-self-enrolled here for a comparable reason.Review rounds — what the panel found, and what moved
Rounds 1–2 (
b804098,1b93baf)Ten findings from the cursor-review panel. Five are fixed; five are detection changes and were deferred, with a follow-up specified for each. All ten threads carry a reply and are resolved.
Fixed — every route by which the guard could decline to look and still report green:
workflows_refthat is not a 40-hex SHA only warned, so nothing enforced that the checker came from the commituses:pinnedjob.workflow_sha— the commit this reusable resolved from, which the runner computes fromuses:and a caller cannot setopen()follows symlinks: a tracked link read whatever it pointed at, so one out of the repo pulled arbitrary runner content into a public run log, and one to/dev/zeroor a FIFO was an OOM or a hangos.lstatskips anything not a regular file, with a::warning::naming it; reads capped at 5 MiB with the tail warned about;checkjob giventimeout-minutes: 15(it had none)_public_repo_hygiene/path was skipped unconditionally — and since the workflow's own checkout lands untracked, that skip only ever fired in the bypass casescannedcounted them anywayNOT SCANNED:counts;scannednow counts only files actually read as text, so an all-binary tree trips the zero-scan netSCANNED: <n> file(s) read as textDeferred — the five that change what counts as a finding. This PR's acceptance criterion is a parity proof: old and new run over the same trees, findings diffed 18/18 identical, which is what licenses deleting the two per-repo SDK copies next. Each of these invalidates that proof, so each is its own reviewable change:
Category 3 case-sensitivity— since fixed, inb0ea12d(#207, BE-8697): the org segment and allowlist membership are now matched case-insensitively and the trailing-period false positive is gone. Routed exactly as intended — deferred here, filed, and landed as its own reviewed PR.docs/<TICKET>-migration.mdor anotion-exports/directory passes clean, and the path is the more exposed half.CVE-2024-style false positive is fixed (public identifier namespaces now clear by prefix, so nothing expires each January). What remains deferred is the other half —\btreats_as a word character, soBE-1234_designnever matches — which is a widening of what counts as a finding and belongs on its own PR.None of these is live coverage being lost: this PR enrols zero callers. The remaining follow-ups should land before adoption.
Round 3 (
8af49e6)Ten more findings. Nine acted on, one declined; all ten threads carry a reply and are resolved. Every one of the fixes is the same shape as round 2's: a route by which the guard declines to look at something and still reports green.
.gitattributessettingworking-tree-encoding=UTF-16keeps the committed blob UTF-8 — plainly readable on github.com, in the API and in the diff — while checkout writes NUL-laden UTF-16 to disk, which the scan skipped asbinarygit check-attr, so the answer is git's own, at every directory level plus.git/info/attributes. An excluded path may still carry one — that hole is already named by its exclusion countworkflows_ref == job.workflow_shabinds the checker to whateveruses:resolved to, but cannot bind whichuses:line runs — apull_requestcaller executes its workflow file from the PR head.github/workflows/. See "The tamper boundary" abovescanned, sogit lfs track '*.md'plus a leaking commit exited 0 on a required checkskippedkind. An all-LFS tree now reads zero files and exits 2-e,-L, andgit ls-files). The checker's guard stays as the in-tree backstop for direct invocationwarningshad no cap, unlike the findings — six producers each embed a path, so a tree of tracked symlinks floods a public run logMAX_WARNINGS_TOTAL(200) with a+N moretail. The counts stay uncapped, so the coverage arithmetic is still completestat.S_ISDIRbranch ahead of it, with its own skip kind and a message naming it as an unfetched submoduletest-public-repo-hygiene.ymlset notimeout-minutes, while its own fixtures build a FIFO and a link to/dev/zerotimeout-minutes: 15, matching the scan jobworkflows_reflist left the sentence below it wrong about which workflows reject a non-SHAjob.workflow_sha, runner ≥ v2.334.0) and links to the caller docs for the full three tiersfooslack.com/archives/xis flagged) and require/straight after the host (notion.so:443/team/pageis not)Declined — one. A symlink counting towards
scannedwas flagged as holding the zero-scan net open. It is not: for a symlink the target string is the blob git stores and publishes at that path, andos.readlinkreads it in full. There is no unexamined "elsewhere", which is exactly what distinguishes it from the LFS stub in the same round — that one did move toskipped. Reasoning is on the thread.Round 4 (
982f5dc)Ten more findings, all ten fixed; every thread carries a reply and is resolved. Unlike round 3, this round does move detection semantics — see the note under Risk.
skippedalso stopped scanning it, and the classifier was a barestartswith— so any tracked file whose first line is the pointer header was skipped whole, with its references unread, while still rendering as plain text on github.comsha256:<64 hex>oid + digitsize, stub-sized), so a lookalike is ordinary text again. And a genuine stub is now read for findings while still not counting towardscanned— coverage and detection are separate questions, and this is the one file kind where they divergeComfy-Org/comfyui‐internalwritten with U+2010 (visually identical to-) cleared ascomfyuiNotComfy-Org/privateis no longer read as this orgbinaryskip while GitHub served the blob as readable textscannedwith no skipped kind, and a per-file findings truncation had no count eitherPARTIAL: <n> file(s) …, with two kinds (read cap, per-file findings cap). Never capped, so 200 symlink warnings can no longer hide that a large file's tail went unreadgit check-attrwithout--cachedresolves attributes from the work tree — the same.gitattributesa conversion may have mangled, so a commit converting that file too made the guard fail open--cached: the property being asserted is about the bytes git stores. Verified the bypass first — with a UTF-16.gitattributeson disk,check-attrreturnsunspecifiedand--cachedreturnsUTF-16\b[A-Z]{2,6}-\d{2,6}\bmatchesCVE-2021insideCVE-2021-44228, so a SECURITY.md or a dependency changelog reddens a required check — and an exact-token carve-out expires every JanuaryCVE-,CWE-,PEP-,RFC-,ISO-,UTF-. (UTFcloses a gap the test suite was already working around in its own fixture.)CVEX-1234is still a finding@path to spare npm scopes also cleared any CODEOWNERS team whose slug matches a public repo name — the commonest naming convention there is@comfy-org/<pkg>still clears and@Comfy-Org/ComfyUI_frontendis a finding again. Residual noted on the thread: an all-lowercase handle colliding with a public repo name still crosses, and what that discloses is a public string by constructionworking-tree-encodingvalue as a conversion hard-failed for two that convert nothing:UTF-8(the identity mapping) and any rule matching a symlink or gitlink, whichcheck-attranswers by path pattern aloneUTF-8exempt casefolded (BOM variants stay fatal);convertedfiltered tostat.S_ISREG. A path that cannot be stat'd stays in the list — unresolvable is not harmlessOne deliberate cost is recorded rather than hidden: because a non-ASCII character after a reference now reads as part of the name, an allowlisted name butted straight against non-Latin prose with no separator (
Comfy-Org/ComfyUIを使う) is a finding. Narrowing the rule to characters that casefold onto ASCII would reopen the bypass for every alphabet that does not, so it is deliberately broad; a space clears it, the finding message says so, and a test pins it as a decision.Risk
The riskiest thing a guard can do is decline to look at something and still report green. After three review rounds, every such route is either traced or fatal:
SCANNED: <n> file(s) read as text— the number everything below has to account against;::warning::naming it; binary, non-UTF-8, submodule and LFS files get a per-runNOT SCANNED:count; an oversized file is truncated loudly. Those per-file warnings are capped at 200 with a+N moretail — but the counts are not, so the arithmetic stays complete, and a file read only as far as a cap now has its own uncappedPARTIAL:count rather than relying on a warning that the cap can drop;SCANNED:: a file whose bytes were read as text, and a symlink whose target string was read — that string being the blob git stores and publishes at that path. An LFS stub and a submodule gitlink deliberately do not (the stub is still checked for references — it just cannot prop up the coverage claim);--exclude, tracked content at the reserved_public_repo_hygiene/path, and aworking-tree-encodinggitattribute on any path the run would read;_public_repo_hygiene/before its own checkout, so nothing at that path is ever replaced;workflows_refmust be a full commit SHA and equaljob.workflow_sha, or the run fails — so the checker cannot be loaded from a commit other than the oneuses:pins. What that cannot cover — whichuses:line runs at all — is now documented rather than claimed away, with the out-of-band control named.Detection semantics have now moved twice: in
b0ea12d(#207, under its own review) and again in982f5dc, where round 4's findings could not be answered any other way — seven of its ten are bypasses in the matching itself. That invalidates the 18/18 parity diff below as a current claim: it describese6b5b72, and the divergences since are enumerated under "Parity". Re-run it as the first step of the SDK conversions. Every semantic change in982f5dcis in the strict-tightening direction except two, both narrowings of a false positive (CVE--style namespaces,UTF-8/symlink encoding rules), and each has a test asserting the tightening did not become a loosening.Verified
Every command below was run against this branch; see the Provenance block for the summary.
Refs BE-8654 —
Refs, notCloses: the ticket's SDK-conversion criterion is unmet (it needs PRs in two other repos) and the coverage gap it opens with is untouched by this change. The ticket stays open to carry those.Provenance
982f5dc.python3 -m unittest discover -s .github/public-repo-hygiene/tests: 97 tests (was 74), 96 passed / 1 skipped (a non-UTF-8 filename cannot be created on APFS; it runs on the ubuntu-latest runner), 0 failed. All 13run:steps oftest-public-repo-hygiene.ymlexecuted locally (11 previous + 2 new: a pointer-lookalike file is still scanned, a homoglyph name is not cleared): 13 pass — including the clean-repo, per-category, tamper-fixture, additive-inputs, symlink, work-tree-encoding, all-LFS and non-git smokes, all unchanged and all still green after the detection changes.check_workflow_pins.py: OK — 11 workflows declareworkflows_ref, none with a default, every ref checkout guarded.workflow-pinstests: 216 passed.git check-attrbehaviour asserted empirically before relying on it (--cachedreturnsUTF-16where the work-tree read returnsunspecified, on git 2.54.0).bump-callerssuites:test_paths_contract.sh70 passed,test_preflight.sh278 passed,test_bump_callers.sh429 passed, 0 failed.agents-md-integritytests: 46 passed.shellcheck -xonbump-callers.sh+preflight.sh: clean, and on the new pre-checkout guard step's script: clean, plus a functional run of it against a fixture that tracks the reserved path. Both new workflow files parse as YAML. Parity: the 18/18 diff describese6b5b72, not the branch tip — see "Parity" above for the two intentional divergences since, and re-run it as the first step of the SDK conversions._-word-boundary half of category-1 shape) — a fourth was deferred the same way and has since landed as #207, and theCVE--namespace half of the fifth is fixed in982f5dc. Round 4 did not defer: all ten of its findings are fixed here, because seven were bypasses in the matching itself and deferring a bypass is not the same trade as deferring a widening. Each is explained above. One round-3 finding was declined rather than fixed, with the reasoning on its thread. Round 4 moves detection semantics, so the 18/18 parity diff is stale as a current claim and must be re-run before the SDK copies are deleted — stated under Risk rather than left implicit. One deliberate new false-positive shape (an allowlisted name butted against non-Latin prose with no separator) is accepted, documented and pinned by a test, because the alternative reopens a homoglyph bypass. Separately:AGENTS.mdis 305 lines against the 200-line ceiling its own standard enforces — pre-existing (mainis already 281) and not gated here, since this repo is not self-enrolled onagents-md-integrity.yml, but this PR does add to it and a trim belongs on its own PR.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.