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 if: github.repository_owner == 'langchain-ai'23 runs-on: ubuntu-latest24 permissions:25 actions: write26 pull-requests: write2728 steps:29 - name: Find and reopen matching PRs30 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.031 with:32 script: |33 const { owner, repo } = context.repo;34 const issueNumber = context.payload.issue.number;35 const assignee = context.payload.assignee.login;3637 console.log(38 `Issue #${issueNumber} assigned to ${assignee} — searching for closed PRs to reopen`,39 );4041 const q = [42 `is:pr`,43 `is:closed`,44 `author:${assignee}`,45 `label:missing-issue-link`,46 `repo:${owner}/${repo}`,47 ].join(' ');4849 let data;50 try {51 ({ data } = await github.rest.search.issuesAndPullRequests({52 q,53 per_page: 30,54 }));55 } catch (e) {56 throw new Error(57 `Failed to search for closed PRs to reopen after assigning ${assignee} ` +58 `to #${issueNumber} (HTTP ${e.status ?? 'unknown'}): ${e.message}`,59 );60 }6162 if (data.total_count === 0) {63 console.log('No matching closed PRs found');64 return;65 }6667 console.log(`Found ${data.total_count} candidate PR(s)`);6869 // Must stay in sync with the identical pattern in require_issue_link.yml70 const pattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*#(\d+)/gi;7172 for (const item of data.items) {73 const prNumber = item.number;74 const body = item.body || '';75 const matches = [...body.matchAll(pattern)];76 const referencedIssues = matches.map(m => parseInt(m[1], 10));7778 if (!referencedIssues.includes(issueNumber)) {79 console.log(`PR #${prNumber} does not reference #${issueNumber} — skipping`);80 continue;81 }8283 // Skip if already bypassed84 const labels = item.labels.map(l => l.name);85 if (labels.includes('bypass-issue-check')) {86 console.log(`PR #${prNumber} already has bypass-issue-check — skipping`);87 continue;88 }8990 // Reopen first, remove label second — a closed PR that still has91 // missing-issue-link is recoverable; a closed PR with the label92 // stripped is invisible to both workflows.93 try {94 await github.rest.pulls.update({95 owner,96 repo,97 pull_number: prNumber,98 state: 'open',99 });100 console.log(`Reopened PR #${prNumber}`);101 } catch (e) {102 if (e.status === 422) {103 // Head branch deleted — PR is unrecoverable. Notify the104 // contributor so they know to open a new PR.105 core.warning(`Cannot reopen PR #${prNumber}: head branch was likely deleted`);106 try {107 await github.rest.issues.createComment({108 owner,109 repo,110 issue_number: prNumber,111 body:112 `You have been assigned to #${issueNumber}, but this PR could not be ` +113 `reopened because the head branch has been deleted. Please open a new ` +114 `PR referencing the issue.`,115 });116 } catch (commentErr) {117 core.warning(118 `Also failed to post comment on PR #${prNumber}: ${commentErr.message}`,119 );120 }121 continue;122 }123 // Transient errors (rate limit, 5xx) should fail the job so124 // the label is NOT removed and the run can be retried.125 throw e;126 }127128 // Remove missing-issue-link label only after successful reopen129 try {130 await github.rest.issues.removeLabel({131 owner,132 repo,133 issue_number: prNumber,134 name: 'missing-issue-link',135 });136 console.log(`Removed missing-issue-link from PR #${prNumber}`);137 } catch (e) {138 if (e.status !== 404) throw e;139 }140141 // Minimize stale enforcement comment (best-effort;142 // sync w/ require_issue_link.yml minimize blocks)143 try {144 const marker = '<!-- require-issue-link -->';145 const comments = await github.paginate(146 github.rest.issues.listComments,147 { owner, repo, issue_number: prNumber, per_page: 100 },148 );149 const stale = comments.find(c => c.body && c.body.includes(marker));150 if (stale) {151 await github.graphql(`152 mutation($id: ID!) {153 minimizeComment(input: {subjectId: $id, classifier: OUTDATED}) {154 minimizedComment { isMinimized }155 }156 }157 `, { id: stale.node_id });158 console.log(`Minimized stale enforcement comment ${stale.id} as outdated`);159 }160 } catch (e) {161 core.warning(`Could not minimize stale comment on PR #${prNumber}: ${e.message}`);162 }163164 // Re-run the failed require_issue_link check so it picks up the165 // new assignment. The re-run uses the original event payload but166 // fetches live issue data, so the assignment check will pass.167 //168 // Limitation: we look up runs by the PR's current head SHA. If the169 // contributor pushed new commits while the PR was closed, head.sha170 // won't match the SHA of the original failed run and the query will171 // return 0 results. This is acceptable because any push after reopen172 // triggers a fresh require_issue_link run against the new SHA.173 try {174 const { data: pr } = await github.rest.pulls.get({175 owner, repo, pull_number: prNumber,176 });177 const { data: runs } = await github.rest.actions.listWorkflowRuns({178 owner, repo,179 workflow_id: 'require_issue_link.yml',180 head_sha: pr.head.sha,181 status: 'failure',182 per_page: 1,183 });184 if (runs.workflow_runs.length > 0) {185 await github.rest.actions.reRunWorkflowFailedJobs({186 owner, repo,187 run_id: runs.workflow_runs[0].id,188 });189 console.log(`Re-ran failed require_issue_link run ${runs.workflow_runs[0].id} for PR #${prNumber}`);190 } else {191 console.log(`No failed require_issue_link runs found for PR #${prNumber} — skipping re-run`);192 }193 } catch (e) {194 core.warning(`Could not re-run require_issue_link check for PR #${prNumber} (HTTP ${e.status ?? 'unknown'}): ${e.message}`);195 }196 }
Findings
✓ No findings reported for this file.