.github/workflows/reopen_on_assignment.yml YAML 197 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    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.

Get this view in your editor

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