ci(labeler): dedupe labels, add hover rules, and tune low-sat palette (#6)
* ci(labeler): dedupe scope labels and prioritize risk/size * ci(labeler): add hover rule descriptions and refresh label palette * style(labeler): reduce label saturation for better readability
This commit is contained in:
parent
b3fcdad3b5
commit
389496823d
3 changed files with 290 additions and 66 deletions
349
.github/workflows/labeler.yml
vendored
349
.github/workflows/labeler.yml
vendored
|
|
@ -50,7 +50,7 @@ jobs:
|
||||||
{ label: "experienced contributor", minMergedPRs: 10 },
|
{ label: "experienced contributor", minMergedPRs: 10 },
|
||||||
];
|
];
|
||||||
const contributorTierLabels = contributorTierRules.map((rule) => rule.label);
|
const contributorTierLabels = contributorTierRules.map((rule) => rule.label);
|
||||||
const contributorTierColor = "39FF14";
|
const contributorTierColor = "C5D7A2";
|
||||||
|
|
||||||
const managedPathLabels = [
|
const managedPathLabels = [
|
||||||
"docs",
|
"docs",
|
||||||
|
|
@ -82,6 +82,7 @@ jobs:
|
||||||
"scripts",
|
"scripts",
|
||||||
"dev",
|
"dev",
|
||||||
];
|
];
|
||||||
|
const managedPathLabelSet = new Set(managedPathLabels);
|
||||||
|
|
||||||
const moduleNamespaceRules = [
|
const moduleNamespaceRules = [
|
||||||
{ root: "src/agent/", prefix: "agent", coreEntries: new Set(["mod.rs"]) },
|
{ root: "src/agent/", prefix: "agent", coreEntries: new Set(["mod.rs"]) },
|
||||||
|
|
@ -107,72 +108,170 @@ jobs:
|
||||||
{ root: "src/tunnel/", prefix: "tunnel", coreEntries: new Set(["mod.rs"]) },
|
{ root: "src/tunnel/", prefix: "tunnel", coreEntries: new Set(["mod.rs"]) },
|
||||||
];
|
];
|
||||||
const managedModulePrefixes = [...new Set(moduleNamespaceRules.map((rule) => `${rule.prefix}:`))];
|
const managedModulePrefixes = [...new Set(moduleNamespaceRules.map((rule) => `${rule.prefix}:`))];
|
||||||
|
const modulePrefixPriority = [
|
||||||
|
"security",
|
||||||
|
"runtime",
|
||||||
|
"gateway",
|
||||||
|
"tool",
|
||||||
|
"provider",
|
||||||
|
"channel",
|
||||||
|
"config",
|
||||||
|
"memory",
|
||||||
|
"agent",
|
||||||
|
"integration",
|
||||||
|
"observability",
|
||||||
|
"onboard",
|
||||||
|
"service",
|
||||||
|
"tunnel",
|
||||||
|
"cron",
|
||||||
|
"daemon",
|
||||||
|
"doctor",
|
||||||
|
"health",
|
||||||
|
"heartbeat",
|
||||||
|
"skillforge",
|
||||||
|
"skills",
|
||||||
|
];
|
||||||
|
const pathLabelPriority = [
|
||||||
|
...modulePrefixPriority,
|
||||||
|
"core",
|
||||||
|
"ci",
|
||||||
|
"dependencies",
|
||||||
|
"tests",
|
||||||
|
"scripts",
|
||||||
|
"dev",
|
||||||
|
"docs",
|
||||||
|
];
|
||||||
|
const riskDisplayOrder = ["risk: high", "risk: medium", "risk: low", "risk: manual"];
|
||||||
|
const sizeDisplayOrder = ["size: XS", "size: S", "size: M", "size: L", "size: XL"];
|
||||||
|
const contributorDisplayOrder = [
|
||||||
|
"distinguished contributor",
|
||||||
|
"principal contributor",
|
||||||
|
"experienced contributor",
|
||||||
|
];
|
||||||
|
const modulePrefixPriorityIndex = new Map(
|
||||||
|
modulePrefixPriority.map((prefix, index) => [prefix, index])
|
||||||
|
);
|
||||||
|
const pathLabelPriorityIndex = new Map(
|
||||||
|
pathLabelPriority.map((label, index) => [label, index])
|
||||||
|
);
|
||||||
|
const riskPriorityIndex = new Map(
|
||||||
|
riskDisplayOrder.map((label, index) => [label, index])
|
||||||
|
);
|
||||||
|
const sizePriorityIndex = new Map(
|
||||||
|
sizeDisplayOrder.map((label, index) => [label, index])
|
||||||
|
);
|
||||||
|
const contributorPriorityIndex = new Map(
|
||||||
|
contributorDisplayOrder.map((label, index) => [label, index])
|
||||||
|
);
|
||||||
|
|
||||||
const staticLabelColors = {
|
const staticLabelColors = {
|
||||||
"size: XS": "BFDADC",
|
"size: XS": "E9F0F3",
|
||||||
"size: S": "BFDADC",
|
"size: S": "DDE8EE",
|
||||||
"size: M": "BFDADC",
|
"size: M": "CEDBE4",
|
||||||
"size: L": "BFDADC",
|
"size: L": "BDCEDB",
|
||||||
"size: XL": "BFDADC",
|
"size: XL": "AEBFCD",
|
||||||
"risk: low": "2EA043",
|
"risk: low": "B8D8B0",
|
||||||
"risk: medium": "FBCA04",
|
"risk: medium": "E2D391",
|
||||||
"risk: high": "D73A49",
|
"risk: high": "E0A090",
|
||||||
"risk: manual": "1F6FEB",
|
"risk: manual": "B7AFCF",
|
||||||
docs: "1D76DB",
|
docs: "B7CAD6",
|
||||||
dependencies: "C26F00",
|
dependencies: "D8C99A",
|
||||||
ci: "8250DF",
|
ci: "AFA2CF",
|
||||||
core: "24292F",
|
core: "4A4F4A",
|
||||||
agent: "2EA043",
|
agent: "9FC4B8",
|
||||||
channel: "1D76DB",
|
channel: "AFC4D6",
|
||||||
config: "0969DA",
|
config: "C3BCD8",
|
||||||
cron: "9A6700",
|
cron: "C7D6A5",
|
||||||
daemon: "57606A",
|
daemon: "7C7F95",
|
||||||
doctor: "0E8A8A",
|
doctor: "A8D6CD",
|
||||||
gateway: "D73A49",
|
gateway: "D8A58F",
|
||||||
health: "0E8A8A",
|
health: "A7DCCB",
|
||||||
heartbeat: "0E8A8A",
|
heartbeat: "B7ACE0",
|
||||||
integration: "8250DF",
|
integration: "8CAFC4",
|
||||||
memory: "1F883D",
|
memory: "7F96B2",
|
||||||
observability: "6E7781",
|
observability: "6D7482",
|
||||||
onboard: "B62DBA",
|
onboard: "E6E0C8",
|
||||||
provider: "5319E7",
|
provider: "8A7896",
|
||||||
runtime: "C26F00",
|
runtime: "8E88AF",
|
||||||
security: "B60205",
|
security: "D99084",
|
||||||
service: "0052CC",
|
service: "B3C7D6",
|
||||||
skillforge: "A371F7",
|
skillforge: "B9B2DA",
|
||||||
skills: "6F42C1",
|
skills: "C8C2E0",
|
||||||
tool: "D73A49",
|
tool: "9BCFBF",
|
||||||
tunnel: "0052CC",
|
tunnel: "8DAEC0",
|
||||||
tests: "0E8A16",
|
tests: "DCE9EE",
|
||||||
scripts: "B08800",
|
scripts: "E7DFC6",
|
||||||
dev: "6E7781",
|
dev: "C4D3DE",
|
||||||
|
};
|
||||||
|
const staticLabelDescriptions = {
|
||||||
|
"size: XS": "Auto size: <=80 non-doc changed lines.",
|
||||||
|
"size: S": "Auto size: 81-250 non-doc changed lines.",
|
||||||
|
"size: M": "Auto size: 251-500 non-doc changed lines.",
|
||||||
|
"size: L": "Auto size: 501-1000 non-doc changed lines.",
|
||||||
|
"size: XL": "Auto size: >1000 non-doc changed lines.",
|
||||||
|
"risk: low": "Auto risk: docs/chore-only paths.",
|
||||||
|
"risk: medium": "Auto risk: src/** or dependency/config changes.",
|
||||||
|
"risk: high": "Auto risk: security/runtime/gateway/tools/workflows.",
|
||||||
|
"risk: manual": "Maintainer override: keep selected risk label.",
|
||||||
|
docs: "Auto scope: docs/markdown/template files changed.",
|
||||||
|
dependencies: "Auto scope: dependency manifest/lock/policy changed.",
|
||||||
|
ci: "Auto scope: CI/workflow/hook files changed.",
|
||||||
|
core: "Auto scope: root src/*.rs files changed.",
|
||||||
|
agent: "Auto scope: src/agent/** changed.",
|
||||||
|
channel: "Auto scope: src/channels/** changed.",
|
||||||
|
config: "Auto scope: src/config/** changed.",
|
||||||
|
cron: "Auto scope: src/cron/** changed.",
|
||||||
|
daemon: "Auto scope: src/daemon/** changed.",
|
||||||
|
doctor: "Auto scope: src/doctor/** changed.",
|
||||||
|
gateway: "Auto scope: src/gateway/** changed.",
|
||||||
|
health: "Auto scope: src/health/** changed.",
|
||||||
|
heartbeat: "Auto scope: src/heartbeat/** changed.",
|
||||||
|
integration: "Auto scope: src/integrations/** changed.",
|
||||||
|
memory: "Auto scope: src/memory/** changed.",
|
||||||
|
observability: "Auto scope: src/observability/** changed.",
|
||||||
|
onboard: "Auto scope: src/onboard/** changed.",
|
||||||
|
provider: "Auto scope: src/providers/** changed.",
|
||||||
|
runtime: "Auto scope: src/runtime/** changed.",
|
||||||
|
security: "Auto scope: src/security/** changed.",
|
||||||
|
service: "Auto scope: src/service/** changed.",
|
||||||
|
skillforge: "Auto scope: src/skillforge/** changed.",
|
||||||
|
skills: "Auto scope: src/skills/** changed.",
|
||||||
|
tool: "Auto scope: src/tools/** changed.",
|
||||||
|
tunnel: "Auto scope: src/tunnel/** changed.",
|
||||||
|
tests: "Auto scope: tests/** changed.",
|
||||||
|
scripts: "Auto scope: scripts/** changed.",
|
||||||
|
dev: "Auto scope: dev/** changed.",
|
||||||
};
|
};
|
||||||
for (const label of contributorTierLabels) {
|
for (const label of contributorTierLabels) {
|
||||||
staticLabelColors[label] = contributorTierColor;
|
staticLabelColors[label] = contributorTierColor;
|
||||||
|
const rule = contributorTierRules.find((entry) => entry.label === label);
|
||||||
|
if (rule) {
|
||||||
|
staticLabelDescriptions[label] = `Contributor with ${rule.minMergedPRs}+ merged PRs.`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const modulePrefixColors = {
|
const modulePrefixColors = {
|
||||||
"agent:": "2EA043",
|
"agent:": "9FC4B8",
|
||||||
"channel:": "1D76DB",
|
"channel:": "AFC4D6",
|
||||||
"config:": "0969DA",
|
"config:": "C3BCD8",
|
||||||
"cron:": "9A6700",
|
"cron:": "C7D6A5",
|
||||||
"daemon:": "57606A",
|
"daemon:": "7C7F95",
|
||||||
"doctor:": "0E8A8A",
|
"doctor:": "A8D6CD",
|
||||||
"gateway:": "D73A49",
|
"gateway:": "D8A58F",
|
||||||
"health:": "0E8A8A",
|
"health:": "A7DCCB",
|
||||||
"heartbeat:": "0E8A8A",
|
"heartbeat:": "B7ACE0",
|
||||||
"integration:": "8250DF",
|
"integration:": "8CAFC4",
|
||||||
"memory:": "1F883D",
|
"memory:": "7F96B2",
|
||||||
"observability:": "6E7781",
|
"observability:": "6D7482",
|
||||||
"onboard:": "B62DBA",
|
"onboard:": "E6E0C8",
|
||||||
"provider:": "5319E7",
|
"provider:": "8A7896",
|
||||||
"runtime:": "C26F00",
|
"runtime:": "8E88AF",
|
||||||
"security:": "B60205",
|
"security:": "D99084",
|
||||||
"service:": "0052CC",
|
"service:": "B3C7D6",
|
||||||
"skillforge:": "A371F7",
|
"skillforge:": "B9B2DA",
|
||||||
"skills:": "6F42C1",
|
"skills:": "C8C2E0",
|
||||||
"tool:": "D73A49",
|
"tool:": "9BCFBF",
|
||||||
"tunnel:": "0052CC",
|
"tunnel:": "8DAEC0",
|
||||||
};
|
};
|
||||||
|
|
||||||
const providerKeywordHints = [
|
const providerKeywordHints = [
|
||||||
|
|
@ -248,6 +347,77 @@ jobs:
|
||||||
return pattern.test(text);
|
return pattern.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseModuleLabel(label) {
|
||||||
|
const separatorIndex = label.indexOf(":");
|
||||||
|
if (separatorIndex <= 0 || separatorIndex >= label.length - 1) return null;
|
||||||
|
return {
|
||||||
|
prefix: label.slice(0, separatorIndex),
|
||||||
|
segment: label.slice(separatorIndex + 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortByPriority(labels, priorityIndex) {
|
||||||
|
return [...new Set(labels)].sort((left, right) => {
|
||||||
|
const leftPriority = priorityIndex.has(left) ? priorityIndex.get(left) : Number.MAX_SAFE_INTEGER;
|
||||||
|
const rightPriority = priorityIndex.has(right)
|
||||||
|
? priorityIndex.get(right)
|
||||||
|
: Number.MAX_SAFE_INTEGER;
|
||||||
|
if (leftPriority !== rightPriority) return leftPriority - rightPriority;
|
||||||
|
return left.localeCompare(right);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortModuleLabels(labels) {
|
||||||
|
return [...new Set(labels)].sort((left, right) => {
|
||||||
|
const leftParsed = parseModuleLabel(left);
|
||||||
|
const rightParsed = parseModuleLabel(right);
|
||||||
|
if (!leftParsed || !rightParsed) return left.localeCompare(right);
|
||||||
|
|
||||||
|
const leftPrefixPriority = modulePrefixPriorityIndex.has(leftParsed.prefix)
|
||||||
|
? modulePrefixPriorityIndex.get(leftParsed.prefix)
|
||||||
|
: Number.MAX_SAFE_INTEGER;
|
||||||
|
const rightPrefixPriority = modulePrefixPriorityIndex.has(rightParsed.prefix)
|
||||||
|
? modulePrefixPriorityIndex.get(rightParsed.prefix)
|
||||||
|
: Number.MAX_SAFE_INTEGER;
|
||||||
|
|
||||||
|
if (leftPrefixPriority !== rightPrefixPriority) {
|
||||||
|
return leftPrefixPriority - rightPrefixPriority;
|
||||||
|
}
|
||||||
|
if (leftParsed.prefix !== rightParsed.prefix) {
|
||||||
|
return leftParsed.prefix.localeCompare(rightParsed.prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftIsCore = leftParsed.segment === "core";
|
||||||
|
const rightIsCore = rightParsed.segment === "core";
|
||||||
|
if (leftIsCore !== rightIsCore) return leftIsCore ? 1 : -1;
|
||||||
|
|
||||||
|
return leftParsed.segment.localeCompare(rightParsed.segment);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function refineModuleLabels(rawLabels) {
|
||||||
|
const refined = new Set(rawLabels);
|
||||||
|
const segmentsByPrefix = new Map();
|
||||||
|
|
||||||
|
for (const label of rawLabels) {
|
||||||
|
const parsed = parseModuleLabel(label);
|
||||||
|
if (!parsed) continue;
|
||||||
|
if (!segmentsByPrefix.has(parsed.prefix)) {
|
||||||
|
segmentsByPrefix.set(parsed.prefix, new Set());
|
||||||
|
}
|
||||||
|
segmentsByPrefix.get(parsed.prefix).add(parsed.segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [prefix, segments] of segmentsByPrefix) {
|
||||||
|
const hasSpecificSegment = [...segments].some((segment) => segment !== "core");
|
||||||
|
if (hasSpecificSegment) {
|
||||||
|
refined.delete(`${prefix}:core`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return refined;
|
||||||
|
}
|
||||||
|
|
||||||
function colorForLabel(label) {
|
function colorForLabel(label) {
|
||||||
if (staticLabelColors[label]) return staticLabelColors[label];
|
if (staticLabelColors[label]) return staticLabelColors[label];
|
||||||
const matchedPrefix = Object.keys(modulePrefixColors).find((prefix) => label.startsWith(prefix));
|
const matchedPrefix = Object.keys(modulePrefixColors).find((prefix) => label.startsWith(prefix));
|
||||||
|
|
@ -255,18 +425,35 @@ jobs:
|
||||||
return "BFDADC";
|
return "BFDADC";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function descriptionForLabel(label) {
|
||||||
|
if (staticLabelDescriptions[label]) return staticLabelDescriptions[label];
|
||||||
|
|
||||||
|
const parsed = parseModuleLabel(label);
|
||||||
|
if (parsed) {
|
||||||
|
if (parsed.segment === "core") {
|
||||||
|
return `Auto module: ${parsed.prefix} core files changed.`;
|
||||||
|
}
|
||||||
|
return `Auto module: ${parsed.prefix}/${parsed.segment} changed.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Auto-managed label.";
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureLabel(name) {
|
async function ensureLabel(name) {
|
||||||
const expectedColor = colorForLabel(name);
|
const expectedColor = colorForLabel(name);
|
||||||
|
const expectedDescription = descriptionForLabel(name);
|
||||||
try {
|
try {
|
||||||
const { data: existing } = await github.rest.issues.getLabel({ owner, repo, name });
|
const { data: existing } = await github.rest.issues.getLabel({ owner, repo, name });
|
||||||
const currentColor = (existing.color || "").toUpperCase();
|
const currentColor = (existing.color || "").toUpperCase();
|
||||||
if (currentColor !== expectedColor) {
|
const currentDescription = (existing.description || "").trim();
|
||||||
|
if (currentColor !== expectedColor || currentDescription !== expectedDescription) {
|
||||||
await github.rest.issues.updateLabel({
|
await github.rest.issues.updateLabel({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
name,
|
name,
|
||||||
new_name: name,
|
new_name: name,
|
||||||
color: expectedColor,
|
color: expectedColor,
|
||||||
|
description: expectedDescription,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -276,6 +463,7 @@ jobs:
|
||||||
repo,
|
repo,
|
||||||
name,
|
name,
|
||||||
color: expectedColor,
|
color: expectedColor,
|
||||||
|
description: expectedDescription,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -369,12 +557,25 @@ jobs:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refinedModuleLabels = refineModuleLabels(detectedModuleLabels);
|
||||||
|
const modulePrefixesWithLabels = new Set(
|
||||||
|
[...refinedModuleLabels]
|
||||||
|
.map((label) => parseModuleLabel(label)?.prefix)
|
||||||
|
.filter(Boolean)
|
||||||
|
);
|
||||||
|
|
||||||
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
|
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
issue_number: pr.number,
|
issue_number: pr.number,
|
||||||
});
|
});
|
||||||
const currentLabelNames = currentLabels.map((label) => label.name);
|
const currentLabelNames = currentLabels.map((label) => label.name);
|
||||||
|
const currentPathLabels = currentLabelNames.filter((label) => managedPathLabelSet.has(label));
|
||||||
|
|
||||||
|
const dedupedPathLabels = currentPathLabels.filter((label) => {
|
||||||
|
if (label === "core") return true;
|
||||||
|
return !modulePrefixesWithLabels.has(label);
|
||||||
|
});
|
||||||
|
|
||||||
const excludedLockfiles = new Set(["Cargo.lock"]);
|
const excludedLockfiles = new Set(["Cargo.lock"]);
|
||||||
const changedLines = files.reduce((total, file) => {
|
const changedLines = files.reduce((total, file) => {
|
||||||
|
|
@ -426,7 +627,7 @@ jobs:
|
||||||
manualRiskOverrideLabel,
|
manualRiskOverrideLabel,
|
||||||
...managedPathLabels,
|
...managedPathLabels,
|
||||||
...contributorTierLabels,
|
...contributorTierLabels,
|
||||||
...detectedModuleLabels,
|
...refinedModuleLabels,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
for (const label of labelsToEnsure) {
|
for (const label of labelsToEnsure) {
|
||||||
|
|
@ -454,6 +655,7 @@ jobs:
|
||||||
if (label === legacyTrustedContributorLabel) return false;
|
if (label === legacyTrustedContributorLabel) return false;
|
||||||
if (contributorTierLabels.includes(label)) return false;
|
if (contributorTierLabels.includes(label)) return false;
|
||||||
if (sizeLabels.includes(label) || computedRiskLabels.includes(label)) return false;
|
if (sizeLabels.includes(label) || computedRiskLabels.includes(label)) return false;
|
||||||
|
if (managedPathLabelSet.has(label)) return false;
|
||||||
if (managedModulePrefixes.some((prefix) => label.startsWith(prefix))) return false;
|
if (managedModulePrefixes.some((prefix) => label.startsWith(prefix))) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
@ -461,11 +663,28 @@ jobs:
|
||||||
const manualRiskSelection =
|
const manualRiskSelection =
|
||||||
currentLabelNames.find((label) => computedRiskLabels.includes(label)) || riskLabel;
|
currentLabelNames.find((label) => computedRiskLabels.includes(label)) || riskLabel;
|
||||||
|
|
||||||
const moduleLabelList = [...detectedModuleLabels];
|
const moduleLabelList = sortModuleLabels([...refinedModuleLabels]);
|
||||||
const contributorLabelList = contributorTierLabel ? [contributorTierLabel] : [];
|
const contributorLabelList = contributorTierLabel ? [contributorTierLabel] : [];
|
||||||
const nextLabels = hasManualRiskOverride
|
const selectedRiskLabels = hasManualRiskOverride
|
||||||
? [...new Set([...keepNonManagedLabels, ...moduleLabelList, ...contributorLabelList, sizeLabel, manualRiskSelection])]
|
? sortByPriority([manualRiskSelection, manualRiskOverrideLabel], riskPriorityIndex)
|
||||||
: [...new Set([...keepNonManagedLabels, ...moduleLabelList, ...contributorLabelList, sizeLabel, riskLabel])];
|
: sortByPriority([riskLabel], riskPriorityIndex);
|
||||||
|
const selectedSizeLabels = sortByPriority([sizeLabel], sizePriorityIndex);
|
||||||
|
const sortedContributorLabels = sortByPriority(contributorLabelList, contributorPriorityIndex);
|
||||||
|
const sortedPathLabels = sortByPriority(dedupedPathLabels, pathLabelPriorityIndex);
|
||||||
|
const sortedKeepNonManagedLabels = [...new Set(keepNonManagedLabels)].sort((left, right) =>
|
||||||
|
left.localeCompare(right)
|
||||||
|
);
|
||||||
|
|
||||||
|
const nextLabels = [
|
||||||
|
...new Set([
|
||||||
|
...selectedRiskLabels,
|
||||||
|
...selectedSizeLabels,
|
||||||
|
...sortedContributorLabels,
|
||||||
|
...moduleLabelList,
|
||||||
|
...sortedPathLabels,
|
||||||
|
...sortedKeepNonManagedLabels,
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
await github.rest.issues.setLabels({
|
await github.rest.issues.setLabels({
|
||||||
owner,
|
owner,
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,11 @@ Merge-blocking checks should stay small and deterministic. Optional checks are u
|
||||||
|
|
||||||
- `.github/workflows/labeler.yml` (`PR Labeler`)
|
- `.github/workflows/labeler.yml` (`PR Labeler`)
|
||||||
- Purpose: scope/path labels + size/risk labels + fine-grained module labels (`<module>:<component>`)
|
- Purpose: scope/path labels + size/risk labels + fine-grained module labels (`<module>:<component>`)
|
||||||
|
- Additional behavior: label descriptions are auto-managed as hover tooltips to explain each auto-judgment rule
|
||||||
- Additional behavior: provider-related keywords in provider/config/onboard/integration changes are promoted to `provider:*` labels (for example `provider:kimi`, `provider:deepseek`)
|
- Additional behavior: provider-related keywords in provider/config/onboard/integration changes are promoted to `provider:*` labels (for example `provider:kimi`, `provider:deepseek`)
|
||||||
|
- Additional behavior: hierarchical de-duplication keeps only the most specific scope labels (for example `tool:composio` suppresses `tool:core` and `tool`)
|
||||||
- Additional behavior: applies contributor tiers on PRs by merged PR count (`experienced` >=10, `principal` >=20, `distinguished` >=50)
|
- Additional behavior: applies contributor tiers on PRs by merged PR count (`experienced` >=10, `principal` >=20, `distinguished` >=50)
|
||||||
|
- Additional behavior: final label set is priority-sorted (`risk:*` first, then `size:*`, then contributor tier, then module/path labels)
|
||||||
- Additional behavior: risk + size labels are auto-corrected on manual PR label edits (`labeled`/`unlabeled` events); apply `risk: manual` when maintainers intentionally override automated risk selection
|
- Additional behavior: risk + size labels are auto-corrected on manual PR label edits (`labeled`/`unlabeled` events); apply `risk: manual` when maintainers intentionally override automated risk selection
|
||||||
- High-risk heuristic paths: `src/security/**`, `src/runtime/**`, `src/gateway/**`, `src/tools/**`, `.github/workflows/**`
|
- High-risk heuristic paths: `src/security/**`, `src/runtime/**`, `src/gateway/**`, `src/tools/**`, `.github/workflows/**`
|
||||||
- Guardrail: maintainers can apply `risk: manual` to freeze automated risk recalculation
|
- Guardrail: maintainers can apply `risk: manual` to freeze automated risk recalculation
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,9 @@ Maintain these branch protection rules on `main`:
|
||||||
### Step A: Intake
|
### Step A: Intake
|
||||||
|
|
||||||
- Contributor opens PR with full `.github/pull_request_template.md`.
|
- Contributor opens PR with full `.github/pull_request_template.md`.
|
||||||
- `PR Labeler` applies scope/path labels + size labels + risk labels + module labels (for example `channel:telegram`, `provider:kimi`, `tool:shell`) and contributor tiers by merged PR count (`experienced` >=10, `principal` >=20, `distinguished` >=50).
|
- `PR Labeler` applies scope/path labels + size labels + risk labels + module labels (for example `channel:telegram`, `provider:kimi`, `tool:shell`) and contributor tiers by merged PR count (`experienced` >=10, `principal` >=20, `distinguished` >=50), while de-duplicating less-specific scope labels when a more specific module label is present.
|
||||||
|
- Label ordering is priority-first: `risk:*` -> `size:*` -> contributor tier -> module/path labels.
|
||||||
|
- Hovering a label in GitHub shows its auto-managed description (rule/threshold summary).
|
||||||
- `Auto Response` posts first-time guidance and handles label-driven routing for low-signal items.
|
- `Auto Response` posts first-time guidance and handles label-driven routing for low-signal items.
|
||||||
|
|
||||||
### Step B: Validation
|
### Step B: Validation
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue