ci: shield issues with an open PR from the stale bot #1
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Mark linked issues in-progress | |
| # When an open PR references an issue with Closes/Fixes/Resolves, apply the | |
| # 'in-progress' label to that issue so the stale bot leaves it alone (the | |
| # stale workflow already exempts 'in-progress'). Remove the label when the | |
| # PR closes without merging, so a genuinely abandoned PR does not keep its | |
| # referenced issues shielded forever. | |
| # | |
| # actions/stale looks at issue-level events only; a PR that references an | |
| # issue does not reset the issue's stale timer or move it off the stale | |
| # label. This workflow bridges that gap. | |
| # | |
| # Security note: the script only reads pr.body, extracts decimal issue | |
| # numbers via a fixed regex, and passes those numbers to the REST API. | |
| # Body content is never expanded into a run: command or a shell. | |
| on: | |
| pull_request_target: | |
| types: [opened, edited, reopened, synchronize, ready_for_review, closed] | |
| permissions: | |
| issues: write | |
| pull-requests: read | |
| jobs: | |
| link: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const pr = context.payload.pull_request; | |
| const body = pr.body || ''; | |
| const re = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi; | |
| // Cross-repo references (owner/repo#123) are intentionally skipped; | |
| // this workflow only labels issues in the current repo. | |
| const MAX_REFS = 50; // cap runaway PR bodies from forks (pull_request_target). | |
| const extract = (text) => [...new Set( | |
| [...(text || '').matchAll(re)].map(m => Number(m[1])) | |
| )].slice(0, MAX_REFS); | |
| const current = extract(body); | |
| // On `edited`, compute which references were REMOVED so their | |
| // labels come off. Without this, a PR that once said "Closes #42" | |
| // and no longer does would leave #42 shielded indefinitely. | |
| let removed = []; | |
| if (context.payload.action === 'edited') { | |
| const prevBody = context.payload.changes?.body?.from; | |
| if (prevBody !== undefined) { | |
| const previous = extract(prevBody); | |
| const now = new Set(current); | |
| removed = previous.filter(n => !now.has(n)); | |
| } | |
| } | |
| // Apply the label while the PR is open. Remove it when the PR | |
| // closes without merging (merged PRs also close, but the issue | |
| // will be auto-closed by GitHub once the merge lands, so the | |
| // in-progress label on it is harmless). Also remove on `edited` | |
| // when a reference was deleted from the body. | |
| const shouldLabel = pr.state === 'open'; | |
| const closeUnlabel = pr.state === 'closed' && !pr.merged ? current : []; | |
| const toUnlabel = [...new Set([...removed, ...closeUnlabel])]; | |
| if (current.length === 0 && toUnlabel.length === 0) { | |
| core.info('No Closes/Fixes/Resolves references to process; nothing to do.'); | |
| return; | |
| } | |
| const removeLabelSafe = async (n) => { | |
| await github.rest.issues.removeLabel({ | |
| ...context.repo, | |
| issue_number: n, | |
| name: 'in-progress', | |
| }).catch(err => { | |
| // 404 just means the label was not present; not an error. | |
| if (err.status !== 404) throw err; | |
| }); | |
| core.info(`#${n}: removed in-progress`); | |
| }; | |
| if (shouldLabel) { | |
| for (const n of current) { | |
| try { | |
| await github.rest.issues.addLabels({ | |
| ...context.repo, | |
| issue_number: n, | |
| labels: ['in-progress'], | |
| }); | |
| core.info(`#${n}: added in-progress`); | |
| } catch (err) { | |
| core.warning(`#${n}: ${err.message}`); | |
| } | |
| } | |
| } | |
| for (const n of toUnlabel) { | |
| try { | |
| await removeLabelSafe(n); | |
| } catch (err) { | |
| core.warning(`#${n}: ${err.message}`); | |
| } | |
| } |