From c22624caa7eca57fa95bf5c1b725af25f0a2d027 Mon Sep 17 00:00:00 2001 From: nollymarlonga Date: Tue, 19 May 2026 17:44:55 -0500 Subject: [PATCH 01/12] =?UTF-8?q?fix(ci):=20harden=20PR=E2=86=92issue=20li?= =?UTF-8?q?nking=20gate=20against=20shell=20injection=20and=20missed=20ref?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../workflows/issue_comp_link-issue-to-pr.yml | 77 ++++++++++++------- .github/workflows/issue_open-pr.yml | 2 +- 2 files changed, 50 insertions(+), 29 deletions(-) diff --git a/.github/workflows/issue_comp_link-issue-to-pr.yml b/.github/workflows/issue_comp_link-issue-to-pr.yml index fba84216f752..0de89ac76c0c 100644 --- a/.github/workflows/issue_comp_link-issue-to-pr.yml +++ b/.github/workflows/issue_comp_link-issue-to-pr.yml @@ -32,23 +32,27 @@ jobs: runs-on: ubuntu-latest env: GH_TOKEN: ${{ github.token }} - + PR_BRANCH: ${{ inputs.pr_branch }} + PR_URL: ${{ inputs.pr_url }} + PR_TITLE: ${{ inputs.pr_title }} + PR_BODY: ${{ inputs.pr_body }} + PR_AUTHOR: ${{ inputs.pr_author }} + PR_MERGED: ${{ inputs.pr_merged }} + steps: - name: Debug workflow inputs run: | - echo "PR Branch: ${{ inputs.pr_branch }}" - echo "PR URL: ${{ inputs.pr_url }}" - echo "PR Title: ${{ inputs.pr_title }}" - echo "PR Body: ${{ inputs.pr_body }}" - echo "PR Author: ${{ inputs.pr_author }}" - echo "PR Merged: ${{ inputs.pr_merged }}" - env: - GITHUB_CONTEXT: ${{ toJson(github) }} + echo "PR Branch: $PR_BRANCH" + echo "PR URL: $PR_URL" + echo "PR Title: $PR_TITLE" + echo "PR Body: $PR_BODY" + echo "PR Author: $PR_AUTHOR" + echo "PR Merged: $PR_MERGED" - name: Check if PR already has linked issues id: check_existing_issues run: | - pr_url="${{ inputs.pr_url }}" + pr_url="$PR_URL" pr_number=$(echo "$pr_url" | grep -o '[0-9]*$') # Get PR details @@ -96,7 +100,7 @@ jobs: - name: Extract issue number from branch name id: extract_issue_number run: | - branch_name="${{ inputs.pr_branch }}" + branch_name="$PR_BRANCH" issue_number="" # Try multiple patterns to extract issue number (more flexible but specific) @@ -118,7 +122,9 @@ jobs: - name: Determine final issue number id: determine_issue run: | - # Priority: 1) Manually linked issues (Development section or PR body), 2) Branch name extraction + # Priority: 1) Manually linked issues (Development section or PR body), + # 2) Branch name extraction, + # 3) GitHub closingIssuesReferences fallback (catches markdown-linked refs the regex misses, e.g. "fixes [#123](url)") if [[ "${{ steps.check_existing_issues.outputs.has_linked_issues }}" == "true" ]]; then final_issue_number="${{ steps.check_existing_issues.outputs.linked_issue_number }}" link_method="${{ steps.check_existing_issues.outputs.link_method }}" @@ -127,15 +133,30 @@ jobs: final_issue_number="${{ steps.extract_issue_number.outputs.issue_number }}" echo "Using issue from branch name: $final_issue_number" else - echo "::error::No issue number found in Development section, PR body, or branch name" - echo "::error::Please link an issue using one of these methods:" - echo "::error::1. Link via GitHub UI: Go to PR → Development section → Link issue" - echo "::error::2. Add 'fixes #123' (or closes/resolves) to PR body, or" - echo "::error::3. Use branch naming like 'issue-123-feature' or '123-feature'" - echo "failure_detected=true" >> "$GITHUB_OUTPUT" - exit 1 + pr_number=$(echo "$PR_URL" | grep -o '[0-9]*$') + owner="${GITHUB_REPOSITORY%/*}" + repo="${GITHUB_REPOSITORY#*/}" + closing_issue=$(gh api graphql \ + -F owner="$owner" \ + -F name="$repo" \ + -F num="$pr_number" \ + -f query='query($owner:String!,$name:String!,$num:Int!){repository(owner:$owner,name:$name){pullRequest(number:$num){closingIssuesReferences(first:1){nodes{number}}}}}' \ + --jq '.data.repository.pullRequest.closingIssuesReferences.nodes[0].number // ""' 2>/dev/null || echo "") + + if [[ -n "$closing_issue" && "$closing_issue" != "null" ]]; then + final_issue_number="$closing_issue" + echo "Using issue from GitHub closingIssuesReferences fallback: $final_issue_number" + else + echo "::error::No issue number found in Development section, PR body, branch name, or GitHub linked issues" + echo "::error::Please link an issue using one of these methods:" + echo "::error::1. Link via GitHub UI: Go to PR → Development section → Link issue" + echo "::error::2. Add 'fixes #123' (or closes/resolves) to PR body, or" + echo "::error::3. Use branch naming like 'issue-123-feature' or '123-feature'" + echo "failure_detected=true" >> "$GITHUB_OUTPUT" + exit 1 + fi fi - + echo "final_issue_number=$final_issue_number" >> "$GITHUB_OUTPUT" - name: Get existing issue comments @@ -155,7 +176,7 @@ jobs: id: check_comment run: | comments_file="${{ steps.get_comments.outputs.comments_file }}" - pr_url="${{ inputs.pr_url }}" + pr_url="$PR_URL" # Check if our bot comment already exists (read from file instead of env var) existing_comment=$(jq -r '.[] | select(.user.login == "github-actions[bot]" and (.body | contains("PRs linked to this issue"))) | .id' "$comments_file" | head -1) @@ -185,8 +206,8 @@ jobs: } >> "$GITHUB_OUTPUT" else # Add new PR to the list - new_pr_line="- [${{ inputs.pr_title }}](${{ inputs.pr_url }}) by @${{ inputs.pr_author }}" - if [[ "${{ inputs.pr_merged }}" == "true" ]]; then + new_pr_line="- [${PR_TITLE}](${PR_URL}) by @${PR_AUTHOR}" + if [[ "$PR_MERGED" == "true" ]]; then new_pr_line="$new_pr_line ✅" fi @@ -201,8 +222,8 @@ jobs: fi else # Create new PR list - new_pr_line="- [${{ inputs.pr_title }}](${{ inputs.pr_url }}) by @${{ inputs.pr_author }}" - if [[ "${{ inputs.pr_merged }}" == "true" ]]; then + new_pr_line="- [${PR_TITLE}](${PR_URL}) by @${PR_AUTHOR}" + if [[ "$PR_MERGED" == "true" ]]; then new_pr_line="$new_pr_line ✅" fi @@ -230,7 +251,7 @@ jobs: - name: Link PR to issue run: | - pr_url="${{ inputs.pr_url }}" + pr_url="$PR_URL" pr_number=$(echo "$pr_url" | grep -o '[0-9]*$') issue_number="${{ steps.determine_issue.outputs.final_issue_number }}" @@ -267,7 +288,7 @@ jobs: - name: Remove failure comment if issue is now resolved run: | - pr_url="${{ inputs.pr_url }}" + pr_url="$PR_URL" pr_number=$(echo "$pr_url" | grep -o '[0-9]*$') # Check for existing failure comment @@ -298,7 +319,7 @@ jobs: - name: Add failure comment to PR if: failure() && steps.determine_issue.outputs.failure_detected == 'true' run: | - pr_url="${{ inputs.pr_url }}" + pr_url="$PR_URL" pr_number=$(echo "$pr_url" | grep -o '[0-9]*$') comment_body=$(printf "%s\n\n%s\n\n%s\n\n%s\n%s\n%s\n%s\n%s\n\n%s\n%s\n%s\n\n%s\n%s\n%s\n%s\n%s\n\n%s\n%s\n\n%s\n%s" \ diff --git a/.github/workflows/issue_open-pr.yml b/.github/workflows/issue_open-pr.yml index 0629d29595e1..df61951e4c8e 100644 --- a/.github/workflows/issue_open-pr.yml +++ b/.github/workflows/issue_open-pr.yml @@ -2,7 +2,7 @@ name: PR opened on: pull_request: - types: [opened] + types: [opened, edited, synchronize, reopened] jobs: add-issue-to-pr: From 7d40207fe085a28a68de6e98382dec70a7059b2e Mon Sep 17 00:00:00 2001 From: nollymarlonga Date: Tue, 19 May 2026 17:57:11 -0500 Subject: [PATCH 02/12] fix(ci): dedupe failure comment on PR linking gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .github/workflows/issue_comp_link-issue-to-pr.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/issue_comp_link-issue-to-pr.yml b/.github/workflows/issue_comp_link-issue-to-pr.yml index 0de89ac76c0c..bb9820f01327 100644 --- a/.github/workflows/issue_comp_link-issue-to-pr.yml +++ b/.github/workflows/issue_comp_link-issue-to-pr.yml @@ -321,7 +321,19 @@ jobs: run: | pr_url="$PR_URL" pr_number=$(echo "$pr_url" | grep -o '[0-9]*$') - + + # Skip if a failure comment already exists (avoid duplicates on synchronize/edited re-runs) + existing_comments=$(curl -s \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ env.GH_TOKEN }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/${{ github.repository }}/issues/$pr_number/comments") + existing_failure_id=$(echo "$existing_comments" | jq -r '.[] | select(.user.login == "github-actions[bot]" and (.body | contains("❌ Issue Linking Required"))) | .id' | head -1) + if [[ -n "$existing_failure_id" && "$existing_failure_id" != "null" ]]; then + echo "Failure comment $existing_failure_id already exists on PR #$pr_number; skipping" + exit 0 + fi + comment_body=$(printf "%s\n\n%s\n\n%s\n\n%s\n%s\n%s\n%s\n%s\n\n%s\n%s\n%s\n\n%s\n%s\n%s\n%s\n%s\n\n%s\n%s\n\n%s\n%s" \ "## ❌ Issue Linking Required" \ "This PR could not be linked to an issue. **All PRs must be linked to an issue** for tracking purposes." \ From 38667e0278ecd4cc5260377e974db836ac103659 Mon Sep 17 00:00:00 2001 From: nollymarlonga Date: Tue, 19 May 2026 19:26:24 -0500 Subject: [PATCH 03/12] fix(ci): address PR review on linking gate hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../workflows/issue_comp_link-issue-to-pr.yml | 30 ++++++++++++++----- .github/workflows/issue_open-pr.yml | 2 ++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/.github/workflows/issue_comp_link-issue-to-pr.yml b/.github/workflows/issue_comp_link-issue-to-pr.yml index bb9820f01327..e7945301c26b 100644 --- a/.github/workflows/issue_comp_link-issue-to-pr.yml +++ b/.github/workflows/issue_comp_link-issue-to-pr.yml @@ -30,6 +30,9 @@ on: jobs: link-issue: runs-on: ubuntu-latest + permissions: + pull-requests: write + issues: write env: GH_TOKEN: ${{ github.token }} PR_BRANCH: ${{ inputs.pr_branch }} @@ -134,14 +137,25 @@ jobs: echo "Using issue from branch name: $final_issue_number" else pr_number=$(echo "$PR_URL" | grep -o '[0-9]*$') - owner="${GITHUB_REPOSITORY%/*}" - repo="${GITHUB_REPOSITORY#*/}" - closing_issue=$(gh api graphql \ - -F owner="$owner" \ - -F name="$repo" \ - -F num="$pr_number" \ - -f query='query($owner:String!,$name:String!,$num:Int!){repository(owner:$owner,name:$name){pullRequest(number:$num){closingIssuesReferences(first:1){nodes{number}}}}}' \ - --jq '.data.repository.pullRequest.closingIssuesReferences.nodes[0].number // ""' 2>/dev/null || echo "") + closing_issue="" + if [[ -z "$pr_number" ]]; then + echo "::warning::Could not extract PR number from PR_URL=$PR_URL; skipping closingIssuesReferences fallback" + else + owner="${GITHUB_REPOSITORY%/*}" + repo="${GITHUB_REPOSITORY#*/}" + gh_err=$(mktemp) + closing_issue=$(gh api graphql \ + -F owner="$owner" \ + -F name="$repo" \ + -F num="$pr_number" \ + -f query='query($owner:String!,$name:String!,$num:Int!){repository(owner:$owner,name:$name){pullRequest(number:$num){closingIssuesReferences(first:1){nodes{number}}}}}' \ + --jq '.data.repository.pullRequest.closingIssuesReferences.nodes[0].number // ""' 2>"$gh_err") || { + echo "::warning::closingIssuesReferences GraphQL fallback failed:" + cat "$gh_err" + closing_issue="" + } + rm -f "$gh_err" + fi if [[ -n "$closing_issue" && "$closing_issue" != "null" ]]; then final_issue_number="$closing_issue" diff --git a/.github/workflows/issue_open-pr.yml b/.github/workflows/issue_open-pr.yml index df61951e4c8e..106ce1bc8e74 100644 --- a/.github/workflows/issue_open-pr.yml +++ b/.github/workflows/issue_open-pr.yml @@ -7,6 +7,8 @@ on: jobs: add-issue-to-pr: name: Add Issue to PR + # Skip fork PRs — pull_request token is read-only for forks, so PATCH/POST/DELETE calls would 403. + if: github.event.pull_request.head.repo.full_name == github.repository uses: ./.github/workflows/issue_comp_link-issue-to-pr.yml with: pr_branch: ${{ github.head_ref }} From 1144cf43844d233a0d08def8c108cf26fd7f2a2c Mon Sep 17 00:00:00 2001 From: nollymarlonga Date: Thu, 21 May 2026 13:19:39 -0500 Subject: [PATCH 04/12] chore: trigger PR linking validation re-run Empty commit to exercise the synchronize trigger after the linking-gate hardening changes. Co-Authored-By: Claude Opus 4.7 (1M context) From cadde9a58ee49f7270af329dc1ffaeb46fc56062 Mon Sep 17 00:00:00 2001 From: nollymarlonga Date: Thu, 21 May 2026 15:21:51 -0500 Subject: [PATCH 05/12] feat(ci): require linked issue to have a team label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../workflows/issue_comp_link-issue-to-pr.yml | 80 ++++++++++++++++++- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue_comp_link-issue-to-pr.yml b/.github/workflows/issue_comp_link-issue-to-pr.yml index e7945301c26b..fb198d1399a3 100644 --- a/.github/workflows/issue_comp_link-issue-to-pr.yml +++ b/.github/workflows/issue_comp_link-issue-to-pr.yml @@ -330,6 +330,46 @@ jobs: echo "No failure comment found to remove" fi + - name: Validate linked issue has team label + id: validate_issue + run: | + issue_number="${{ steps.determine_issue.outputs.final_issue_number }}" + + labels=$(gh issue view "$issue_number" --repo "${{ github.repository }}" --json labels --jq '.labels[].name' 2>/dev/null || echo "") + team_label=$(echo "$labels" | grep -E '^Team : ' | head -1) + + if [[ -z "$team_label" ]]; then + echo "::error::Linked issue #$issue_number has no 'Team : *' label" + echo "::error::Apply a team label (e.g., 'Team : Scout', 'Team : Platform') to the linked issue" + echo "validation_failed=true" >> "$GITHUB_OUTPUT" + exit 1 + fi + + echo "Linked issue #$issue_number has team label: $team_label" + + - name: Remove issue-validation failure comment if resolved + run: | + pr_url="$PR_URL" + pr_number=$(echo "$pr_url" | grep -o '[0-9]*$') + + existing_comments=$(curl -s \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ env.GH_TOKEN }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/${{ github.repository }}/issues/$pr_number/comments") + validation_comment_id=$(echo "$existing_comments" | jq -r '.[] | select(.user.login == "github-actions[bot]" and (.body | contains("❌ Linked Issue Needs Team Label"))) | .id' | head -1) + + if [[ -n "$validation_comment_id" && "$validation_comment_id" != "null" ]]; then + curl -X DELETE \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ env.GH_TOKEN }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/${{ github.repository }}/issues/comments/$validation_comment_id" + echo "Removed issue-validation failure comment from PR #$pr_number (linked issue now has team label)" + else + echo "No issue-validation failure comment found to remove" + fi + - name: Add failure comment to PR if: failure() && steps.determine_issue.outputs.failure_detected == 'true' run: | @@ -378,5 +418,41 @@ jobs: -H "X-GitHub-Api-Version: 2022-11-28" \ "https://api.github.com/repos/${{ github.repository }}/issues/$pr_number/comments" \ -d "{\"body\":$(echo "$comment_body" | jq -R -s .)}" - - echo "Added failure comment to PR #$pr_number" \ No newline at end of file + + echo "Added failure comment to PR #$pr_number" + + - name: Add issue-validation failure comment to PR + if: failure() && steps.validate_issue.outputs.validation_failed == 'true' + run: | + pr_url="$PR_URL" + pr_number=$(echo "$pr_url" | grep -o '[0-9]*$') + issue_number="${{ steps.determine_issue.outputs.final_issue_number }}" + + # Skip if a validation failure comment already exists + existing_comments=$(curl -s \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ env.GH_TOKEN }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/${{ github.repository }}/issues/$pr_number/comments") + existing_id=$(echo "$existing_comments" | jq -r '.[] | select(.user.login == "github-actions[bot]" and (.body | contains("❌ Linked Issue Needs Team Label"))) | .id' | head -1) + if [[ -n "$existing_id" && "$existing_id" != "null" ]]; then + echo "Validation failure comment $existing_id already exists on PR #$pr_number; skipping" + exit 0 + fi + + comment_body=$(printf "%s\n\n%s\n\n%s\n%s\n\n%s\n%s" \ + "## ❌ Linked Issue Needs Team Label" \ + "This PR is linked to issue #$issue_number, but that issue has **no \`Team : *\` label**. Every linked issue must be owned by a team for tracking and triage." \ + "### How to fix this:" \ + "Apply a \`Team : *\` label to the linked issue (e.g., \`Team : Scout\`, \`Team : Platform\`, \`Team : Falcon\`, \`Team : Maintenance\`). Then push a new commit or edit the PR description to re-run this check." \ + "---" \ + "*This comment was automatically generated by the issue linking workflow*") + + curl -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ env.GH_TOKEN }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/${{ github.repository }}/issues/$pr_number/comments" \ + -d "{\"body\":$(echo "$comment_body" | jq -R -s .)}" + + echo "Added issue-validation failure comment to PR #$pr_number" \ No newline at end of file From b132d801c883f5436f2bf7ce9afec5e00e0f3a1b Mon Sep 17 00:00:00 2001 From: nollymarlonga Date: Thu, 21 May 2026 15:31:27 -0500 Subject: [PATCH 06/12] fix(ci): close heredoc EOF on idempotent re-run path 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) --- .github/workflows/issue_comp_link-issue-to-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue_comp_link-issue-to-pr.yml b/.github/workflows/issue_comp_link-issue-to-pr.yml index fb198d1399a3..c29531182ba1 100644 --- a/.github/workflows/issue_comp_link-issue-to-pr.yml +++ b/.github/workflows/issue_comp_link-issue-to-pr.yml @@ -215,7 +215,7 @@ jobs: echo "PR already exists in comment, keeping existing list" { echo "pr_list<> "$GITHUB_OUTPUT" else From 2e6b0ed0f4eb349794e68a94d04021626346c8f3 Mon Sep 17 00:00:00 2001 From: nollymarlonga Date: Thu, 21 May 2026 16:00:39 -0500 Subject: [PATCH 07/12] chore: trigger team-label validation re-run 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) From 312ced3e0444cebd028a394dc45efc242602467e Mon Sep 17 00:00:00 2001 From: nollymarlonga Date: Thu, 21 May 2026 18:00:34 -0500 Subject: [PATCH 08/12] fix(ci): address review feedback on PR linking gate - 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_` 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) --- .../workflows/issue_comp_link-issue-to-pr.yml | 197 ++++++++++-------- 1 file changed, 111 insertions(+), 86 deletions(-) diff --git a/.github/workflows/issue_comp_link-issue-to-pr.yml b/.github/workflows/issue_comp_link-issue-to-pr.yml index c29531182ba1..be4440c168ca 100644 --- a/.github/workflows/issue_comp_link-issue-to-pr.yml +++ b/.github/workflows/issue_comp_link-issue-to-pr.yml @@ -173,6 +173,92 @@ jobs: echo "final_issue_number=$final_issue_number" >> "$GITHUB_OUTPUT" + # Gates run before any side effects so that a PR whose linked issue is + # missing required metadata does not get its body patched or have a + # PR-list comment posted on the issue thread. + + - name: Remove failure comment if issue is now resolved + run: | + pr_url="$PR_URL" + pr_number=$(echo "$pr_url" | grep -o '[0-9]*$') + + # Check for existing failure comment + existing_comments=$(curl -s \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ env.GH_TOKEN }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/${{ github.repository }}/issues/$pr_number/comments") + + # Find failure comment by looking for the distinctive header + failure_comment_id=$(echo "$existing_comments" | jq -r '.[] | select(.user.login == "github-actions[bot]" and (.body | contains("❌ Issue Linking Required"))) | .id' | head -1) + + if [[ -n "$failure_comment_id" && "$failure_comment_id" != "null" ]]; then + echo "Found existing failure comment: $failure_comment_id" + + # Delete the failure comment since the issue is now resolved + curl -X DELETE \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ env.GH_TOKEN }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/${{ github.repository }}/issues/comments/$failure_comment_id" + + echo "Removed failure comment from PR #$pr_number (issue now resolved)" + else + echo "No failure comment found to remove" + fi + + - name: Validate linked issue has team label + id: validate_issue + run: | + issue_number="${{ steps.determine_issue.outputs.final_issue_number }}" + + # Distinguish API/permission errors from a genuine missing-label result — + # otherwise a transient 5xx, a deleted issue, or a scope problem would + # all surface as "no team label" and start a cycle where authors add + # the label but the next run still fails. + gh_err=$(mktemp) + if ! labels=$(gh issue view "$issue_number" --repo "${{ github.repository }}" --json labels --jq '.labels[].name' 2>"$gh_err"); then + echo "::error::Failed to fetch labels for linked issue #$issue_number:" + cat "$gh_err" + rm -f "$gh_err" + exit 1 + fi + rm -f "$gh_err" + + team_label=$(echo "$labels" | grep -E '^Team : ' | head -1) + + if [[ -z "$team_label" ]]; then + echo "::error::Linked issue #$issue_number has no 'Team : *' label" + echo "::error::Apply a team label (e.g., 'Team : Scout', 'Team : Platform') to the linked issue" + echo "validation_failed=true" >> "$GITHUB_OUTPUT" + exit 1 + fi + + echo "Linked issue #$issue_number has team label: $team_label" + + - name: Remove issue-validation failure comment if resolved + run: | + pr_url="$PR_URL" + pr_number=$(echo "$pr_url" | grep -o '[0-9]*$') + + existing_comments=$(curl -s \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ env.GH_TOKEN }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/${{ github.repository }}/issues/$pr_number/comments") + validation_comment_id=$(echo "$existing_comments" | jq -r '.[] | select(.user.login == "github-actions[bot]" and (.body | contains("❌ Linked Issue Needs Team Label"))) | .id' | head -1) + + if [[ -n "$validation_comment_id" && "$validation_comment_id" != "null" ]]; then + curl -X DELETE \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ env.GH_TOKEN }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/${{ github.repository }}/issues/comments/$validation_comment_id" + echo "Removed issue-validation failure comment from PR #$pr_number (linked issue now has team label)" + else + echo "No issue-validation failure comment found to remove" + fi + - name: Get existing issue comments id: get_comments run: | @@ -192,9 +278,17 @@ jobs: comments_file="${{ steps.get_comments.outputs.comments_file }}" pr_url="$PR_URL" + # Escape markdown link characters in the user-supplied PR title so an + # adversarial title like 'Fix bug](https://evil)' can't redirect the bot link. + escaped_title=$(printf '%s' "$PR_TITLE" | sed -e 's/\\/\\\\/g' -e 's/\[/\\[/g' -e 's/\]/\\]/g') + + # Random heredoc delimiter — prevents collision if user-supplied content + # (PR titles in previous list entries) contains a literal "EOF" line. + delim="ghadelimiter_$(openssl rand -hex 16)" + # Check if our bot comment already exists (read from file instead of env var) existing_comment=$(jq -r '.[] | select(.user.login == "github-actions[bot]" and (.body | contains("PRs linked to this issue"))) | .id' "$comments_file" | head -1) - + if [[ -n "$existing_comment" && "$existing_comment" != "null" ]]; then echo "Found existing comment: $existing_comment" echo "existing_comment_id=$existing_comment" >> "$GITHUB_OUTPUT" @@ -202,50 +296,51 @@ jobs: echo "No existing comment found" echo "existing_comment_id=" >> "$GITHUB_OUTPUT" fi - + # Get existing PR list from the comment if it exists if [[ -n "$existing_comment" && "$existing_comment" != "null" ]]; then existing_body=$(jq -r --arg id "$existing_comment" '.[] | select(.id == ($id | tonumber)) | .body' "$comments_file") - + # Extract existing PR lines (lines starting with "- [") existing_pr_lines=$(echo "$existing_body" | grep "^- \[" | sort -u) - - # Check if current PR is already in the list - if echo "$existing_pr_lines" | grep -q "$pr_url"; then + + # Check if current PR is already in the list (fixed-string match to avoid + # regex metacharacters in the URL causing false positives). + if echo "$existing_pr_lines" | grep -qF "$pr_url"; then echo "PR already exists in comment, keeping existing list" { - echo "pr_list<> "$GITHUB_OUTPUT" else # Add new PR to the list - new_pr_line="- [${PR_TITLE}](${PR_URL}) by @${PR_AUTHOR}" + new_pr_line="- [${escaped_title}](${PR_URL}) by @${PR_AUTHOR}" if [[ "$PR_MERGED" == "true" ]]; then new_pr_line="$new_pr_line ✅" fi - + # Combine existing and new PR lines all_pr_lines=$(echo -e "$existing_pr_lines\n$new_pr_line" | sort -u) new_body=$(printf "## PRs linked to this issue\n\n%s" "$all_pr_lines") { - echo "pr_list<> "$GITHUB_OUTPUT" fi else # Create new PR list - new_pr_line="- [${PR_TITLE}](${PR_URL}) by @${PR_AUTHOR}" + new_pr_line="- [${escaped_title}](${PR_URL}) by @${PR_AUTHOR}" if [[ "$PR_MERGED" == "true" ]]; then new_pr_line="$new_pr_line ✅" fi - + new_body=$(printf "## PRs linked to this issue\n\n%s" "$new_pr_line") { - echo "pr_list<> "$GITHUB_OUTPUT" fi @@ -300,76 +395,6 @@ jobs: echo "Issue #$issue_number already referenced in PR #$pr_number body" fi - - name: Remove failure comment if issue is now resolved - run: | - pr_url="$PR_URL" - pr_number=$(echo "$pr_url" | grep -o '[0-9]*$') - - # Check for existing failure comment - existing_comments=$(curl -s \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${{ env.GH_TOKEN }}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/repos/${{ github.repository }}/issues/$pr_number/comments") - - # Find failure comment by looking for the distinctive header - failure_comment_id=$(echo "$existing_comments" | jq -r '.[] | select(.user.login == "github-actions[bot]" and (.body | contains("❌ Issue Linking Required"))) | .id' | head -1) - - if [[ -n "$failure_comment_id" && "$failure_comment_id" != "null" ]]; then - echo "Found existing failure comment: $failure_comment_id" - - # Delete the failure comment since the issue is now resolved - curl -X DELETE \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${{ env.GH_TOKEN }}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/repos/${{ github.repository }}/issues/comments/$failure_comment_id" - - echo "Removed failure comment from PR #$pr_number (issue now resolved)" - else - echo "No failure comment found to remove" - fi - - - name: Validate linked issue has team label - id: validate_issue - run: | - issue_number="${{ steps.determine_issue.outputs.final_issue_number }}" - - labels=$(gh issue view "$issue_number" --repo "${{ github.repository }}" --json labels --jq '.labels[].name' 2>/dev/null || echo "") - team_label=$(echo "$labels" | grep -E '^Team : ' | head -1) - - if [[ -z "$team_label" ]]; then - echo "::error::Linked issue #$issue_number has no 'Team : *' label" - echo "::error::Apply a team label (e.g., 'Team : Scout', 'Team : Platform') to the linked issue" - echo "validation_failed=true" >> "$GITHUB_OUTPUT" - exit 1 - fi - - echo "Linked issue #$issue_number has team label: $team_label" - - - name: Remove issue-validation failure comment if resolved - run: | - pr_url="$PR_URL" - pr_number=$(echo "$pr_url" | grep -o '[0-9]*$') - - existing_comments=$(curl -s \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${{ env.GH_TOKEN }}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/repos/${{ github.repository }}/issues/$pr_number/comments") - validation_comment_id=$(echo "$existing_comments" | jq -r '.[] | select(.user.login == "github-actions[bot]" and (.body | contains("❌ Linked Issue Needs Team Label"))) | .id' | head -1) - - if [[ -n "$validation_comment_id" && "$validation_comment_id" != "null" ]]; then - curl -X DELETE \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${{ env.GH_TOKEN }}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/repos/${{ github.repository }}/issues/comments/$validation_comment_id" - echo "Removed issue-validation failure comment from PR #$pr_number (linked issue now has team label)" - else - echo "No issue-validation failure comment found to remove" - fi - - name: Add failure comment to PR if: failure() && steps.determine_issue.outputs.failure_detected == 'true' run: | From bf0899cdd0e25b34ed53338755e989234917e509 Mon Sep 17 00:00:00 2001 From: nollymarlonga Date: Thu, 21 May 2026 18:31:38 -0500 Subject: [PATCH 09/12] fix(ci): address second-round review on PR linking gate - 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-, 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) --- .../workflows/issue_comp_link-issue-to-pr.yml | 47 +++++++++++++------ .github/workflows/issue_open-pr.yml | 8 ++++ 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/.github/workflows/issue_comp_link-issue-to-pr.yml b/.github/workflows/issue_comp_link-issue-to-pr.yml index be4440c168ca..f32859714f42 100644 --- a/.github/workflows/issue_comp_link-issue-to-pr.yml +++ b/.github/workflows/issue_comp_link-issue-to-pr.yml @@ -124,16 +124,21 @@ jobs: - name: Determine final issue number id: determine_issue + env: + HAS_LINKED_ISSUES: ${{ steps.check_existing_issues.outputs.has_linked_issues }} + LINKED_ISSUE_NUMBER: ${{ steps.check_existing_issues.outputs.linked_issue_number }} + LINK_METHOD: ${{ steps.check_existing_issues.outputs.link_method }} + BRANCH_ISSUE_NUMBER: ${{ steps.extract_issue_number.outputs.issue_number }} run: | # Priority: 1) Manually linked issues (Development section or PR body), # 2) Branch name extraction, # 3) GitHub closingIssuesReferences fallback (catches markdown-linked refs the regex misses, e.g. "fixes [#123](url)") - if [[ "${{ steps.check_existing_issues.outputs.has_linked_issues }}" == "true" ]]; then - final_issue_number="${{ steps.check_existing_issues.outputs.linked_issue_number }}" - link_method="${{ steps.check_existing_issues.outputs.link_method }}" + if [[ "$HAS_LINKED_ISSUES" == "true" ]]; then + final_issue_number="$LINKED_ISSUE_NUMBER" + link_method="$LINK_METHOD" echo "Using manually linked issue: $final_issue_number (via $link_method)" - elif [[ -n "${{ steps.extract_issue_number.outputs.issue_number }}" ]]; then - final_issue_number="${{ steps.extract_issue_number.outputs.issue_number }}" + elif [[ -n "$BRANCH_ISSUE_NUMBER" ]]; then + final_issue_number="$BRANCH_ISSUE_NUMBER" echo "Using issue from branch name: $final_issue_number" else pr_number=$(echo "$PR_URL" | grep -o '[0-9]*$') @@ -209,8 +214,10 @@ jobs: - name: Validate linked issue has team label id: validate_issue + env: + ISSUE_NUMBER: ${{ steps.determine_issue.outputs.final_issue_number }} run: | - issue_number="${{ steps.determine_issue.outputs.final_issue_number }}" + issue_number="$ISSUE_NUMBER" # Distinguish API/permission errors from a genuine missing-label result — # otherwise a transient 5xx, a deleted issue, or a scope problem would @@ -261,21 +268,25 @@ jobs: - name: Get existing issue comments id: get_comments + env: + ISSUE_NUMBER: ${{ steps.determine_issue.outputs.final_issue_number }} run: | # Fetch comments and write to temp file to avoid multiline JSON issues in GitHub Actions outputs curl -s \ -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer ${{ env.GH_TOKEN }}" \ -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.determine_issue.outputs.final_issue_number }}/comments \ + "https://api.github.com/repos/${{ github.repository }}/issues/$ISSUE_NUMBER/comments" \ | jq -c . > /tmp/issue_comments.json echo "comments_file=/tmp/issue_comments.json" >> "$GITHUB_OUTPUT" - name: Check if comment already exists id: check_comment + env: + COMMENTS_FILE: ${{ steps.get_comments.outputs.comments_file }} run: | - comments_file="${{ steps.get_comments.outputs.comments_file }}" + comments_file="$COMMENTS_FILE" pr_url="$PR_URL" # Escape markdown link characters in the user-supplied PR title so an @@ -304,9 +315,11 @@ jobs: # Extract existing PR lines (lines starting with "- [") existing_pr_lines=$(echo "$existing_body" | grep "^- \[" | sort -u) - # Check if current PR is already in the list (fixed-string match to avoid - # regex metacharacters in the URL causing false positives). - if echo "$existing_pr_lines" | grep -qF "$pr_url"; then + # Check if current PR is already in the list. Match against the literal + # markdown-link terminator `($pr_url)` so a URL is not seen as a prefix of + # another PR's URL (e.g. /pull/10 substring-matching /pull/100), and so + # regex metacharacters in the URL are not interpreted. + if echo "$existing_pr_lines" | grep -qF "($pr_url)"; then echo "PR already exists in comment, keeping existing list" { echo "pr_list<<$delim" @@ -359,10 +372,12 @@ jobs: body: ${{ steps.check_comment.outputs.pr_list }} - name: Link PR to issue + env: + ISSUE_NUMBER: ${{ steps.determine_issue.outputs.final_issue_number }} run: | pr_url="$PR_URL" pr_number=$(echo "$pr_url" | grep -o '[0-9]*$') - issue_number="${{ steps.determine_issue.outputs.final_issue_number }}" + issue_number="$ISSUE_NUMBER" # Get current PR body current_pr=$(curl -s \ @@ -373,8 +388,8 @@ jobs: current_body=$(echo "$current_pr" | jq -r '.body // ""') - # Check if issue reference already exists - if ! echo "$current_body" | grep -q "#$issue_number"; then + # Check if issue reference already exists (word-boundary match so #10 does not match #100) + if ! echo "$current_body" | grep -qE "(^|[^0-9])#${issue_number}([^0-9]|\$)"; then # Add issue reference to PR body if [[ -n "$current_body" && "$current_body" != "null" ]]; then new_body=$(printf "%s\n\nThis PR fixes: #%s" "$current_body" "$issue_number") @@ -448,10 +463,12 @@ jobs: - name: Add issue-validation failure comment to PR if: failure() && steps.validate_issue.outputs.validation_failed == 'true' + env: + ISSUE_NUMBER: ${{ steps.determine_issue.outputs.final_issue_number }} run: | pr_url="$PR_URL" pr_number=$(echo "$pr_url" | grep -o '[0-9]*$') - issue_number="${{ steps.determine_issue.outputs.final_issue_number }}" + issue_number="$ISSUE_NUMBER" # Skip if a validation failure comment already exists existing_comments=$(curl -s \ diff --git a/.github/workflows/issue_open-pr.yml b/.github/workflows/issue_open-pr.yml index 106ce1bc8e74..09d6de617b21 100644 --- a/.github/workflows/issue_open-pr.yml +++ b/.github/workflows/issue_open-pr.yml @@ -4,6 +4,14 @@ on: pull_request: types: [opened, edited, synchronize, reopened] +# Serialize per-PR so concurrent events (synchronize + the edited fired by our own +# Link PR to issue body PATCH) don't race on the PATCH and on comment writes. +# cancel-in-progress is false so each event settles before the next runs — we want +# the final state of every event, not the latest one only. +concurrency: + group: link-issue-${{ github.event.pull_request.number }} + cancel-in-progress: false + jobs: add-issue-to-pr: name: Add Issue to PR From a73a2aebabe3a4450caa2f392fd2a23fd919b232 Mon Sep 17 00:00:00 2001 From: nollymarlonga Date: Thu, 21 May 2026 18:33:00 -0500 Subject: [PATCH 10/12] chore: trigger linking-gate validation after review fixes 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) From 9fc7a102249017468679391b52862f398e8496b3 Mon Sep 17 00:00:00 2001 From: nollymarlonga Date: Thu, 21 May 2026 18:45:23 -0500 Subject: [PATCH 11/12] fix(ci): promote closingIssuesReferences above branch-name heuristic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../workflows/issue_comp_link-issue-to-pr.yml | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/workflows/issue_comp_link-issue-to-pr.yml b/.github/workflows/issue_comp_link-issue-to-pr.yml index f32859714f42..5c2d5c82ae65 100644 --- a/.github/workflows/issue_comp_link-issue-to-pr.yml +++ b/.github/workflows/issue_comp_link-issue-to-pr.yml @@ -130,21 +130,21 @@ jobs: LINK_METHOD: ${{ steps.check_existing_issues.outputs.link_method }} BRANCH_ISSUE_NUMBER: ${{ steps.extract_issue_number.outputs.issue_number }} run: | - # Priority: 1) Manually linked issues (Development section or PR body), - # 2) Branch name extraction, - # 3) GitHub closingIssuesReferences fallback (catches markdown-linked refs the regex misses, e.g. "fixes [#123](url)") + # Priority: + # 1) check_existing_issues: timeline (Development section) or PR body plain-keyword regex + # 2) closingIssuesReferences GraphQL: GitHub's own parser, catches markdown-linked refs + # the regex misses (e.g. "fixes [#123](url)"). This must outrank branch-name + # heuristics — author intent in the PR body should win over a branch pattern. + # 3) Branch name extraction: heuristic of last resort. if [[ "$HAS_LINKED_ISSUES" == "true" ]]; then final_issue_number="$LINKED_ISSUE_NUMBER" link_method="$LINK_METHOD" echo "Using manually linked issue: $final_issue_number (via $link_method)" - elif [[ -n "$BRANCH_ISSUE_NUMBER" ]]; then - final_issue_number="$BRANCH_ISSUE_NUMBER" - echo "Using issue from branch name: $final_issue_number" else pr_number=$(echo "$PR_URL" | grep -o '[0-9]*$') closing_issue="" if [[ -z "$pr_number" ]]; then - echo "::warning::Could not extract PR number from PR_URL=$PR_URL; skipping closingIssuesReferences fallback" + echo "::warning::Could not extract PR number from PR_URL=$PR_URL; skipping closingIssuesReferences lookup" else owner="${GITHUB_REPOSITORY%/*}" repo="${GITHUB_REPOSITORY#*/}" @@ -155,7 +155,7 @@ jobs: -F num="$pr_number" \ -f query='query($owner:String!,$name:String!,$num:Int!){repository(owner:$owner,name:$name){pullRequest(number:$num){closingIssuesReferences(first:1){nodes{number}}}}}' \ --jq '.data.repository.pullRequest.closingIssuesReferences.nodes[0].number // ""' 2>"$gh_err") || { - echo "::warning::closingIssuesReferences GraphQL fallback failed:" + echo "::warning::closingIssuesReferences GraphQL lookup failed:" cat "$gh_err" closing_issue="" } @@ -164,9 +164,12 @@ jobs: if [[ -n "$closing_issue" && "$closing_issue" != "null" ]]; then final_issue_number="$closing_issue" - echo "Using issue from GitHub closingIssuesReferences fallback: $final_issue_number" + echo "Using issue from GitHub closingIssuesReferences (PR body / sidebar link): $final_issue_number" + elif [[ -n "$BRANCH_ISSUE_NUMBER" ]]; then + final_issue_number="$BRANCH_ISSUE_NUMBER" + echo "Using issue from branch name: $final_issue_number" else - echo "::error::No issue number found in Development section, PR body, branch name, or GitHub linked issues" + echo "::error::No issue number found in Development section, PR body, GitHub linked issues, or branch name" echo "::error::Please link an issue using one of these methods:" echo "::error::1. Link via GitHub UI: Go to PR → Development section → Link issue" echo "::error::2. Add 'fixes #123' (or closes/resolves) to PR body, or" From 157b6772097de2d8b619994d388feb5793aeb1cb Mon Sep 17 00:00:00 2001 From: nollymarlonga Date: Fri, 22 May 2026 15:21:01 -0500 Subject: [PATCH 12/12] fix(ci): replace linked-issue PR-list comment instead of appending MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .github/workflows/issue_comp_link-issue-to-pr.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/issue_comp_link-issue-to-pr.yml b/.github/workflows/issue_comp_link-issue-to-pr.yml index 5c2d5c82ae65..2c54f4b4a1ea 100644 --- a/.github/workflows/issue_comp_link-issue-to-pr.yml +++ b/.github/workflows/issue_comp_link-issue-to-pr.yml @@ -373,6 +373,10 @@ jobs: with: comment-id: ${{ steps.check_comment.outputs.existing_comment_id }} body: ${{ steps.check_comment.outputs.pr_list }} + # Replace the comment body — the action's default `append` mode tacks the new + # body onto the old one, causing the "PRs linked to this issue" block to + # accumulate on every re-run (issue #35794 hit 10 stacked copies). + edit-mode: replace - name: Link PR to issue env: