# Labels new issues and PRs from fixed rules, `/triage` for maintainers, asks short bug reports # for reproduction steps (`needs-info`, closed after 14 days unanswered), and a manual backfill # (CONTRIBUTING.md, "Labels and triage"). The logic is in scripts/triage/ (tested in ci.yml). name: Triage on: issues: types: [opened, labeled, edited] issue_comment: types: [created] # pull_request_target so PRs from forks can be labelled. It runs the base branch's # scripts/triage and only lists the PR's changed paths; the PR's code is never checked out. pull_request_target: types: [opened] schedule: - cron: '41 5 * * *' workflow_dispatch: inputs: dry_run: description: 'Only report what would be labelled' type: boolean default: true permissions: contents: read # to check out scripts/triage issues: write pull-requests: write concurrency: group: triage-${{ github.event.issue.number || github.event.pull_request.number || 'repo' }} cancel-in-progress: false jobs: triage: name: Triage bot runs-on: ubuntu-24.04 timeout-minutes: 30 # Filtered here so the common events (every comment, every label) start no runner. if: >- (github.event_name == 'issue_comment' && github.event.comment.user.type != 'Bot' && startsWith(github.event.comment.body, '/triage')) || ((github.event_name == 'issue_comment' || github.event.action == 'edited') && contains(github.event.issue.labels.*.name, 'needs-info') && github.event.sender.login == github.event.issue.user.login) || (github.event.action == 'labeled' && startsWith(github.event.label.name, 'priority') && contains(github.event.issue.labels.*.name, 'needs-triage')) || (github.event_name != 'issue_comment' && github.event.action != 'labeled' && github.event.action != 'edited') steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: sparse-checkout: scripts/triage - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: DRY_RUN: ${{ inputs.dry_run }} with: script: | const triage = require('./scripts/triage/triage.js'); const info = require('./scripts/triage/needs-info.js'); const event = context.eventName; const action = context.payload.action; const esc = (t) => String(t).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]); const th = (...names) => names.map((data) => ({ data, header: true })); if (event === 'issue_comment') { core.info(await triage.handleComment({ github, context })); await info.handleReply({ github, context }); } else if (event === 'issues' && action === 'labeled') { await triage.handleLabeled({ github, context }); } else if (event === 'issues' && action === 'edited') { await info.handleReply({ github, context }); } else if (event === 'schedule') { const closed = await info.sweep({ github, context }); core.info(`Closed for missing information: ${closed.join(', ') || 'none'}`); } else if (event === 'workflow_dispatch') { const dryRun = process.env.DRY_RUN !== 'false'; const plan = await triage.backfill({ github, context, dryRun }); const asks = await info.backfill({ github, context, dryRun }); await core.summary .addHeading(dryRun ? `Backfill dry run: ${plan.length} unlabelled` : `Backfill: ${plan.length} labelled`) .addTable([th('Item', 'Kind', 'Labels', 'Title'), ...plan.map((p) => [`#${p.number}`, p.pull ? 'PR' : 'issue', esc(p.labels.join(', ') || '—'), esc(p.title)])]) .addHeading(dryRun ? `needs-info dry run: ${asks.length} would be asked` : `needs-info: ${asks.length} asked`) .addTable([th('Issue', 'Asks', 'Title'), ...asks.map((p) => [`#${p.number}`, esc(p.asks.join(', ')), esc(p.title)])]) .write(); } else { core.info(`Labelled: ${(await triage.handleOpened({ github, context })).join(', ') || 'nothing'}`); if (event === 'issues') await info.handleOpened({ github, context }); }