Skip to content

fix(ci): harden PR→issue linking gate against shell injection and missed refs#35761

Open
nollymar wants to merge 3 commits into
mainfrom
fix/pr-issue-linking-gate-hardening
Open

fix(ci): harden PR→issue linking gate against shell injection and missed refs#35761
nollymar wants to merge 3 commits into
mainfrom
fix/pr-issue-linking-gate-hardening

Conversation

@nollymar
Copy link
Copy Markdown
Member

Summary

Three fixes to issue_open-pr.yml and the reusable issue_comp_link-issue-to-pr.yml:

  • Shell injection / template interpolation. inputs.pr_body, pr_title, pr_author, pr_url, pr_branch, pr_merged were template-substituted by GitHub Actions directly into the bash run: 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 very first Debug workflow inputs step → entire job exits with code 2 → Add failure comment to PR is skipped (its condition requires failure_detected=true, never set) → PR appears unchecked. Hoisted all inputs into a job-level env: block and reference them as $ENV_VAR in shell, so user-supplied values are treated as data, not code.
  • Missed markdown-link refs. The regex (close[ds]?|fix(e[ds])?|resolve[ds]?)(:)?\s+#([0-9]+) requires a literal # immediately after the keyword and misses GitHub's other valid form fixes [#123](url) (as in feat(dotAI): Dot AI LangChain4J - Amazon Bedrock #35242). Added a GraphQL closingIssuesReferences lookup as the 4th and final fallback in Determine final issue number — GitHub's own parser handles markdown-linked refs.
  • Stale-on-open. The workflow only triggered on 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 -u on PR-list comments, Remove failure comment step) handles the extra runs cleanly.

The check remains advisory (not in branch-protection required checks on main). This PR does not change that.

Test plan

  • CI runs the modified Link issue to PR workflow on this PR itself. Confirm no shell error in Debug workflow inputs (the PR body contains backticks and $markdownTool.blockToMarkdown(json)-style text).
  • Verify the GraphQL fallback step executes only when neither the body regex nor the branch-name extraction succeeds.
  • Push an empty commit after merge to confirm synchronize re-fires the check.

🤖 Generated with Claude Code

…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>
@github-actions
Copy link
Copy Markdown
Contributor

❌ Issue Linking Required

This PR could not be linked to an issue. All PRs must be linked to an issue for tracking purposes.

How to fix this:

Option 1: Add keyword to PR body (Recommended - auto-removes this comment)
Edit this PR description and add one of these lines:

  • This PR fixes #123 or Fixes: #123

  • This PR closes #123 or Closes: #123

  • This PR resolves #123 or Resolves: #123

  • Other supported keywords: fix, fixed, close, closed, resolve, resolved
    Option 2: Link via GitHub UI (Note: won't clear the failed check)

  1. Go to the PR → Development section (right sidebar)

  2. Click "Link issue" and select an existing issue

  3. Push a new commit or re-run the workflow to clear the failed check
    Option 3: Use branch naming
    Create a new branch with one of these patterns:

  • 123-feature-description (number at start)

  • issue-123-feature-description (issue-number at start)

  • feature-issue-123 (issue-number anywhere)

Why is this required?

Issue linking ensures proper tracking, documentation, and helps maintain project history. It connects your code changes to the problem they solve.---

This comment was automatically generated by the issue linking workflow

@github-actions github-actions Bot added the Area : CI/CD PR changes GitHub Actions/workflows label May 19, 2026
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 19, 2026

Claude finished @nollymar's task in 2m 24s —— View job


PR Review

  • Read changed workflow files
  • Verify shell injection hardening
  • Verify GraphQL fallback logic
  • Check trigger expansion and idempotency
  • Post review

The three fixes are coherent and the approach is sound. A few findings worth a second look:


1. Latent substring bug now exposed by broader triggers (medium)

.github/workflows/issue_comp_link-issue-to-pr.yml:282

if ! echo "$current_body" | grep -q "#$issue_number"; then

This was already in the file, but the synchronize / edited triggers add new ways for it to misfire. grep -q "#12" matches #1234, #125, #12_anything. So if a PR's final_issue_number=12 and the body legitimately mentions another issue like #1234, the "This PR fixes: #12" line will never be appended — the gate then passes for the wrong reason on future runs (Development-section/closingIssuesReferences may catch it, or it may not).

Suggest a word-boundaried match:

if ! grep -qE "(^|[^0-9])#${issue_number}([^0-9]|$)" <<<"$current_body"; then

2. pull_request: edited is noisier than the PR body suggests (low)

.github/workflows/issue_open-pr.yml:5

edited fires on title, base ref, and body changes — not just body. Every retitle (including the bot's own "Add Issue to PR" PR-body PATCH? no, body edits by the workflow token don't re-fire pull_request, but human title edits do) will re-run the whole pipeline. The runs are idempotent so nothing breaks, but it's a meaningful uptick in workflow runs and API calls per PR. Consider gating with a paths/if: on github.event.changes.body for the edited case if cost matters; otherwise the comment in the PR description should just acknowledge this.

3. GraphQL fallback duplicates the timeline check for UI-linked issues (low, cosmetic)

.github/workflows/issue_comp_link-issue-to-pr.yml:147-152

closingIssuesReferences includes UI-linked issues and keyword-parsed issues from the body. The first step (Check if PR already has linked issues) already catches UI-linked via timeline connected events. So the GraphQL fallback is doing double-duty — that's fine, just note that the fallback effectively supersedes the timeline check; you could simplify long-term by replacing both with a single closingIssuesReferences call at the top of the flow.

4. Empty gh api graphql stderr cleanup (nit)

.github/workflows/issue_comp_link-issue-to-pr.yml:146-157

mktemp+cat+rm -f works, but a simpler form avoids the temp file:

if ! closing_issue=$(gh api graphql ... --jq '...' 2>&1); then
  echo "::warning::closingIssuesReferences GraphQL fallback failed: $closing_issue"
  closing_issue=""
fi

Just a style nit — current version is correct.

5. pr_number=$(echo "$PR_URL" | grep -o '[0-9]*$') (pre-existing, not introduced here)

[0-9]*$ with -o can match an empty trailing position on some grep builds. Prefer [0-9][0-9]*$ or grep -oE '[0-9]+$'. The new GraphQL branch already guards with [[ -z "$pr_number" ]], so this is defensive only.


Things that look right

  • Hoisting inputs.* into env: and dereferencing as $ENV_VAR is the correct fix for the shell-injection class. Verified all ${{ inputs.* }} references inside run: blocks are gone — every remaining ${{ ... }} inside run: is either ${{ github.token }}, ${{ github.repository }}, or a steps.*.outputs.* value derived from internal logic (PR number, issue number) — all safe.
  • The Skip fork PRs guard via if: github.event.pull_request.head.repo.full_name == github.repository correctly avoids the 403-on-fork-token issue; advisory check just won't run for forks, which matches existing behavior.
  • Explicit permissions: block (least-privilege) is a good add.
  • Failure-comment dedupe via existing-comment lookup is correct and necessary given the new trigger set.
  • printf "%s" ... + jq -R -s . for JSON encoding is the right pattern.
    Branch

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback Area : CI/CD PR changes GitHub Actions/workflows

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant