.github/workflows/reopen_on_assignment.yml YAML 196 lines View on github.com → Search inside
1# Reopen PRs that were auto-closed by require_issue_link.yml when the2# contributor was not assigned to the linked issue. When a maintainer3# assigns the contributor to the issue, this workflow finds matching4# closed PRs, verifies the issue link, and reopens them.5#6# Uses the default GITHUB_TOKEN (not a PAT or app token) so that the7# reopen and label-removal events do NOT re-trigger other workflows.8# GitHub suppresses events created by the default GITHUB_TOKEN within9# workflow runs to prevent infinite loops.1011name: Reopen PR on Issue Assignment1213on:14  issues:15    types: [assigned]1617permissions:18  contents: read1920jobs:21  reopen-linked-prs:22    runs-on: ubuntu-latest23    permissions:24      actions: write25      pull-requests: write2627    steps:28      - name: Find and reopen matching PRs29        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.030        with:31          script: |32            const { owner, repo } = context.repo;33            const issueNumber = context.payload.issue.number;34            const assignee = context.payload.assignee.login;3536            console.log(37              `Issue #${issueNumber} assigned to ${assignee} — searching for closed PRs to reopen`,38            );3940            const q = [41              `is:pr`,42              `is:closed`,43              `author:${assignee}`,44              `label:missing-issue-link`,45              `repo:${owner}/${repo}`,46            ].join(' ');4748            let data;49            try {50              ({ data } = await github.rest.search.issuesAndPullRequests({51                q,52                per_page: 30,53              }));54            } catch (e) {55              throw new Error(56                `Failed to search for closed PRs to reopen after assigning ${assignee} ` +57                `to #${issueNumber} (HTTP ${e.status ?? 'unknown'}): ${e.message}`,58              );59            }6061            if (data.total_count === 0) {62              console.log('No matching closed PRs found');63              return;64            }6566            console.log(`Found ${data.total_count} candidate PR(s)`);6768            // Must stay in sync with the identical pattern in require_issue_link.yml69            const pattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*#(\d+)/gi;7071            for (const item of data.items) {72              const prNumber = item.number;73              const body = item.body || '';74              const matches = [...body.matchAll(pattern)];75              const referencedIssues = matches.map(m => parseInt(m[1], 10));7677              if (!referencedIssues.includes(issueNumber)) {78                console.log(`PR #${prNumber} does not reference #${issueNumber} — skipping`);79                continue;80              }8182              // Skip if already bypassed83              const labels = item.labels.map(l => l.name);84              if (labels.includes('bypass-issue-check')) {85                console.log(`PR #${prNumber} already has bypass-issue-check — skipping`);86                continue;87              }8889              // Reopen first, remove label second — a closed PR that still has90              // missing-issue-link is recoverable; a closed PR with the label91              // stripped is invisible to both workflows.92              try {93                await github.rest.pulls.update({94                  owner,95                  repo,96                  pull_number: prNumber,97                  state: 'open',98                });99                console.log(`Reopened PR #${prNumber}`);100              } catch (e) {101                if (e.status === 422) {102                  // Head branch deleted — PR is unrecoverable. Notify the103                  // contributor so they know to open a new PR.104                  core.warning(`Cannot reopen PR #${prNumber}: head branch was likely deleted`);105                  try {106                    await github.rest.issues.createComment({107                      owner,108                      repo,109                      issue_number: prNumber,110                      body:111                        `You have been assigned to #${issueNumber}, but this PR could not be ` +112                        `reopened because the head branch has been deleted. Please open a new ` +113                        `PR referencing the issue.`,114                    });115                  } catch (commentErr) {116                    core.warning(117                      `Also failed to post comment on PR #${prNumber}: ${commentErr.message}`,118                    );119                  }120                  continue;121                }122                // Transient errors (rate limit, 5xx) should fail the job so123                // the label is NOT removed and the run can be retried.124                throw e;125              }126127              // Remove missing-issue-link label only after successful reopen128              try {129                await github.rest.issues.removeLabel({130                  owner,131                  repo,132                  issue_number: prNumber,133                  name: 'missing-issue-link',134                });135                console.log(`Removed missing-issue-link from PR #${prNumber}`);136              } catch (e) {137                if (e.status !== 404) throw e;138              }139140              // Minimize stale enforcement comment (best-effort;141              // sync w/ require_issue_link.yml minimize blocks)142              try {143                const marker = '<!-- require-issue-link -->';144                const comments = await github.paginate(145                  github.rest.issues.listComments,146                  { owner, repo, issue_number: prNumber, per_page: 100 },147                );148                const stale = comments.find(c => c.body && c.body.includes(marker));149                if (stale) {150                  await github.graphql(`151                    mutation($id: ID!) {152                      minimizeComment(input: {subjectId: $id, classifier: OUTDATED}) {153                        minimizedComment { isMinimized }154                      }155                    }156                  `, { id: stale.node_id });157                  console.log(`Minimized stale enforcement comment ${stale.id} as outdated`);158                }159              } catch (e) {160                core.warning(`Could not minimize stale comment on PR #${prNumber}: ${e.message}`);161              }162163              // Re-run the failed require_issue_link check so it picks up the164              // new assignment.  The re-run uses the original event payload but165              // fetches live issue data, so the assignment check will pass.166              //167              // Limitation: we look up runs by the PR's current head SHA.  If the168              // contributor pushed new commits while the PR was closed, head.sha169              // won't match the SHA of the original failed run and the query will170              // return 0 results.  This is acceptable because any push after reopen171              // triggers a fresh require_issue_link run against the new SHA.172              try {173                const { data: pr } = await github.rest.pulls.get({174                  owner, repo, pull_number: prNumber,175                });176                const { data: runs } = await github.rest.actions.listWorkflowRuns({177                  owner, repo,178                  workflow_id: 'require_issue_link.yml',179                  head_sha: pr.head.sha,180                  status: 'failure',181                  per_page: 1,182                });183                if (runs.workflow_runs.length > 0) {184                  await github.rest.actions.reRunWorkflowFailedJobs({185                    owner, repo,186                    run_id: runs.workflow_runs[0].id,187                  });188                  console.log(`Re-ran failed require_issue_link run ${runs.workflow_runs[0].id} for PR #${prNumber}`);189                } else {190                  console.log(`No failed require_issue_link runs found for PR #${prNumber} — skipping re-run`);191                }192              } catch (e) {193                core.warning(`Could not re-run require_issue_link check for PR #${prNumber} (HTTP ${e.status ?? 'unknown'}): ${e.message}`);194              }195            }

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.