.github/workflows/pr_labeler.yml YAML 214 lines View on github.com → Search inside
1# Unified PR labeler  applies size, file-based, title-based, and2# contributor classification labels in a single sequential workflow.3#4# Consolidates pr_labeler_file.yml, pr_labeler_title.yml,5# pr_size_labeler.yml, and PR-handling from tag-external-contributions.yml6# into one workflow to eliminate race conditions from concurrent label7# mutations. tag-external-issues.yml remains active for issue-only8# labeling. Backfill lives in pr_labeler_backfill.yml.9#10# Config and shared logic live in .github/scripts/pr-labeler-config.json11# and .github/scripts/pr-labeler.js  update those when adding partners.12#13# Setup Requirements:14# 1. Create a GitHub App with permissions:15#    - Repository: Pull requests (write)16#    - Repository: Issues (write)17#    - Organization: Members (read)18# 2. Install the app on your organization and this repository19# 3. Add these repository secrets:20#    - ORG_MEMBERSHIP_APP_ID: Your app's ID21#    - ORG_MEMBERSHIP_APP_PRIVATE_KEY: Your app's private key22#23# The GitHub App token is required to check private organization membership24# and to propagate label events to downstream workflows.2526name: "🏷️ PR Labeler"2728on:29  # Safe since we're not checking out or running the PR's code.30  # NEVER CHECK OUT UNTRUSTED CODE FROM A PR's HEAD IN A pull_request_target JOB.31  # Doing so would allow attackers to execute arbitrary code in the context of your repository.32  pull_request_target:33    types: [opened, synchronize, reopened, edited]3435permissions:36  contents: read3738concurrency:39  # Separate opened events so external/tier labels are never lost to cancellation40  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}-${{ github.event.action == 'opened' && 'opened' || 'update' }}41  cancel-in-progress: ${{ github.event.action != 'opened' }}4243jobs:44  label:45    runs-on: ubuntu-latest46    permissions:47      contents: read48      pull-requests: write49      issues: write5051    steps:52      # Checks out the BASE branch (safe for pull_request_target  never53      # the PR head). Needed to load .github/scripts/pr-labeler*.54      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v65556      - name: Generate GitHub App token57        if: github.event.action == 'opened'58        id: app-token59        uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v360        with:61          app-id: ${{ secrets.ORG_MEMBERSHIP_APP_ID }}62          private-key: ${{ secrets.ORG_MEMBERSHIP_APP_PRIVATE_KEY }}6364      - name: Verify App token65        if: github.event.action == 'opened'66        run: |67          if [ -z "${{ steps.app-token.outputs.token }}" ]; then68            echo "::error::GitHub App token generation failed — cannot classify contributor"69            exit 170          fi7172      - name: Check org membership73        if: github.event.action == 'opened'74        id: check-membership75        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.076        with:77          github-token: ${{ steps.app-token.outputs.token }}78          script: |79            const { owner, repo } = context.repo;80            const { h } = require('./.github/scripts/pr-labeler.js').loadAndInit(github, owner, repo, core);8182            const author = context.payload.sender.login;83            const { isExternal } = await h.checkMembership(84              author, context.payload.sender.type,85            );86            core.setOutput('is-external', isExternal ? 'true' : 'false');8788      - name: Apply PR labels89        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.090        env:91          IS_EXTERNAL: ${{ steps.check-membership.outputs.is-external }}92        with:93          github-token: ${{ secrets.GITHUB_TOKEN }}94          script: |95            const { owner, repo } = context.repo;96            const { h } = require('./.github/scripts/pr-labeler.js').loadAndInit(github, owner, repo, core);9798            const pr = context.payload.pull_request;99            if (!pr) return;100            const prNumber = pr.number;101            const action = context.payload.action;102103            const toAdd = new Set();104            const toRemove = new Set();105106            const currentLabels = (await github.paginate(107              github.rest.issues.listLabelsOnIssue,108              { owner, repo, issue_number: prNumber, per_page: 100 },109            )).map(l => l.name ?? '');110111            // ── Size + file labels (skip on 'edited' — files unchanged) ──112            if (action !== 'edited') {113              for (const sl of h.sizeLabels) await h.ensureLabel(sl);114115              const files = await github.paginate(github.rest.pulls.listFiles, {116                owner, repo, pull_number: prNumber, per_page: 100,117              });118119              const { totalChanged, sizeLabel } = h.computeSize(files);120              toAdd.add(sizeLabel);121              for (const sl of h.sizeLabels) {122                if (currentLabels.includes(sl) && sl !== sizeLabel) toRemove.add(sl);123              }124              console.log(`Size: ${totalChanged} changed lines → ${sizeLabel}`);125126              for (const label of h.matchFileLabels(files)) {127                toAdd.add(label);128              }129            }130131            // ── Title-based labels ──132            const { labels: titleLabels, typeLabel } = h.matchTitleLabels(pr.title || '');133            for (const label of titleLabels) toAdd.add(label);134135            // Remove stale type labels only when a type was detected136            if (typeLabel) {137              for (const tl of h.allTypeLabels) {138                if (currentLabels.includes(tl) && !titleLabels.has(tl)) toRemove.add(tl);139              }140            }141142            // ── Internal label (only on open, non-external contributors) ──143            // IS_EXTERNAL is empty string on non-opened events (step didn't144            // run), so this guard is only true for opened + internal.145            if (action === 'opened' && process.env.IS_EXTERNAL === 'false') {146              toAdd.add('internal');147            }148149            // ── Apply changes ──150            // Ensure all labels we're about to add exist (addLabels returns151            // 422 if any label in the batch is missing, which would prevent152            // ALL labels from being applied).153            for (const name of toAdd) {154              await h.ensureLabel(name);155            }156157            for (const name of toRemove) {158              if (toAdd.has(name)) continue;159              try {160                await github.rest.issues.removeLabel({161                  owner, repo, issue_number: prNumber, name,162                });163              } catch (e) {164                if (e.status !== 404) throw e;165              }166            }167168            const addList = [...toAdd];169            if (addList.length > 0) {170              await github.rest.issues.addLabels({171                owner, repo, issue_number: prNumber, labels: addList,172              });173            }174175            const removed = [...toRemove].filter(r => !toAdd.has(r));176            console.log(`PR #${prNumber}: +[${addList.join(', ')}] -[${removed.join(', ')}]`);177178      # Apply tier label BEFORE the external label so that179      # "trusted-contributor" is already present when the "external" labeled180      # event fires and triggers require_issue_link.yml.181      - name: Apply contributor tier label182        if: github.event.action == 'opened' && steps.check-membership.outputs.is-external == 'true'183        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0184        with:185          github-token: ${{ steps.app-token.outputs.token }}186          script: |187            const { owner, repo } = context.repo;188            const { h } = require('./.github/scripts/pr-labeler.js').loadAndInit(github, owner, repo, core);189190            const pr = context.payload.pull_request;191            await h.applyTierLabel(pr.number, pr.user.login);192193      - name: Add external label194        if: github.event.action == 'opened' && steps.check-membership.outputs.is-external == 'true'195        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0196        with:197          # Use App token so the "labeled" event propagates to downstream198          # workflows (e.g. require_issue_link.yml). Events created by the199          # default GITHUB_TOKEN do not trigger additional workflow runs.200          github-token: ${{ steps.app-token.outputs.token }}201          script: |202            const { owner, repo } = context.repo;203            const prNumber = context.payload.pull_request.number;204205            const { h } = require('./.github/scripts/pr-labeler.js').loadAndInit(github, owner, repo, core);206207            await h.ensureLabel('external');208            await github.rest.issues.addLabels({209              owner, repo,210              issue_number: prNumber,211              labels: ['external'],212            });213            console.log(`Added 'external' label to PR #${prNumber}`);

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.