[PR #289] [MERGED] feat(linear-ticket): add an exempt-paths input so config-only PRs skip the gate #290

Closed
opened 2026-10-26 22:38:07 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/Comfy-Org/github-workflows/pull/289
Author: @mattmillerai
Created: 9/15/2026
Status: ✅ Merged
Merged: 9/15/2026
Merged by: @mattmillerai

Base: main ← Head: matt/be-14482-linear-ticket-exempt-paths


📝 Commits (2)

  • d0ac747 feat(linear-ticket): add an exempt-paths input so config-only PRs skip the gate
  • 1e70455 fix(linear-ticket): fail the path exemption closed on a changed-file list that does not match the PR (BE-14482)

📊 Changes

8 files changed (+794 additions, -13 deletions)

View changed files

📝 .github/workflows/linear-ticket.yml (+22 -2)
📝 README.md (+1 -1)
📝 docs/callers/linear-ticket.md (+78 -1)
📝 scripts/linear-ticket/README.md (+30 -0)
📝 scripts/linear-ticket/lib.py (+119 -0)
📝 scripts/linear-ticket/tests/test_lib.py (+125 -4)
📝 scripts/linear-ticket/tests/test_validate.py (+273 -3)
📝 scripts/linear-ticket/validate.py (+146 -2)

📄 Description

ELI-5

The "did you link a Linear ticket?" check has only two escape hatches today: a label someone applies by hand to one PR, or a list of author accounts that skip the check forever. Neither one fits a repo that gets a steady drip of ticket-less config-only chores — launch-gate flips, version syncs — opened by half a dozen different people. This adds a third hatch: give the workflow a list of path patterns, and a PR whose changed files all match is waived automatically.

The interesting part is where the exemption lives. The obvious fix — a paths-ignore: on the caller's signal workflow — is a trap, because Linear ticket is a commit status that repos eventually mark required. Filtering the signal means no status is ever published, and a required context that is never published doesn't pass by omission: GitHub leaves it pending forever, blocking exactly the PRs you were trying to let through. So the exemption is decided inside the validator and exits through the existing finish_exempt(), publishing a green status that names the reason.

Description

New optional exempt-paths input on linear-ticket.yml, plus the validator logic, tests and docs behind it.

  • Semantics. A PR is exempt only when every changed file matches at least one pattern, mirroring paths-ignore. One config file plus one Go file is not exempt.
  • Inert by default. Empty is the default, so every existing caller's behaviour is byte-identical with no caller-side edit — including making no extra API call (pinned by a test).
  • Short-circuits before the Linear query, like the label and actor hatches, so an exempt PR spends no Linear token budget and passes even while Linear is unreachable.
  • Zero changed files is NOT exempt. "Every file matches" is vacuously true over an empty list, and that list is reachable on an empty or fully-reverted branch.
  • Changed-file lookup uses GET /repos/{owner}/{repo}/pulls/{number}/files — GitHub-owned metadata, not the PR branch, so it stays inside the reusable's fork-safety constraints. Paginated at 100/page. A rename contributes both its new path and its previous_filename, so code cannot be moved into an exempted directory to buy the exemption.
  • Fails closed on an untrustworthy list. That endpoint caps at 3000 files, so a read at or past the cap — or any failed/malformed read — publishes a new changed_files_unavailable red status rather than declaring a partial list fully matched. "Malformed" also covers a returned length that disagrees with the PR payload's own changed_files count, and any entry that is not an object carrying string paths.
  • Pattern grammar is a deliberate subset of GitHub's: ** crosses /, * does not, everything else is literal, ! negation is rejected. Every limitation under-matches, and under-matching leaves the ticket requirement in force — the safe direction.
  • Malformed input fails the run (empty entry, duplicate, absolute, .. segment), the way a malformed team-keys already does.

Judgment calls worth a reviewer's attention

  1. changed_files_unavailable is a new failure category rather than reuse of infra_error. infra_error's copy says Linear could not be queried — a diagnosis this run never reached. The repo already treats that distinction as load-bearing (there is a test asserting infra_error's status description must not read "no linked Linear issue"), so a fifth category with its own headline, guidance and advisory tail seemed more in keeping than borrowing copy that lies. It routes through finish_fail and follows infra_error's exit-code contract exactly.

  2. >= rather than > at the 3000-file cap. GitHub documents the maximum but not what it does past it, so a PR sitting exactly at the ceiling is indistinguishable from a truncated one and is refused too. Fails in the safe direction (red, not silently exempt).

  3. An unreadable file list reds a PR that might have a perfectly good linked ticket. This is the acceptance criteria's explicit instruction and I implemented it as specified, but it is worth naming the cost: in a repo that sets exempt-paths, a >3000-file PR fails the check as an infrastructure error even if its ticket is linked and open. The alternative — falling through to the Linear query, since a >3000-file PR is definitionally not config-only — would be strictly friendlier, and is a defensible follow-up if the fail-closed posture ever bites. It affects opt-in callers only.

  4. Only the changed-file COUNT is logged, never the filenames. A git path may legally contain a newline, so echoing an author-controlled filename from this privileged job would let a branch forge ::error:: workflow commands in the run log. The cost is that "why isn't my config PR exempt?" is less self-diagnosing from the log alone.

  5. Stricter than normalize_team_keys on empty entries. team-keys tolerates a stray BE,,ENG; exempt-paths rejects it. The acceptance criteria call for it, and the rationale holds: this input is a merge-gate waiver, so it runs under exactly what the caller wrote or it does not run at all. Flagging it because it is a deliberate inconsistency with the neighbouring parser, not an oversight.

  6. Nothing stops exempt-paths: **, which turns the gate off while still looking configured. I did not add a guard — a maintainer writing that is explicitly opting out, and the label/actor hatches have no equivalent paternalism — but the caller doc now carries a warning callout.

