| 1 | name: Contribution intake - issues |
| 2 | |
| 3 | on: |
| 4 | issues: |
| 5 | types: [opened, reopened] |
| 6 | |
| 7 | permissions: |
| 8 | contents: read |
| 9 | issues: write |
| 10 | |
| 11 | jobs: |
| 12 | gate: |
| 13 | runs-on: ubuntu-latest |
| 14 | timeout-minutes: 10 |
| 15 | steps: |
| 16 | # Labels only, never comments (founder, 2026-09-22): the intake note used |
| 17 | # to thank other bots, and a comment is noise a label does not make. |
| 18 | # Maintainers still see who needs triage; `/lgtmi` still skips it. |
| 19 | - name: Label new external issues for triage |
| 20 | uses: actions/github-script@v9 |
| 21 | with: |
| 22 | script: | |
| 23 | const issue = context.payload.issue; |
| 24 | const owner = context.repo.owner; |
| 25 | const repo = context.repo.repo; |
| 26 | const privileged = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']); |
| 27 | |
| 28 | async function label(name, color, description) { |
| 29 | try { |
| 30 | await github.rest.issues.createLabel({ owner, repo, name, color, description }); |
| 31 | } catch (error) { |
| 32 | if (error.status !== 422) throw error; // 422: label already exists |
| 33 | } |
| 34 | await github.rest.issues.addLabels({ |
| 35 | owner, |
| 36 | repo, |
| 37 | issue_number: issue.number, |
| 38 | labels: [name], |
| 39 | }); |
| 40 | } |
| 41 | |
| 42 | if (privileged.has(issue.author_association)) return; |
| 43 | // `user.type` is set by GitHub for app and bot accounts and cannot |
| 44 | // be spoofed by a login that merely ends in "[bot]". |
| 45 | if (issue.user.type === 'Bot') { |
| 46 | await label('bot-authored', 'ededed', 'Opened by a bot or app account'); |
| 47 | return; |
| 48 | } |
| 49 | |
| 50 | function parseAllowlist(content) { |
| 51 | return new Set( |
| 52 | content |
| 53 | .split(/\r?\n/) |
| 54 | .map(line => line.replace(/#.*/, '').trim().toLowerCase()) |
| 55 | .filter(Boolean) |
| 56 | ); |
| 57 | } |
| 58 | |
| 59 | async function readAllowlist() { |
| 60 | try { |
| 61 | const { data } = await github.rest.repos.getContent({ |
| 62 | owner, |
| 63 | repo, |
| 64 | path: '.github/APPROVED_CONTRIBUTORS', |
| 65 | ref: context.payload.repository.default_branch, |
| 66 | }); |
| 67 | if (Array.isArray(data) || data.type !== 'file') return new Set(); |
| 68 | return parseAllowlist( |
| 69 | Buffer.from(data.content, data.encoding || 'base64').toString('utf8') |
| 70 | ); |
| 71 | } catch (error) { |
| 72 | if (error.status === 404) return new Set(); |
| 73 | throw error; |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | const allowlist = await readAllowlist(); |
| 78 | const login = issue.user.login.toLowerCase(); |
| 79 | if ( |
| 80 | allowlist.has(`all:${login}`) || |
| 81 | allowlist.has(`issue:${login}`) |
| 82 | ) { |
| 83 | return; |
| 84 | } |
| 85 | |
| 86 | await label( |
| 87 | 'needs-triage', |
| 88 | 'fbca04', |
| 89 | 'New external report awaiting maintainer triage; repro, logs and version output help' |
| 90 | ); |
| 91 |