mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #313] [MERGED] fix(reviewers): warn when an unrecognised column-0 line ends a default_pool/rules block, on both ports #315
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#315
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/313
Author: @mattmillerai
Created: 9/18/2026
Status: ✅ Merged
Merged: 9/19/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-15377-reviewers-parser-unrecognised-key-warning📝 Commits (10+)
1ab8b60test(reviewers): drive both reviewers.yml parsers from one shared corpus02f501ctest(reviewers): bracket the whole script scalar, frame nested spans, pin multi-line flow62980a5fix(reviewers): converge both reviewers.yml parsers on last-wins duplicate keys, s-white comments and a leading BOM3559301fix(reviewers): repoint locs on a shadowed default_pool, anchor the flow span, scope the last-wins claimc1e2a92fix(reviewers): trim tokens with one explicit space-and-tab set in both parsers2a8c65afix(reviewers): keep the rewriter, the rule-key regex and the config read in step with s-white trimming21ce8bffix(reviewers): warn when an unrecognised column-0 line ends a default_pool/rules block, on both portsba664a5fix(reviewers): warn only where a column-0 line breaks something, and escape the annotationbc66d13Merge remote-tracking branch 'origin/main' into matt/be-15312-reviewers-parser-convergence3140808fix(reviewers): trim tokens with one explicit space-and-tab set in both reviewers.yml parsers (#312)📊 Changes
8 files changed (+793 additions, -28 deletions)
View changed files
📝
.github/assign-reviewers/README.md(+15 -1)📝
.github/assign-reviewers/parser-corpus.json(+41 -3)📝
.github/assign-reviewers/tests/assignment.test.cjs(+196 -0)📝
.github/refresh-reviewers/generate.py(+170 -5)📝
.github/refresh-reviewers/tests/test_generate.py(+234 -6)📝
.github/workflows/assign-reviewers.yml(+94 -9)📝
docs/callers/assign-reviewers.md(+31 -4)📝
docs/callers/refresh-reviewers.md(+12 -0)📄 Description
ELI-5
Both copies of the
reviewers.ymlparser stop reading a list the moment they hit a line that starts at column 0 and isn'tdefault_pool:orrules:. A line containing nothing but one non-breaking space is such a line — it looks empty, isn't, and silently throws away every owner or rule below it. Now each parser says so: one warning naming the line number and showing the character as\u00a0.It warns only where something actually broke, which is the part the review cycle corrected. A
---/...document marker, aversion:key before the first block, and a stray key between two complete blocks all leave every laterdefault_pool:/rules:parsing, so they are silent — the first version annotated them anyway, asserting the block above was ended and the rules below were dropped, on documents whose parse is complete. Narrowing the trigger also removed a flood: a tab-indented or zero-indented config used to warn once per line and spend GitHub's ~10-annotation budget on symptoms.Why not just treat those lines as blank
Because real YAML doesn't. PyYAML raises
ScannerErroron an NBSP-at-column-0 document andReaderErroron a form feed, so widening the blank-line test would make these parsers accept documents YAML rejects — a new divergence in place of the old one. Making the truncation visible is the fix that leaves parse results alone, which is why the corpusexpectedvalues in this diff are untouched.What changed
.github/refresh-reviewers/generate.py(parse_reviewer_config) — at the outer-loop fallthrough, a non-empty line with_indent_of(raw) == 0that matched neither key prints one::warning::, on two arms:default_pool:/rules:block. Tracked by anended_blockflag the scans set at theirbreakand the next outer iteration consumes; sound because both scansbreakon a non-blank column-0 line without advancing the cursor, so the next iteration is guaranteed to be the terminating line.<BOM><BOM>default_pool:and<NEL>rules:, where the stray character is not indentation, and (new here)rules:v2:/default_pool:[alice].Everything else at column 0 is silent, including
---/.... Indented fallthrough lines stay silent as before: they're the orphaned tail of an already-reported truncation, and one warning per orphan would bury the one that explains them._is_keyreplacesstartswithon both ports. A bare prefix test claims a different key that shares the prefix:yaml.safe_load("rules:v2:\n - paths: [a]")is{"rules:v2": [...]}, a key namedrules:v2, yet the prefix test parsed its children as live routing rules — silently honouring an unsupported key. Likewiseyaml.safe_load("default_pool:[alice]")is the plain stringdefault_pool:[alice], because YAML readskey:as a mapping only with s-white or a line end after the colon. Both ports now require that, so both shapes fall through to the near-miss warning instead. This is the one parse change in the PR (default_pool:[alice]no longer yields a pool) — deliberate, YAML-correct, loud, and corpus-pinned; see Residual._wc_escapeingenerate.py.::warning::was built by hand with no workflow-command escaping, so a%in a config token (printable ASCII, so_show_invisibleleft it) decoded into a mangled multi-line annotation, and a CR/LF in the caller-configuredreviewer_config_path— interpolated raw — could start a second runner command.%→%25, CR→%0D, LF→%0Aover the whole message, path prefix included, applied to the duplicate-key warning too. Same three characters and same order as@actions/core'sescapeData, which is whatcore.warningalready does for the JS port..github/workflows/assign-reviewers.yml(parseReviewerConfig, insidePARITY-HARNESS:config-parser) — the same conditions, same message text, viacore.warning(which escapes for itself).showInvisiblemoved up into the parity region. This is the one non-obvious edit and the riskiest line in the diff. The plan said to reuse the existing helper; it was declared at line 639 whileparseReviewerConfigis called at line 598. Aconstsits in the temporal dead zone until its own line executes, so a parser reaching for it would have thrownReferenceErrorat run time on any config with a stray column-0 key — i.e. it would have converted a silent truncation into a hard routing failure. It now lives above the parser, inside the region, which also makes it reachable from the extracted span the corpus harness executes. Its other caller (the invalid-configured-login warning) is unchanged. Note the pre-existing comment at that site already flagged exactly this trap for theescapehelper declared further down — the hazard was documented, just not upstream of this change._show_invisibleingenerate.py— the Python port of it, so both ports render the escape\u00a0identically rather thanascii()'s'\xa0'. See Deviations. It now emits a surrogate pair above the BMP:"\\u%04x" % ord(c)treats 4 as a minimum width, so U+1F600 rendered as the five-digit\u1f600— not a valid\uescape, and ambiguous — while JS (nouflag on its regex) walks code units and emits\ud83d\ude00. The helper escapes the whole token, so any emoji in a stray line or a configured login diverged.Both parsers take the config path (
source_path/ the existingsourcePath), so the annotation names the file a caller actually configured viareviewer_config_pathinstead of hardcodingreviewers.yml.Docs —
.github/assign-reviewers/README.md,docs/callers/assign-reviewers.md(now quoting the message a consumer will see) anddocs/callers/refresh-reviewers.md(the drift run emits it too, and matters more there: the generator rewrites the truncated list it parsed and leaves the orphaned items below the stray line where they are).Tests
.github/refresh-reviewers/tests/test_generate.py— NBSP-ends-default_pooland NBSP-ends-ruleseach warn exactly once and parse unchanged; the indented NBSP document is silent; an unknownowners:key at column 0 warns once; both warnings carry the configured path when given and noNone:when not; a comments-and-blanks document of the shape every live consumer config has is silent..github/assign-reviewers/tests/assignment.test.cjs— the same cases throughhelperWarnings, asserting both the\u00a0escape and the literalU+00A0in the message's own prose, plus two full-script tests that run the whole shippedscript:rather than the extracted region. Those exist specifically to catch the TDZ hazard above: they're the only thing that provesshowInvisibleresolves at both call sites, and the invalid-login path had no test at all before this PR, so the move would otherwise have been unverified where it was riskiest.assertEqual(counts, CORPUS_WARNINGS); the JS side gained the matchingevery CORPUS_WARNINGS key names a corpus case that still exists, so a renamed or deleted case can no longer silently retire its own assertion.test_document_markers_are_silent,test_metadata_key_before_any_block_is_silent,test_non_yaml_file_is_silent,test_zero_indented_block_sequence_warns_once_not_per_item, and the near-miss trio (test_invisible_prefix_before_a_key_warns_as_a_near_miss, which also asserts a single leading BOM stays silent because the parser strips it;test_prefix_sharing_key_is_not_honoured_and_warns;test_no_space_after_the_colon_is_not_a_key). JS mirrors for each.test_workflow_command_characters_are_escapedparses a config whose stray line is100%0Adonewith asource_pathcontaining a newline, then asserts the output is a single physical line containinga%0Ab.yml:and(100%250Adone)— i.e. the runner decodes back to literal text rather than splitting the line or starting a second command.TestShowInvisiblecovers all three bands (BMP, printable ASCII, astral), and the JS suite asserts the surrogate pair from its side so a lateruflag there cannot silently reopen the divergence.Corpus sweep — including the part not being changed
The corpus is now 41 documents (37 before this PR), and the sweep is asserted exhaustively in both suites. 10 warn; the remaining 31 are silent, which is the number that matters: it's what says this doesn't fire on well-formed documents.
The three duplicate-
default_poolcases keep their one warning each. Of the seven unrecognised-key warnings:two leading BOMs: only one is stripped<BOM>default_pool: [alice]— unrecognised precisely because the second BOM is not indentation, which is the failure that case exists to pin. No block is open on line 1, so a pure block-termination test would have dropped thisU+0085 before a top-level key is not indentationa key sharing a prefix with \rules:` is not the `rules:` key`rules:v2:used to be silently honouredno space after the colon is a plain scalar, not a keydefault_pool:[alice]used to be silently accepted as a flow poola U+00A0-only line at column 0 …casesan unknown top-level key at column 0 ends the blockTwo cases that warned in the first version and are now silent, both correctly:
indented decoys and unknown top-level keys are ignoredversion: 1andnotes:sit before the first block, so they end nothing and drop nothing — yet the annotation asserted both, on a document whose expected parse is completeYAML document markers are not unrecognised keys(new)---/...are syntax, not keys; yamllint's defaultdocument-startrule requires the leading---, so a conformant config has oneAcceptance
default_pool:\n - alice\n<U+00A0>\n - bob\nparses to['alice']on both ports, with exactly one warning naming line 3 and rendering the character. Verified against the shipped JS by executing the region out of the workflow file, and against the Python port directly. Message text is byte-identical across ports:and the near-miss arm:
Measured volume on the flood shapes, before → after: zero-indented block sequence 5 → 1, tab-indented config 6 → 2,
reviewer_config_pathaimed at a non-YAML file N → 0..github/reviewers.ymlfiles, on both ports — the two org consumers plus this repo's own. Exercised read-only against each clone'sorigin/mainblob (git show, no checkout, no mutation); parsed pool/rule counts came back non-zero in every case, so the runs were not vacuous. The two org consumers are private repos, so they are not named here and no content from them appears in this PR. That check predates the review-cycle commit and was not re-run against it — see Residual.Falsification of the capability claim
The warning asserts "only
default_pool:andrules:are read", which denies a capability, so it was checked rather than assumed: both parsers match exactly those two strings at column 0 and nothing else; the corpus caseindented decoys and unknown top-level keys are ignoredalready pinned that aversion:key is ignored; and every other knob (num_reviewers, exclusions, the load cap) comes from workflow inputs and repo variables, not from this file. No third top-level key is read by either port, so the claim holds.Residual
reviewers.yml; a consumer who ignores the annotation still gets silently-dropped owners. That is the deliberate never-fail posture this shares with the duplicate-key and invalid-login warnings, not an oversight — but it does mean the fix depends on someone reading run annotations._is_keytightening means a consumer whosereviewers.ymlwritesdefault_pool:[alice]with no space after the colon gets an empty pool where they previously got a pool — loudly warned about, but a behaviour change. The earlier zero-warning sweep over the three live configs predates this commit and covered the old parser; the two org consumers are private and unreadable from this sandbox, so it could not be repeated. It is a narrow shape (YAML itself reads it as a scalar, so no YAML-aware tool would have produced it) and this repo's own config does not use it, but worth confirming against the two org configs before merge — it is the only input in the PR whose parse result moves.mainversus post-#312) was re-derived here from the code and the corpus rather than taken on trust; it agreed. Its worker transcript is a local file on another host and likewise unread. The two private org consumer configs were read (see Acceptance) but cannot be quoted here.Provenance
origin/main(PR #311's parser convergence: last-wins duplicate keys, s-white comments, leading-BOM stripping) to resolve a real merge conflict across all 7 parser/test/doc files, re-ran both suites on the merged tree:python3 -m unittest discover -s .github/refresh-reviewers/tests -p 'test_*.py': 126 tests, 0 failures.node --test .github/assign-reviewers/tests/*.test.cjs: 159 tests, 0 fail.check_workflow_pins.py: OK, 156 pinned refs, 0 defaults, every ref checkout guarded.check_agents_md.py --root .: passed (2 pre-existing warnings, unrelated).check-org-repo-literals.sh: OK. The merge combined this PR's unrecognised-key warning logic with main's independently-landedwarnConfiguredLoginbroadening (JS port) without dropping either side's tests. Not green:.github/coderabbit-configfails one test (test_the_two_halves_partition_the_objects_that_declare_properties,105 != 104) — reproduced identically onorigin/main, pre-existing, untouched by this PR, and path-filtered out of its CI. Not re-run: the live-consumer config sweep (private repos, unreadable from here) — see Residual.ascii(line)on the Python side; it renders NBSP as'\xa0', which does not contain00a0and so misses the acceptance criterion that the character be findable asU+00A0— a_show_invisibleport of the JS helper is used instead, which also gives the two ports identical message text. (2) The plan said to reuse the existing JSshowInvisiblein place; done, but its declaration had to move up into the parity region, because as written it would have thrown a TDZReferenceError(details above). (3) The plan described appending to thewhyof "the threeU+00A0-only linecases" the fact that both ports now warn; one of those three is the INDENTED case, which correctly does not warn, so its note records it as the negative control for the warning instead. (4) The plan specified the warning with no carve-outs; the review cycle established that firing on every column-0 fallthrough both overclaimed (the message asserted a block was ended and rules dropped on documents that parse completely) and flooded GitHub's ~10-annotation budget, so the trigger narrowed to two arms with---/...carved out. That is a deliberate departure from the plan, not an omission. (5) Wider than asked, in three places the review cycle justified: the pre-existing Python duplicate-key warning picks up the new path prefix and the new workflow-command escaping, so the function's two warnings are consistent and match the JS port; and_is_keychanges what parses (default_pool:[alice],rules:v2:), which the original scope explicitly excluded — taken because the PR's own warning was the thing failing to catch it, and flagged in Residual as the one input whose parse result moves.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.