fix(ci): harden PR→issue linking gate against shell injection and missed refs#35761
fix(ci): harden PR→issue linking gate against shell injection and missed refs#35761nollymar wants to merge 12 commits into
Conversation
…sed refs The "PR opened" check silently failed on PRs whose body contained shell metacharacters (backticks, `$var(`, etc.), exiting before the linkage logic ran. Move `inputs.*` into a job-level `env:` block so they are no longer template-interpolated into bash sources. Add a GraphQL `closingIssuesReferences` fallback for refs the body regex misses (e.g. markdown-linked `fixes [#123](url)`). Re-evaluate the check on `edited`, `synchronize`, and `reopened` so a once-broken PR can recover after a body edit or new push instead of being stuck on the stale `opened` run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Claude finished @nollymar's task in 4m 57s —— View job PR ReviewThe hardening is solid — env hoisting is consistent, the heredoc-delimiter randomization closes the GH_OUTPUT injection class, markdown-link escaping in the bot comment looks right, and the gate/side-effect ordering is mostly correct. A few things worth a second look: Findings1. Priority inversion between body-regex and 2. 3. 4. 5. GraphQL 6. Pre-existing: 7. Unchecked test-plan item is the most important one to verify Nits
Summary
The shell-injection hardening is the highest-value change and looks correct. The two findings worth addressing before merge are #1 (priority ordering of GraphQL vs body regex) and #2 (silent failure on |
The widened triggers (synchronize/edited) caused the "Add failure comment to PR" step to repost the ❌ Issue Linking Required comment on every push to an unlinked PR. Mirror the lookup used by the Remove step and skip the POST if a matching bot comment already exists. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Skip the linking job on fork-sourced PRs. The pull_request token is read-only for forks, so the PATCH/POST/DELETE calls would 403 on every run — now amplified by synchronize/edited triggers. - Declare explicit pull-requests/issues write permissions on the reusable workflow so the dependency is visible and survives future default permission tightening. - Surface GraphQL fallback errors via ::warning:: instead of silently swallowing stderr with 2>/dev/null; users debugging a misleading "no issue found" failure can now see the underlying call's error. - Guard the fallback against an empty pr_number (malformed PR_URL) so it skips the GraphQL call with a clear warning instead of issuing an invalid Int query. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empty commit to exercise the synchronize trigger after the linking-gate hardening changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a second gate after the issue-linking check: if the resolved issue has no 'Team : *' label, the workflow fails and posts a distinct '❌ Linked Issue Needs Team Label' comment on the PR. The comment is dedup-guarded and auto-removed on the next successful run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the linked-issue comment already lists the current PR, the "keeping existing list" branch built the GITHUB_OUTPUT heredoc with printf and no trailing newline, so the closing EOF was appended to the last PR line instead of being on its own. The runner then errored with "Matching delimiter not found 'EOF'" and skipped the rest of the job. Only exposed now that synchronize/edited triggers cause the workflow to re-run against issues whose PR-list comment already exists. Add the missing trailing \n. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empty commit to test the new "Linked Issue Needs Team Label" gate after removing the Team : Scout label from issue #35794. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Move the issue-link-success cleanup, team-label validation, and the validation-cleanup steps to run before the side-effect steps (issue PR-list comment, PR body PATCH). A PR whose linked issue is missing a team label no longer mutates remote state before failing. - Surface gh CLI errors in the team-label step distinctly from "no team label" so transient API failures, deleted issues, or scope problems do not masquerade as missing-label and start a fix cycle that never works. - Replace the literal `EOF` heredoc delimiter on $GITHUB_OUTPUT multiline values with a random `ghadelimiter_<random>` so user-supplied PR titles containing a literal "EOF" line cannot terminate the output block early or leak subsequent shell content into the value. - Escape `\`, `[`, and `]` in PR titles before embedding them in the markdown link of the linked-issue PR-list comment, so titles like 'Fix bug](https://evil)' cannot redirect the bot-rendered link. - Switch the URL-presence check on the existing PR list from `grep -q` to `grep -qF` so `.` and `/` in the URL are not interpreted as regex. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Hoist all step-output values into per-step env: blocks instead of
inlining them as ${{ steps.X.outputs.Y }} into run: scripts. Today
these values are all numeric or trusted-shape, but the env-hoist
invariant means a future widening (a label name, a title) cannot
silently re-introduce the bash injection the PR was created to close.
- Add concurrency: { group: link-issue-<pr_number>, cancel-in-progress:
false } to issue_open-pr.yml so synchronize + the edited fired by the
workflow's own body PATCH cannot race on the PATCH/POST/DELETE calls.
- Anchor the issue-reference dedup in Link PR to issue with a
non-digit-boundary regex so the body containing "closes #100" is no
longer considered to already reference #10.
- Tighten the PR-list URL dedup in Check if comment already exists to
match against the literal markdown-link terminator "($pr_url)" so a
URL ending in /pull/10 cannot substring-match an existing entry for
/pull/100.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empty commit to exercise the synchronize trigger against the env-hoisted, concurrency-guarded, anchor-matched version of the linking workflow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A PR body containing 'fixes [#123](url)' was losing to a branch named '456-some-feature' because the GraphQL closingIssuesReferences lookup ran only AFTER branch-name extraction. The body was effectively unreadable for markdown-linked refs — the exact case the PR claimed to fix. Reorder priority so author intent in the PR body (whether plain or markdown-linked) outranks the branch-name heuristic: 1) check_existing_issues: timeline / plain-keyword regex 2) closingIssuesReferences GraphQL: catches markdown-linked refs 3) Branch name extraction: last-resort heuristic Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
peter-evans/create-or-update-comment defaults to edit-mode: append. On every re-run, the workflow was appending the full "PRs linked to this issue" block to the existing bot comment instead of replacing it — issue #35794's comment accumulated 10 stacked copies of the same block after the synchronize/edited triggers started firing on every push. Set edit-mode: replace so each run produces a fresh single block. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes #35794
Summary
Hardens the PR-to-issue linking gate (
.github/workflows/issue_open-pr.yml→issue_comp_link-issue-to-pr.yml) and adds a second gate on the linked issue's metadata.Linking-gate fixes (the original three)
inputs.pr_body,pr_title,pr_author,pr_url,pr_branch,pr_mergedwere template-substituted by GitHub Actions directly into the bashrun:scripts. PRs whose body contained backticks,$var(...), or unbalanced parens (e.g. feat(tiptap): convert Story Block content to Markdown (#35727) #35728's$markdownTool.blockToMarkdown(json)) caused a bash syntax error on the firstDebug workflow inputsstep → entire job exited code 2 →Add failure comment to PRwas skipped (its condition needsfailure_detected=true, never set) → PR appeared unchecked. Hoisted all inputs into a job-levelenv:block and reference them as$ENV_VARin shell, so user-supplied values are treated as data, not code.(close[ds]?|fix(e[ds])?|resolve[ds]?)(:)?\s+#([0-9]+)requires a literal#immediately after the keyword and misses GitHub's other valid formfixes [#123](url)(as in feat(dotAI): Dot AI LangChain4J - Amazon Bedrock #35242). Added a GraphQLclosingIssuesReferenceslookup as the 4th and final fallback inDetermine final issue number.pull_request: [opened], so editing the body or pushing new commits never re-evaluated the gate. A once-broken PR stayed broken even after the author fixed the link. Broadened triggers to[opened, edited, synchronize, reopened]. Existing idempotency (grep -q "#$issue_number"before body patch,sort -uon PR-list comments,Remove failure commentstep) handles the extra runs; the failure-comment step is now dedup-guarded.New second gate — linked issue must have a team label
After the link is resolved, the workflow validates that the linked issue carries a
Team : *label and fails with a distinct❌ Linked Issue Needs Team LabelPR comment if it doesn't. The validation step runs before the side-effect steps (PR-list comment on the issue, PR body PATCH), so a PR whose linked issue is missing a team label does not mutate any remote state before failing.Review-driven follow-ups (commit
312ced3e)Link PR to issueand the issue's PR-list comment no longer run when validation fails).gh issue viewerrors in the team-label step are surfaced distinctly instead of being collapsed into "no team label."$GITHUB_OUTPUTheredocs use a randomghadelimiter_<random>instead of literalEOFto prevent delimiter collision with user-controlled content.\,[,]) inPR_TITLEare escaped before being embedded in the bot's linked-issue comment.grep -qtogrep -qF.Operational notes
if: github.event.pull_request.head.repo.full_name == github.repositorybecause thepull_requestGITHUB_TOKENis read-only on forks. If this gate ever becomes a required check, fork contributors will be unchecked by design — flag for follow-up at that point.permissions: { pull-requests: write, issues: write }declared on the reusable workflow.main).Test plan
Debug workflow inputswhen the PR body contains backticks /$var(...)(this PR's own description is the test case).synchronizere-fires the check on a new push (empty commit2e6b0ed0validated this).❌ Linked Issue Needs Team Labelwhen the linked issue has noTeam : *label.Team : *is restored on the issue.Create new comment,Link PR to issue) are skipped on a run where team-label validation fails.🤖 Generated with Claude Code