.github/workflows/pr_labeler.yml YAML 215 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_CLIENT_ID: Your app's client 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    if: github.repository_owner == 'langchain-ai'46    runs-on: ubuntu-latest47    permissions:48      contents: read49      pull-requests: write50      issues: write5152    steps:53      # Checks out the BASE branch (safe for pull_request_target  never54      # the PR head). Needed to load .github/scripts/pr-labeler*.55      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v65657      - name: Generate GitHub App token58        if: github.event.action == 'opened'59        id: app-token60        uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v361        with:62          client-id: ${{ secrets.ORG_MEMBERSHIP_APP_CLIENT_ID }}63          private-key: ${{ secrets.ORG_MEMBERSHIP_APP_PRIVATE_KEY }}6465      - name: Verify App token66        if: github.event.action == 'opened'67        run: |68          if [ -z "${{ steps.app-token.outputs.token }}" ]; then69            echo "::error::GitHub App token generation failed — cannot classify contributor"70            exit 171          fi7273      - name: Check org membership74        if: github.event.action == 'opened'75        id: check-membership76        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.077        with:78          github-token: ${{ steps.app-token.outputs.token }}79          script: |80            const { owner, repo } = context.repo;81            const { h } = require('./.github/scripts/pr-labeler.js').loadAndInit(github, owner, repo, core);8283            const author = context.payload.sender.login;84            const { isExternal } = await h.checkMembership(85              author, context.payload.sender.type,86            );87            core.setOutput('is-external', isExternal ? 'true' : 'false');8889      - name: Apply PR labels90        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.091        env:92          IS_EXTERNAL: ${{ steps.check-membership.outputs.is-external }}93        with:94          github-token: ${{ secrets.GITHUB_TOKEN }}95          script: |96            const { owner, repo } = context.repo;97            const { h } = require('./.github/scripts/pr-labeler.js').loadAndInit(github, owner, repo, core);9899            const pr = context.payload.pull_request;100            if (!pr) return;101            const prNumber = pr.number;102            const action = context.payload.action;103104            const toAdd = new Set();105            const toRemove = new Set();106107            const currentLabels = (await github.paginate(108              github.rest.issues.listLabelsOnIssue,109              { owner, repo, issue_number: prNumber, per_page: 100 },110            )).map(l => l.name ?? '');111112            // ── Size + file labels (skip on 'edited' — files unchanged) ──113            if (action !== 'edited') {114              for (const sl of h.sizeLabels) await h.ensureLabel(sl);115116              const files = await github.paginate(github.rest.pulls.listFiles, {117                owner, repo, pull_number: prNumber, per_page: 100,118              });119120              const { totalChanged, sizeLabel } = h.computeSize(files);121              toAdd.add(sizeLabel);122              for (const sl of h.sizeLabels) {123                if (currentLabels.includes(sl) && sl !== sizeLabel) toRemove.add(sl);124              }125              console.log(`Size: ${totalChanged} changed lines → ${sizeLabel}`);126127              for (const label of h.matchFileLabels(files)) {128                toAdd.add(label);129              }130            }131132            // ── Title-based labels ──133            const { labels: titleLabels, typeLabel } = h.matchTitleLabels(pr.title || '');134            for (const label of titleLabels) toAdd.add(label);135136            // Remove stale type labels only when a type was detected137            if (typeLabel) {138              for (const tl of h.allTypeLabels) {139                if (currentLabels.includes(tl) && !titleLabels.has(tl)) toRemove.add(tl);140              }141            }142143            // ── Internal label (only on open, non-external contributors) ──144            // IS_EXTERNAL is empty string on non-opened events (step didn't145            // run), so this guard is only true for opened + internal.146            if (action === 'opened' && process.env.IS_EXTERNAL === 'false') {147              toAdd.add('internal');148            }149150            // ── Apply changes ──151            // Ensure all labels we're about to add exist (addLabels returns152            // 422 if any label in the batch is missing, which would prevent153            // ALL labels from being applied).154            for (const name of toAdd) {155              await h.ensureLabel(name);156            }157158            for (const name of toRemove) {159              if (toAdd.has(name)) continue;160              try {161                await github.rest.issues.removeLabel({162                  owner, repo, issue_number: prNumber, name,163                });164              } catch (e) {165                if (e.status !== 404) throw e;166              }167            }168169            const addList = [...toAdd];170            if (addList.length > 0) {171              await github.rest.issues.addLabels({172                owner, repo, issue_number: prNumber, labels: addList,173              });174            }175176            const removed = [...toRemove].filter(r => !toAdd.has(r));177            console.log(`PR #${prNumber}: +[${addList.join(', ')}] -[${removed.join(', ')}]`);178179      # Apply tier label BEFORE the external label so that180      # "trusted-contributor" is already present when the "external" labeled181      # event fires and triggers require_issue_link.yml.182      - name: Apply contributor tier label183        if: github.event.action == 'opened' && steps.check-membership.outputs.is-external == 'true'184        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0185        with:186          github-token: ${{ steps.app-token.outputs.token }}187          script: |188            const { owner, repo } = context.repo;189            const { h } = require('./.github/scripts/pr-labeler.js').loadAndInit(github, owner, repo, core);190191            const pr = context.payload.pull_request;192            await h.applyTierLabel(pr.number, pr.user.login);193194      - name: Add external label195        if: github.event.action == 'opened' && steps.check-membership.outputs.is-external == 'true'196        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0197        with:198          # Use App token so the "labeled" event propagates to downstream199          # workflows (e.g. require_issue_link.yml). Events created by the200          # default GITHUB_TOKEN do not trigger additional workflow runs.201          github-token: ${{ steps.app-token.outputs.token }}202          script: |203            const { owner, repo } = context.repo;204            const prNumber = context.payload.pull_request.number;205206            const { h } = require('./.github/scripts/pr-labeler.js').loadAndInit(github, owner, repo, core);207208            await h.ensureLabel('external');209            await github.rest.issues.addLabels({210              owner, repo,211              issue_number: prNumber,212              labels: ['external'],213            });214            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.