Verification

No behavioural change reaches any existing caller: the input defaults to empty, and a test pins that an empty value never even looks up the changed files.

The negative-claim check applies to the 3000-file dead-end this diff adds, so I falsified it against the source rather than asserting it: GitHub's REST docs for the endpoint state verbatim "Responses include a maximum of 3000 files." They notably do not document overflow behaviour, which is the actual argument for failing closed — a partial read cannot be ruled out. Page size is 30 by default, hence the explicit per_page=100.

The matcher was also exercised by hand against realistic paths to confirm it does not leak across a sibling prefix (infrastructure/dynamicconfig-backup/x.yaml and Infrastructure/dynamicconfig/x.yaml both correctly do not match infrastructure/dynamicconfig/**).

Residual

  • The companion Comfy-Org/cloud caller PR is not included. The acceptance criteria call for a second PR in that repo bumping the caller's uses: SHA and workflows_ref: in lockstep and setting exempt-paths: infrastructure/dynamicconfig/**, verified by a config-only PR showing a green Linear ticket with the exemption reason. That is a different repository and is out of this PR's scope; it is also blocked until this merges, since both pins must point at a merged-upstream commit (the reusable's ancestry guard rejects anything else). Note the SHA half will happen on its own: bump-linear-ticket-callers.yml watches .github/workflows/linear-ticket.yml and scripts/linear-ticket/**, both of which this touches, so the fleet opens the bump PR on merge — what still needs a human is adding the exempt-paths: line to that caller and confirming the green status on a real config-only PR. Nothing in this PR has been exercised against a live PR, in cloud or anywhere else; verification is the hermetic suite plus the by-hand matcher run above.
  • An exempt PR gets a job summary and a green status naming the exemption, but no marker comment. The acceptance criteria ask for a marker comment too. finish_exempt() — which the ticket separately, and explicitly, requires this to reuse — deletes the marker comment rather than writing one, because the gate deliberately maintains exactly one comment and only for failures. Honouring the comment half would mean changing finish_exempt() for the existing exempt-label and exempt-actors paths as well, putting a new comment on every Dependabot PR in every consumer repo. I took the reuse instruction as the stronger of the two and left the comment behaviour alone; the exemption reason is still on the PR, in the status description beside the check name. Reversing that call is a small change if a maintainer disagrees.
  • require-open-issue is untouched, per the ticket's own out-of-scope list. The PR that surfaced this went red because a linked-but-Done ticket fails require-open-issue: true, not because no ticket was linked. A config-only PR is exempt under this change regardless of that setting, so the two are independent — but if the Done-ticket case deserves its own treatment, nothing here addresses it.
  • Unexercised artifacts. Every Comfy-Org/cloud PR the ticket cites as evidence (the config-only PR stream used to establish the authorship spread, and the workflow run that published the red status) sits in a private repository I cannot read from this environment, so the authorship-spread claim that motivates a path exemption over exempt-actors is taken from the ticket rather than re-measured. The corpus sizing the ticket did — ~15 recent config-only PRs — was likewise not re-swept, so I have no independent count of the portion this exemption would not cover.

Provenance

  • Authored by: agent-work loop
  • Verified: python3 -m unittest discover -s scripts/linear-ticket/tests -p 'test_*.py': 116 passed, 0 failed (6 added this round covering the changed-file count mismatch in both directions, the rename case that proves the count is compared against entries rather than expanded paths, and the three malformed-entry shapes); python3 -m py_compile scripts/linear-ticket/{lib,validate}.py: clean; python3 .github/workflow-pins/check_workflow_pins.py: OK, 11 workflows, none with a defaulted ref; python3 .github/agents-md-integrity/check_agents_md.py --root .: passed (2 pre-existing warnings, untouched by this diff). Two claims in the review round were checked empirically rather than asserted: the new count-mismatch guard was falsified for false positives against ten real PRs in this repo (changed_files vs. the files-endpoint length agreed 10/10, including a merge commit and PRs with renames), and the strengthened startup-order test was mutation-checked (it fails when main() is reordered to parse the event before validating EXEMPT_PATHS, and passes when restored). The --slurp review finding was refuted by running gh api --paginate at per_page=2 against this PR: four pages decode to one flat 8-element list, not None.
  • Deviations: two acceptance criteria not met, both named under ## Residual — the companion Comfy-Org/cloud caller PR (different repository, and blocked on this merging) and the marker comment on an exempt PR (incompatible with the finish_exempt() reuse the same ticket requires). No deviation in the review round: four of the five review findings were applied as filed; the --slurp one was declined on the empirical evidence above rather than changed.

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/Comfy-Org/github-workflows/pull/289 **Author:** [@mattmillerai](https://github.com/mattmillerai) **Created:** 9/15/2026 **Status:** ✅ Merged **Merged:** 9/15/2026 **Merged by:** [@mattmillerai](https://github.com/mattmillerai) **Base:** `main` ← **Head:** `matt/be-14482-linear-ticket-exempt-paths` --- ### 📝 Commits (2) - [`d0ac747`](https://github.com/Comfy-Org/github-workflows/commit/d0ac747db7c1b33fe67eed071cfab948bfe62d88) feat(linear-ticket): add an `exempt-paths` input so config-only PRs skip the gate - [`1e70455`](https://github.com/Comfy-Org/github-workflows/commit/1e70455afce08d683bb46f8a6872f94f5ffee432) fix(linear-ticket): fail the path exemption closed on a changed-file list that does not match the PR (BE-14482) ### 📊 Changes **8 files changed** (+794 additions, -13 deletions) <details> <summary>View changed files</summary> 📝 `.github/workflows/linear-ticket.yml` (+22 -2) 📝 `README.md` (+1 -1) 📝 `docs/callers/linear-ticket.md` (+78 -1) 📝 `scripts/linear-ticket/README.md` (+30 -0) 📝 `scripts/linear-ticket/lib.py` (+119 -0) 📝 `scripts/linear-ticket/tests/test_lib.py` (+125 -4) 📝 `scripts/linear-ticket/tests/test_validate.py` (+273 -3) 📝 `scripts/linear-ticket/validate.py` (+146 -2) </details> ### 📄 Description ## ELI-5 The "did you link a Linear ticket?" check has only two escape hatches today: a label someone applies by hand to one PR, or a list of author accounts that skip the check forever. Neither one fits a repo that gets a steady drip of ticket-less config-only chores — launch-gate flips, version syncs — opened by half a dozen different people. This adds a third hatch: give the workflow a list of path patterns, and a PR whose changed files *all* match is waived automatically. The interesting part is where the exemption lives. The obvious fix — a `paths-ignore:` on the caller's signal workflow — is a trap, because `Linear ticket` is a commit *status* that repos eventually mark **required**. Filtering the signal means no status is ever published, and a required context that is never published doesn't pass by omission: GitHub leaves it pending forever, blocking exactly the PRs you were trying to let through. So the exemption is decided inside the validator and exits through the existing `finish_exempt()`, publishing a **green** status that names the reason. ## Description New optional `exempt-paths` input on `linear-ticket.yml`, plus the validator logic, tests and docs behind it. - **Semantics.** A PR is exempt only when **every** changed file matches at least one pattern, mirroring `paths-ignore`. One config file plus one Go file is **not** exempt. - **Inert by default.** Empty is the default, so every existing caller's behaviour is byte-identical with no caller-side edit — including making no extra API call (pinned by a test). - **Short-circuits before the Linear query**, like the label and actor hatches, so an exempt PR spends no Linear token budget and passes even while Linear is unreachable. - **Zero changed files is NOT exempt.** "Every file matches" is vacuously true over an empty list, and that list is reachable on an empty or fully-reverted branch. - **Changed-file lookup** uses `GET /repos/{owner}/{repo}/pulls/{number}/files` — GitHub-owned metadata, not the PR branch, so it stays inside the reusable's fork-safety constraints. Paginated at 100/page. A **rename** contributes both its new path and its `previous_filename`, so code cannot be moved into an exempted directory to buy the exemption. - **Fails closed on an untrustworthy list.** That endpoint caps at 3000 files, so a read at or past the cap — or any failed/malformed read — publishes a new `changed_files_unavailable` red status rather than declaring a partial list fully matched. "Malformed" also covers a returned length that disagrees with the PR payload's own `changed_files` count, and any entry that is not an object carrying string paths. - **Pattern grammar is a deliberate subset of GitHub's**: `**` crosses `/`, `*` does not, everything else is literal, `!` negation is rejected. Every limitation under-matches, and under-matching leaves the ticket requirement in force — the safe direction. - **Malformed input fails the run** (empty entry, duplicate, absolute, `..` segment), the way a malformed `team-keys` already does. ## Judgment calls worth a reviewer's attention 1. **`changed_files_unavailable` is a new failure category rather than reuse of `infra_error`.** `infra_error`'s copy says Linear could not be queried — a diagnosis this run never reached. The repo already treats that distinction as load-bearing (there is a test asserting `infra_error`'s status description must not read "no linked Linear issue"), so a fifth category with its own headline, guidance and advisory tail seemed more in keeping than borrowing copy that lies. It routes through `finish_fail` and follows `infra_error`'s exit-code contract exactly. 2. **`>=` rather than `>` at the 3000-file cap.** GitHub documents the maximum but *not* what it does past it, so a PR sitting exactly at the ceiling is indistinguishable from a truncated one and is refused too. Fails in the safe direction (red, not silently exempt). 3. **An unreadable file list reds a PR that might have a perfectly good linked ticket.** This is the acceptance criteria's explicit instruction and I implemented it as specified, but it is worth naming the cost: in a repo that sets `exempt-paths`, a >3000-file PR fails the check as an infrastructure error even if its ticket is linked and open. The alternative — falling through to the Linear query, since a >3000-file PR is definitionally not config-only — would be strictly friendlier, and is a defensible follow-up if the fail-closed posture ever bites. It affects opt-in callers only. 4. **Only the changed-file COUNT is logged, never the filenames.** A git path may legally contain a newline, so echoing an author-controlled filename from this privileged job would let a branch forge `::error::` workflow commands in the run log. The cost is that "why isn't my config PR exempt?" is less self-diagnosing from the log alone. 5. **Stricter than `normalize_team_keys` on empty entries.** `team-keys` tolerates a stray `BE,,ENG`; `exempt-paths` rejects it. The acceptance criteria call for it, and the rationale holds: this input is a merge-gate waiver, so it runs under exactly what the caller wrote or it does not run at all. Flagging it because it is a deliberate inconsistency with the neighbouring parser, not an oversight. 6. **Nothing stops `exempt-paths: **`, which turns the gate off while still looking configured.** I did not add a guard — a maintainer writing that is explicitly opting out, and the label/actor hatches have no equivalent paternalism — but the caller doc now carries a warning callout. ## Verification No behavioural change reaches any existing caller: the input defaults to empty, and a test pins that an empty value never even looks up the changed files. The negative-claim check applies to the 3000-file dead-end this diff adds, so I falsified it against the source rather than asserting it: GitHub's REST docs for the endpoint state verbatim *"Responses include a maximum of 3000 files."* They notably do **not** document overflow behaviour, which is the actual argument for failing closed — a partial read cannot be ruled out. Page size is 30 by default, hence the explicit `per_page=100`. The matcher was also exercised by hand against realistic paths to confirm it does not leak across a sibling prefix (`infrastructure/dynamicconfig-backup/x.yaml` and `Infrastructure/dynamicconfig/x.yaml` both correctly do **not** match `infrastructure/dynamicconfig/**`). ## Residual - **The companion `Comfy-Org/cloud` caller PR is not included.** The acceptance criteria call for a second PR in that repo bumping the caller's `uses:` SHA and `workflows_ref:` in lockstep and setting `exempt-paths: infrastructure/dynamicconfig/**`, verified by a config-only PR showing a green `Linear ticket` with the exemption reason. That is a different repository and is out of this PR's scope; it is also blocked until this merges, since both pins must point at a merged-upstream commit (the reusable's ancestry guard rejects anything else). Note the SHA half will happen on its own: `bump-linear-ticket-callers.yml` watches `.github/workflows/linear-ticket.yml` and `scripts/linear-ticket/**`, both of which this touches, so the fleet opens the bump PR on merge — what still needs a human is adding the `exempt-paths:` line to that caller and confirming the green status on a real config-only PR. **Nothing in this PR has been exercised against a live PR, in cloud or anywhere else**; verification is the hermetic suite plus the by-hand matcher run above. - **An exempt PR gets a job summary and a green status naming the exemption, but no marker *comment*.** The acceptance criteria ask for a marker comment too. `finish_exempt()` — which the ticket separately, and explicitly, requires this to reuse — *deletes* the marker comment rather than writing one, because the gate deliberately maintains exactly one comment and only for failures. Honouring the comment half would mean changing `finish_exempt()` for the existing `exempt-label` and `exempt-actors` paths as well, putting a new comment on every Dependabot PR in every consumer repo. I took the reuse instruction as the stronger of the two and left the comment behaviour alone; the exemption reason is still on the PR, in the status description beside the check name. Reversing that call is a small change if a maintainer disagrees. - **`require-open-issue` is untouched, per the ticket's own out-of-scope list.** The PR that surfaced this went red because a linked-but-Done ticket fails `require-open-issue: true`, not because no ticket was linked. A config-only PR is exempt under this change regardless of that setting, so the two are independent — but if the Done-ticket case deserves its own treatment, nothing here addresses it. - **Unexercised artifacts.** Every `Comfy-Org/cloud` PR the ticket cites as evidence (the config-only PR stream used to establish the authorship spread, and the workflow run that published the red status) sits in a private repository I cannot read from this environment, so the authorship-spread claim that motivates a path exemption over `exempt-actors` is taken from the ticket rather than re-measured. The corpus sizing the ticket did — ~15 recent config-only PRs — was likewise not re-swept, so I have no independent count of the portion this exemption would *not* cover. ## Provenance - **Authored by:** agent-work loop - **Verified:** `python3 -m unittest discover -s scripts/linear-ticket/tests -p 'test_*.py'`: **116 passed, 0 failed** (6 added this round covering the changed-file count mismatch in both directions, the rename case that proves the count is compared against entries rather than expanded paths, and the three malformed-entry shapes); `python3 -m py_compile scripts/linear-ticket/{lib,validate}.py`: clean; `python3 .github/workflow-pins/check_workflow_pins.py`: OK, 11 workflows, none with a defaulted ref; `python3 .github/agents-md-integrity/check_agents_md.py --root .`: passed (2 pre-existing warnings, untouched by this diff). Two claims in the review round were checked empirically rather than asserted: the new count-mismatch guard was falsified for false positives against ten real PRs in this repo (`changed_files` vs. the files-endpoint length agreed 10/10, including a merge commit and PRs with renames), and the strengthened startup-order test was mutation-checked (it fails when `main()` is reordered to parse the event before validating `EXEMPT_PATHS`, and passes when restored). The `--slurp` review finding was refuted by running `gh api --paginate` at `per_page=2` against this PR: four pages decode to one flat 8-element list, not `None`. - **Deviations:** two acceptance criteria not met, both named under `## Residual` — the companion `Comfy-Org/cloud` caller PR (different repository, and blocked on this merging) and the marker comment on an exempt PR (incompatible with the `finish_exempt()` reuse the same ticket requires). No deviation in the review round: four of the five review findings were applied as filed; the `--slurp` one was declined on the empirical evidence above rather than changed. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-26 22:38:07 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Comfy-Org/github-workflows#290
No description provided.