mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #309] [MERGED] fix(refresh-reviewers): byte-faithful rewrite — bytes read, CRLF-aware block inserts, refuse multi-line flow sequences #309
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#309
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/309
Author: @mattmillerai
Created: 9/18/2026
Status: ✅ Merged
Merged: 9/19/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-15304-refresh-reviewers-byte-faithful-rewrite📝 Commits (2)
b3916b9fix(refresh-reviewers): byte-faithful rewrite — bytes read, CRLF-aware block inserts, refuse multi-line flow sequences0bc0348fix(refresh-reviewers): make the skip decision reach the report, and the endings reach every inserted line📊 Changes
4 files changed (+1077 additions, -87 deletions)
View changed files
📝
.github/refresh-reviewers/README.md(+34 -2)📝
.github/refresh-reviewers/generate.py(+332 -53)📝
.github/refresh-reviewers/tests/test_generate.py(+693 -27)📝
docs/callers/refresh-reviewers.md(+18 -5)📄 Description
ELI-5
refresh-reviewerspromises to touch only the reviewer lists in your.github/reviewers.ymland leave every other byte alone. Three things quietly broke that promise. If your file used Windows line endings, the whole file came back with Unix ones. If a reviewer list was written as a block list, the replacement items came out with Unix endings even in a Windows file. And if a list was spread over two lines (reviewers: [alice,…bob]), the rewrite edited the first line and leftbob]stranded, producing a file that is not valid YAML at all. This PR reads the file as raw bytes, carries each line's own ending through the rewrite, and refuses to touch a list it cannot rewrite on one line — warning about it instead and saying how to fix it.What changed
generate.py— read the committed config without newline translation. Thegit showthat loads the config ran withsubprocess.run(..., text=True); Python's universal-newline translation turns a CRLF config's\r\ninto\nbefore the surgical rewrite ever sees the file, so every line of the proposal differs from the committed bytes. It is now read as bytes and decoded explicitly in a new, unit-testableread_committed_config(branch, config_path) -> str | None. A blob that is not valid UTF-8 now takes the documented clean no-op (with a::warning::naming the offending byte offset) rather than a lossyreplace-decoded rewrite of somebody's committed file. The proposal is written withnewline=""so the runner OS cannot translate the endings a second time on the way out.rewrite_config— block-form insertions carry the document's line ending.text.split("\n")leaves the\ron the end of every CRLF line, so the flow and scalar rewrites — which edit a span within a line — were already byte-faithful. Only the inserted block item lines had no line to inherit from. Each inserted item now copies the ending of the item line it replaces, detected per line, so a deliberately mixed-ending file comes back exactly as mixed as it went in.rewrite_config— refuse to rewrite an unterminated (multi-line) flow sequence. Every edit the rewrite makes is confined to the one line a location names, soreviewers: [alice,whose]lives on a later line has no location shape it can safely take: replacing the opening line's[...]span strands the continuation lines as orphaned YAML. A new_flow_is_unterminated()tags those locations("unterminated", i)instead of("flow", i)at the two sites that can produce one (thedefault_poolline andset_key's flow branch; the scalar branch cannot).rewrite_configskips them, a new publicunterminated_locations(locs)names them as("default_pool" | "rules[<n>].reviewers", line_idx),mainemits one::warning::per list with file and line, and the drift PR's body gains a line naming each skipped list and the one-line fix._parse_flowis untouched, so theconfighalf ofparse_reviewer_config's return is unchanged — only the location tag moved. Proven directly rather than assumed: see Verified..github/refresh-reviewers/README.mdanddocs/callers/refresh-reviewers.mdnow state the line-ending guarantee and both shapes the rewrite deliberately cannot manage (the pre-existing inline-comment-on-a-block-item caveat, and the new multi-line flow one). The "Location shapes" comment block ingenerate.pygains the third shape and the reason it exists.Reachability
The entrypoint is the
workflow_callreusablerefresh-reviewers.yml; the producer is a human editing a caller repo's.github/reviewers.yml. No config in any current caller triggers either defect, so this is a latent fix, not a live incident. No behaviour changes at all for an LF, single-line config — the existing byte-identical tests are untouched and green.Verified
Beyond unit tests, both defects were reproduced and the fix confirmed end to end through the real
main()entrypoint, against throwaway git repos, with the GitHub API stubbed:0CRLF and6bare LF — every line's bytes differ from what was committed. After:6CRLF,0bare LF, and only the two intentionally-edited lists differ from the committed text.default_pool: [old-a,/old-b]). Before: the proposal wasdefault_pool: [alice]/old-b]— a fileyaml.safe_loadrefuses (while parsing a block mapping), i.e. a drift PR that, if merged, breaks the consumer's reviewer config outright. After: the list is left exactly as committed,::warning::.github/reviewers.yml:1: default_pool is a multi-line flow sequence…is emitted, the PR body names it, and the output loads cleanly.Negative-claim falsification
This diff ships a refusal path ("left unchanged", "the generator only rewrites single-line lists"), so the capability-denial check applies. The denied capability was attempted via every path the product offers before the refusal was written:
rewrite_configis the only writer of areviewers.ymlanywhere in this repo —assign-reviewers.yml's JSparseReviewerConfigis read-only, and no script or workflow emits one — and running that one path on a multi-line flow sequence produces the unparseable output quoted above. So the behaviour being removed is corruption, not function. The refusal also ships a working redirect rather than a dead end: putting the list on one line is handled by the flow rewrite, which is covered by green tests, and both the::warning::and the PR-body line say exactly that.Judgment calls
pip install pyyamltotest-refresh-reviewers.ymlso theyaml.safe_loadleg runs there.AGENTS.mdpins this repo to stdlib-only Python with one narrowly-scoped, hash-pinned exception, and widening that would be a change to a gated invariant rather than a test tweak — so the leg is keptskipUnless-guarded (the plan's stated alternative) and it will skip on CI. It is not load-bearing: the stdlib re-parse leg beside it reds on the same defect, confirmed by the mutation check below.read_committed_configkeeps the specifiedstr | Nonesignature, which cannot distinguish "missing" from "not UTF-8" at the call site. Both diagnostics are preserved anyway: the helper prints the specific decode error itself, and the caller's no-op reason names both causes.newline=""has no test, deliberately. Python only translates line endings on write on Windows, so any assertion about it is vacuous on theubuntu-latestrunner. The inline comment carries the reason instead.report.json's per-listchangedflag can readtruefor a list the rewrite skipped as unterminated (the refresher still computes what it would propose, which is the more useful thing to show a human next to the "put it on one line" note). The top-levelchanged, which gates whether a PR opens at all, is computed from the actual text diff and stays correct, andskipped_unterminatedin the same report disambiguates.Residual
[alice,/bob]parses to["alice"]— the continuation logins are dropped — in the Python generator and inassign-reviewers.yml's JS parser alike, so a consumer with that shape is already being routed on a truncated reviewer list at assignment time. This PR deliberately does not touch that: the parse half must stay in lockstep with the JS port, so fixing it is a two-port change with its own compatibility surface and is worth its own ticket. What this PR guarantees is only that the rewrite can no longer turn that mis-parse into a corrupted file.main. The upstream PR that addsTestSharedParserCorpustotests/test_generate.py(and the docstrings that go with it) is still open, so this branch is based onorigin/mainatcf7b8d9and will need a rebase once that lands; expect a textual conflict intests/test_generate.pyaround the class list, not a semantic one. Because that corpus does not exist yet, it could not be exercised as an acceptance artifact. Two substitutes were run instead: theconfighalf ofparse_reviewer_configwas diffed against the pre-change parser across 24 fixtures (LF and CRLF, including both unterminated shapes, empty flows and a]inside a trailing comment) and is identical in all 24, and.github/assign-reviewers/tests/assignment.test.cjs— the existing cross-port suite — is unmodified and green at 68/68. A reviewer should still re-run the corpus after the rebase.cf7b8d9before any code was written). Nothing in either artifact was assumed.test_block_rewrite_keeps_crlf,test_block_default_pool_keeps_crlf, andtest_line_endings_are_never_mixed_inon theblock/crlfandblock-pool/crlfcases). Reverting the unterminated-flow change alone reds 12 (the round-trip re-parse leg on all four unterminated cases, theyaml.safe_loadleg on the same four, and the four targeted tests). Reverting the bytes-read change alone reds the 2 read-helper tests. Full suite green at 76 tests; repo self-checkscheck_workflow_pins.pyandcheck_agents_md.py --root .both pass (the twocheck_agents_mdwarnings —AGENTS.mdat 155 lines, noCODEOWNERS— are pre-existing onmainand untouched here).Provenance
python3 -m unittest discover -s .github/refresh-reviewers/tests -p 'test_*.py' -v: 76 passed, 0 failed;node .github/assign-reviewers/tests/assignment.test.cjs: 68 passed, 0 failed (file unmodified);python3 .github/workflow-pins/check_workflow_pins.py: OK, 11 workflows, 0 violations;python3 .github/agents-md-integrity/check_agents_md.py --root .: passed with 2 pre-existing warnings; three mutation reverts each reding only their intended tests (4 / 12 / 2); two end-to-endmain()runs against throwaway git repos with the API stubbed, results quoted under Verified.pip install pyyamlwas not added to.github/workflows/test-refresh-reviewers.yml— see Judgment calls; theyaml.safe_loadleg skips on CI and the stdlib re-parse leg covers the same regression. Not rebased onto the still-open shared-parser-corpus PR — see Residual.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.