mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 16:47:29 +00:00
520 lines
18 KiB
YAML
520 lines
18 KiB
YAML
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
|
|
with:
|
|
app-id: "2729701"
|
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
|
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5
|
|
with:
|
|
configuration-path: .github/labeler.yml
|
|
repo-token: ${{ steps.app-token.outputs.token }}
|
|
sync-labels: true
|
|
- name: Apply PR size label
|
|
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
|
with:
|
|
github-token: ${{ steps.app-token.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 }}
|
|
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;
|
|
}
|
|
|
|
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],
|
|
});
|
|
}
|
|
|
|
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
|
|
with:
|
|
app-id: "2729701"
|
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
|
- name: Backfill PR labels
|
|
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
|
with:
|
|
github-token: ${{ steps.app-token.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";
|
|
}
|
|
|
|
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.`);
|
|
}
|
|
|
|
let 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
|
|
with:
|
|
app-id: "2729701"
|
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
|
- name: Apply maintainer or trusted-contributor label
|
|
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
|
with:
|
|
github-token: ${{ steps.app-token.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;
|
|
}
|
|
|
|
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],
|
|
});
|
|
}
|