name: Labeler on: pull_request_target: types: [opened, synchronize, reopened] issues: types: [opened] workflow_dispatch: inputs: max_prs: description: "Maximum number of open PRs to process (0 = all)" required: false default: "200" per_page: description: "PRs per page (1-100)" required: false default: "50" permissions: {} jobs: label: permissions: contents: read pull-requests: write runs-on: blacksmith-16vcpu-ubuntu-2404 steps: - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 id: app-token continue-on-error: true with: app-id: "2729701" private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 id: app-token-fallback if: steps.app-token.outcome == 'failure' with: app-id: "2971289" private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }} - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5 with: configuration-path: .github/labeler.yml repo-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }} sync-labels: true - name: Apply PR size label uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }} script: | const pullRequest = context.payload.pull_request; if (!pullRequest) { return; } const sizeLabels = ["size: XS", "size: S", "size: M", "size: L", "size: XL"]; const labelColor = "b76e79"; for (const label of sizeLabels) { try { await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, }); } catch (error) { if (error?.status !== 404) { throw error; } await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: labelColor, }); } } const files = await github.paginate(github.rest.pulls.listFiles, { owner: context.repo.owner, repo: context.repo.repo, pull_number: pullRequest.number, per_page: 100, }); const excludedLockfiles = new Set(["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"]); const totalChangedLines = files.reduce((total, file) => { const path = file.filename ?? ""; if (path === "docs.acp.md" || path.startsWith("docs/") || excludedLockfiles.has(path)) { return total; } return total + (file.additions ?? 0) + (file.deletions ?? 0); }, 0); let targetSizeLabel = "size: XL"; if (totalChangedLines < 50) { targetSizeLabel = "size: XS"; } else if (totalChangedLines < 200) { targetSizeLabel = "size: S"; } else if (totalChangedLines < 500) { targetSizeLabel = "size: M"; } else if (totalChangedLines < 1000) { targetSizeLabel = "size: L"; } const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, { owner: context.repo.owner, repo: context.repo.repo, issue_number: pullRequest.number, per_page: 100, }); for (const label of currentLabels) { const name = label.name ?? ""; if (!sizeLabels.includes(name)) { continue; } if (name === targetSizeLabel) { continue; } await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: pullRequest.number, name, }); } await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: pullRequest.number, labels: [targetSizeLabel], }); - name: Apply maintainer or trusted-contributor label uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }} script: | const login = context.payload.pull_request?.user?.login; if (!login) { return; } const repo = `${context.repo.owner}/${context.repo.repo}`; // const trustedLabel = "trusted-contributor"; // const experiencedLabel = "experienced-contributor"; // const trustedThreshold = 4; // const experiencedThreshold = 10; let isMaintainer = false; try { const membership = await github.rest.teams.getMembershipForUserInOrg({ org: context.repo.owner, team_slug: "maintainer", username: login, }); isMaintainer = membership?.data?.state === "active"; } catch (error) { if (error?.status !== 404) { throw error; } } if (isMaintainer) { await github.rest.issues.addLabels({ ...context.repo, issue_number: context.payload.pull_request.number, labels: ["maintainer"], }); return; } // trusted-contributor and experienced-contributor labels disabled. // const mergedQuery = `repo:${repo} is:pr is:merged author:${login}`; // let mergedCount = 0; // try { // const merged = await github.rest.search.issuesAndPullRequests({ // q: mergedQuery, // per_page: 1, // }); // mergedCount = merged?.data?.total_count ?? 0; // } catch (error) { // if (error?.status !== 422) { // throw error; // } // core.warning(`Skipping merged search for ${login}; treating as 0.`); // } // // if (mergedCount >= experiencedThreshold) { // await github.rest.issues.addLabels({ // ...context.repo, // issue_number: context.payload.pull_request.number, // labels: [experiencedLabel], // }); // return; // } // // if (mergedCount >= trustedThreshold) { // await github.rest.issues.addLabels({ // ...context.repo, // issue_number: context.payload.pull_request.number, // labels: [trustedLabel], // }); // } - name: Apply too-many-prs label uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }} script: | const pullRequest = context.payload.pull_request; if (!pullRequest) { return; } const activePrLimitLabel = "r: too-many-prs"; const activePrLimitOverrideLabel = "r: too-many-prs-override"; const activePrLimit = 10; const labelColor = "B60205"; const labelDescription = `Author has more than ${activePrLimit} active PRs in this repo`; const authorLogin = pullRequest.user?.login; if (!authorLogin) { return; } const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, { owner: context.repo.owner, repo: context.repo.repo, issue_number: pullRequest.number, per_page: 100, }); const labelNames = new Set( currentLabels .map((label) => (typeof label === "string" ? label : label?.name)) .filter((name) => typeof name === "string"), ); if (labelNames.has(activePrLimitOverrideLabel)) { if (labelNames.has(activePrLimitLabel)) { try { await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: pullRequest.number, name: activePrLimitLabel, }); } catch (error) { if (error?.status !== 404) { throw error; } } } return; } const ensureLabelExists = async () => { try { await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: activePrLimitLabel, }); } catch (error) { if (error?.status !== 404) { throw error; } await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: activePrLimitLabel, color: labelColor, description: labelDescription, }); } }; const isPrivilegedAuthor = async () => { if (pullRequest.author_association === "OWNER") { return true; } let isMaintainer = false; try { const membership = await github.rest.teams.getMembershipForUserInOrg({ org: context.repo.owner, team_slug: "maintainer", username: authorLogin, }); isMaintainer = membership?.data?.state === "active"; } catch (error) { if (error?.status !== 404) { throw error; } } if (isMaintainer) { return true; } try { const permission = await github.rest.repos.getCollaboratorPermissionLevel({ owner: context.repo.owner, repo: context.repo.repo, username: authorLogin, }); const roleName = (permission?.data?.role_name ?? "").toLowerCase(); return roleName === "admin" || roleName === "maintain"; } catch (error) { if (error?.status !== 404) { throw error; } } return false; }; if (await isPrivilegedAuthor()) { if (labelNames.has(activePrLimitLabel)) { try { await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: pullRequest.number, name: activePrLimitLabel, }); } catch (error) { if (error?.status !== 404) { throw error; } } } return; } let openPrCount = 0; try { const result = await github.rest.search.issuesAndPullRequests({ q: `repo:${context.repo.owner}/${context.repo.repo} is:pr is:open author:${authorLogin}`, per_page: 1, }); openPrCount = result?.data?.total_count ?? 0; } catch (error) { if (error?.status !== 422) { throw error; } core.warning(`Skipping open PR count for ${authorLogin}; treating as 0.`); } if (openPrCount > activePrLimit) { await ensureLabelExists(); if (!labelNames.has(activePrLimitLabel)) { await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: pullRequest.number, labels: [activePrLimitLabel], }); } return; } if (labelNames.has(activePrLimitLabel)) { try { await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: pullRequest.number, name: activePrLimitLabel, }); } catch (error) { if (error?.status !== 404) { throw error; } } } backfill-pr-labels: if: github.event_name == 'workflow_dispatch' permissions: contents: read pull-requests: write runs-on: blacksmith-16vcpu-ubuntu-2404 steps: - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 id: app-token continue-on-error: true with: app-id: "2729701" private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 id: app-token-fallback if: steps.app-token.outcome == 'failure' with: app-id: "2971289" private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }} - name: Backfill PR labels uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }} script: | const owner = context.repo.owner; const repo = context.repo.repo; const repoFull = `${owner}/${repo}`; const inputs = context.payload.inputs ?? {}; const maxPrsInput = inputs.max_prs ?? "200"; const perPageInput = inputs.per_page ?? "50"; const parsedMaxPrs = Number.parseInt(maxPrsInput, 10); const parsedPerPage = Number.parseInt(perPageInput, 10); const maxPrs = Number.isFinite(parsedMaxPrs) ? parsedMaxPrs : 200; const perPage = Number.isFinite(parsedPerPage) ? Math.min(100, Math.max(1, parsedPerPage)) : 50; const processAll = maxPrs <= 0; const maxCount = processAll ? Number.POSITIVE_INFINITY : Math.max(1, maxPrs); const sizeLabels = ["size: XS", "size: S", "size: M", "size: L", "size: XL"]; const labelColor = "b76e79"; // const trustedLabel = "trusted-contributor"; // const experiencedLabel = "experienced-contributor"; // const trustedThreshold = 4; // const experiencedThreshold = 10; const contributorCache = new Map(); async function ensureSizeLabels() { for (const label of sizeLabels) { try { await github.rest.issues.getLabel({ owner, repo, name: label, }); } catch (error) { if (error?.status !== 404) { throw error; } await github.rest.issues.createLabel({ owner, repo, name: label, color: labelColor, }); } } } async function resolveContributorLabel(login) { if (contributorCache.has(login)) { return contributorCache.get(login); } let isMaintainer = false; try { const membership = await github.rest.teams.getMembershipForUserInOrg({ org: owner, team_slug: "maintainer", username: login, }); isMaintainer = membership?.data?.state === "active"; } catch (error) { if (error?.status !== 404) { throw error; } } if (isMaintainer) { contributorCache.set(login, "maintainer"); return "maintainer"; } // trusted-contributor and experienced-contributor labels disabled. // const mergedQuery = `repo:${repoFull} is:pr is:merged author:${login}`; // let mergedCount = 0; // try { // const merged = await github.rest.search.issuesAndPullRequests({ // q: mergedQuery, // per_page: 1, // }); // mergedCount = merged?.data?.total_count ?? 0; // } catch (error) { // if (error?.status !== 422) { // throw error; // } // core.warning(`Skipping merged search for ${login}; treating as 0.`); // } const label = null; // if (mergedCount >= experiencedThreshold) { // label = experiencedLabel; // } else if (mergedCount >= trustedThreshold) { // label = trustedLabel; // } contributorCache.set(login, label); return label; } async function applySizeLabel(pullRequest, currentLabels, labelNames) { const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number: pullRequest.number, per_page: 100, }); const excludedLockfiles = new Set(["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"]); const totalChangedLines = files.reduce((total, file) => { const path = file.filename ?? ""; if (path === "docs.acp.md" || path.startsWith("docs/") || excludedLockfiles.has(path)) { return total; } return total + (file.additions ?? 0) + (file.deletions ?? 0); }, 0); let targetSizeLabel = "size: XL"; if (totalChangedLines < 50) { targetSizeLabel = "size: XS"; } else if (totalChangedLines < 200) { targetSizeLabel = "size: S"; } else if (totalChangedLines < 500) { targetSizeLabel = "size: M"; } else if (totalChangedLines < 1000) { targetSizeLabel = "size: L"; } for (const label of currentLabels) { const name = label.name ?? ""; if (!sizeLabels.includes(name)) { continue; } if (name === targetSizeLabel) { continue; } await github.rest.issues.removeLabel({ owner, repo, issue_number: pullRequest.number, name, }); labelNames.delete(name); } if (!labelNames.has(targetSizeLabel)) { await github.rest.issues.addLabels({ owner, repo, issue_number: pullRequest.number, labels: [targetSizeLabel], }); labelNames.add(targetSizeLabel); } } async function applyContributorLabel(pullRequest, labelNames) { const login = pullRequest.user?.login; if (!login) { return; } const label = await resolveContributorLabel(login); if (!label) { return; } if (labelNames.has(label)) { return; } await github.rest.issues.addLabels({ owner, repo, issue_number: pullRequest.number, labels: [label], }); labelNames.add(label); } await ensureSizeLabels(); let page = 1; let processed = 0; while (processed < maxCount) { const remaining = maxCount - processed; const pageSize = processAll ? perPage : Math.min(perPage, remaining); const { data: pullRequests } = await github.rest.pulls.list({ owner, repo, state: "open", per_page: pageSize, page, }); if (pullRequests.length === 0) { break; } for (const pullRequest of pullRequests) { if (!processAll && processed >= maxCount) { break; } const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, { owner, repo, issue_number: pullRequest.number, per_page: 100, }); const labelNames = new Set( currentLabels.map((label) => label.name).filter((name) => typeof name === "string"), ); await applySizeLabel(pullRequest, currentLabels, labelNames); await applyContributorLabel(pullRequest, labelNames); processed += 1; } if (pullRequests.length < pageSize) { break; } page += 1; } core.info(`Processed ${processed} pull requests.`); label-issues: permissions: issues: write runs-on: blacksmith-16vcpu-ubuntu-2404 steps: - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 id: app-token continue-on-error: true with: app-id: "2729701" private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 id: app-token-fallback if: steps.app-token.outcome == 'failure' with: app-id: "2971289" private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }} - name: Apply maintainer or trusted-contributor label uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }} script: | const login = context.payload.issue?.user?.login; if (!login) { return; } const repo = `${context.repo.owner}/${context.repo.repo}`; // const trustedLabel = "trusted-contributor"; // const experiencedLabel = "experienced-contributor"; // const trustedThreshold = 4; // const experiencedThreshold = 10; let isMaintainer = false; try { const membership = await github.rest.teams.getMembershipForUserInOrg({ org: context.repo.owner, team_slug: "maintainer", username: login, }); isMaintainer = membership?.data?.state === "active"; } catch (error) { if (error?.status !== 404) { throw error; } } if (isMaintainer) { await github.rest.issues.addLabels({ ...context.repo, issue_number: context.payload.issue.number, labels: ["maintainer"], }); return; } // trusted-contributor and experienced-contributor labels disabled. // const mergedQuery = `repo:${repo} is:pr is:merged author:${login}`; // let mergedCount = 0; // try { // const merged = await github.rest.search.issuesAndPullRequests({ // q: mergedQuery, // per_page: 1, // }); // mergedCount = merged?.data?.total_count ?? 0; // } catch (error) { // if (error?.status !== 422) { // throw error; // } // core.warning(`Skipping merged search for ${login}; treating as 0.`); // } // // if (mergedCount >= experiencedThreshold) { // await github.rest.issues.addLabels({ // ...context.repo, // issue_number: context.payload.issue.number, // labels: [experiencedLabel], // }); // return; // } // // if (mergedCount >= trustedThreshold) { // await github.rest.issues.addLabels({ // ...context.repo, // issue_number: context.payload.issue.number, // labels: [trustedLabel], // }); // }