Merge pull request #491 from zeroclaw-labs/fix/label-governance-unify
fix(labels): unify issue contributor tiers and label metadata sync
This commit is contained in:
commit
bc38994867
4 changed files with 44 additions and 17 deletions
21
.github/workflows/auto-response.yml
vendored
21
.github/workflows/auto-response.yml
vendored
|
|
@ -28,7 +28,6 @@ jobs:
|
||||||
const issue = context.payload.issue;
|
const issue = context.payload.issue;
|
||||||
const pullRequest = context.payload.pull_request;
|
const pullRequest = context.payload.pull_request;
|
||||||
const target = issue ?? pullRequest;
|
const target = issue ?? pullRequest;
|
||||||
const legacyTrustedContributorLabel = "trusted contributor";
|
|
||||||
const contributorTierRules = [
|
const contributorTierRules = [
|
||||||
{ label: "distinguished contributor", minMergedPRs: 50 },
|
{ label: "distinguished contributor", minMergedPRs: 50 },
|
||||||
{ label: "principal contributor", minMergedPRs: 20 },
|
{ label: "principal contributor", minMergedPRs: 20 },
|
||||||
|
|
@ -37,10 +36,7 @@ jobs:
|
||||||
];
|
];
|
||||||
const contributorTierLabels = contributorTierRules.map((rule) => rule.label);
|
const contributorTierLabels = contributorTierRules.map((rule) => rule.label);
|
||||||
const contributorTierColor = "2ED9FF"; // Keep in sync with .github/workflows/labeler.yml
|
const contributorTierColor = "2ED9FF"; // Keep in sync with .github/workflows/labeler.yml
|
||||||
const managedContributorLabels = new Set([
|
const managedContributorLabels = new Set(contributorTierLabels);
|
||||||
legacyTrustedContributorLabel,
|
|
||||||
...contributorTierLabels,
|
|
||||||
]);
|
|
||||||
const action = context.payload.action;
|
const action = context.payload.action;
|
||||||
const changedLabel = context.payload.label?.name;
|
const changedLabel = context.payload.label?.name;
|
||||||
|
|
||||||
|
|
@ -52,18 +48,26 @@ jobs:
|
||||||
const author = target.user;
|
const author = target.user;
|
||||||
if (!author || author.type === "Bot") return;
|
if (!author || author.type === "Bot") return;
|
||||||
|
|
||||||
|
function contributorTierDescription(rule) {
|
||||||
|
return `Contributor with ${rule.minMergedPRs}+ merged PRs.`;
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureContributorTierLabels() {
|
async function ensureContributorTierLabels() {
|
||||||
for (const label of contributorTierLabels) {
|
for (const rule of contributorTierRules) {
|
||||||
|
const label = rule.label;
|
||||||
|
const expectedDescription = contributorTierDescription(rule);
|
||||||
try {
|
try {
|
||||||
const { data: existing } = await github.rest.issues.getLabel({ owner, repo, name: label });
|
const { data: existing } = await github.rest.issues.getLabel({ owner, repo, name: label });
|
||||||
const currentColor = (existing.color || "").toUpperCase();
|
const currentColor = (existing.color || "").toUpperCase();
|
||||||
if (currentColor !== contributorTierColor) {
|
const currentDescription = (existing.description || "").trim();
|
||||||
|
if (currentColor !== contributorTierColor || currentDescription !== expectedDescription) {
|
||||||
await github.rest.issues.updateLabel({
|
await github.rest.issues.updateLabel({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
name: label,
|
name: label,
|
||||||
new_name: label,
|
new_name: label,
|
||||||
color: contributorTierColor,
|
color: contributorTierColor,
|
||||||
|
description: expectedDescription,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -73,6 +77,7 @@ jobs:
|
||||||
repo,
|
repo,
|
||||||
name: label,
|
name: label,
|
||||||
color: contributorTierColor,
|
color: contributorTierColor,
|
||||||
|
description: expectedDescription,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -105,7 +110,7 @@ jobs:
|
||||||
});
|
});
|
||||||
const keepLabels = currentLabels
|
const keepLabels = currentLabels
|
||||||
.map((label) => label.name)
|
.map((label) => label.name)
|
||||||
.filter((label) => label !== legacyTrustedContributorLabel && !contributorTierLabels.includes(label));
|
.filter((label) => !contributorTierLabels.includes(label));
|
||||||
|
|
||||||
if (contributorTierLabel) {
|
if (contributorTierLabel) {
|
||||||
keepLabels.push(contributorTierLabel);
|
keepLabels.push(contributorTierLabel);
|
||||||
|
|
|
||||||
36
.github/workflows/labeler.yml
vendored
36
.github/workflows/labeler.yml
vendored
|
|
@ -44,8 +44,6 @@ jobs:
|
||||||
manualRiskOverrideLabel,
|
manualRiskOverrideLabel,
|
||||||
...computedRiskLabels,
|
...computedRiskLabels,
|
||||||
]);
|
]);
|
||||||
const legacyTrustedContributorLabel = "trusted contributor";
|
|
||||||
|
|
||||||
if ((action === "labeled" || action === "unlabeled") && !managedEnforcedLabels.has(changedLabel)) {
|
if ((action === "labeled" || action === "unlabeled") && !managedEnforcedLabels.has(changedLabel)) {
|
||||||
core.info(`skip non-size/risk label event: ${changedLabel || "unknown"}`);
|
core.info(`skip non-size/risk label event: ${changedLabel || "unknown"}`);
|
||||||
return;
|
return;
|
||||||
|
|
@ -442,13 +440,13 @@ jobs:
|
||||||
return "Auto-managed label.";
|
return "Auto-managed label.";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureLabel(name) {
|
async function ensureLabel(name, existing = null) {
|
||||||
const expectedColor = colorForLabel(name);
|
const expectedColor = colorForLabel(name);
|
||||||
const expectedDescription = descriptionForLabel(name);
|
const expectedDescription = descriptionForLabel(name);
|
||||||
try {
|
try {
|
||||||
const { data: existing } = await github.rest.issues.getLabel({ owner, repo, name });
|
const current = existing || (await github.rest.issues.getLabel({ owner, repo, name })).data;
|
||||||
const currentColor = (existing.color || "").toUpperCase();
|
const currentColor = (current.color || "").toUpperCase();
|
||||||
const currentDescription = (existing.description || "").trim();
|
const currentDescription = (current.description || "").trim();
|
||||||
if (currentColor !== expectedColor || currentDescription !== expectedDescription) {
|
if (currentColor !== expectedColor || currentDescription !== expectedDescription) {
|
||||||
await github.rest.issues.updateLabel({
|
await github.rest.issues.updateLabel({
|
||||||
owner,
|
owner,
|
||||||
|
|
@ -471,6 +469,29 @@ jobs:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isManagedLabel(label) {
|
||||||
|
if (label === manualRiskOverrideLabel) return true;
|
||||||
|
if (sizeLabels.includes(label) || computedRiskLabels.includes(label)) return true;
|
||||||
|
if (managedPathLabelSet.has(label)) return true;
|
||||||
|
if (contributorTierLabels.includes(label)) return true;
|
||||||
|
if (managedModulePrefixes.some((prefix) => label.startsWith(prefix))) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureManagedRepoLabelsMetadata() {
|
||||||
|
const repoLabels = await github.paginate(github.rest.issues.listLabelsForRepo, {
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
per_page: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const existingLabel of repoLabels) {
|
||||||
|
const labelName = existingLabel.name || "";
|
||||||
|
if (!isManagedLabel(labelName)) continue;
|
||||||
|
await ensureLabel(labelName, existingLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function selectContributorTier(mergedCount) {
|
function selectContributorTier(mergedCount) {
|
||||||
const matchedTier = contributorTierRules.find((rule) => mergedCount >= rule.minMergedPRs);
|
const matchedTier = contributorTierRules.find((rule) => mergedCount >= rule.minMergedPRs);
|
||||||
return matchedTier ? matchedTier.label : null;
|
return matchedTier ? matchedTier.label : null;
|
||||||
|
|
@ -629,6 +650,8 @@ jobs:
|
||||||
riskLabel = "risk: medium";
|
riskLabel = "risk: medium";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await ensureManagedRepoLabelsMetadata();
|
||||||
|
|
||||||
const labelsToEnsure = new Set([
|
const labelsToEnsure = new Set([
|
||||||
...sizeLabels,
|
...sizeLabels,
|
||||||
...computedRiskLabels,
|
...computedRiskLabels,
|
||||||
|
|
@ -660,7 +683,6 @@ jobs:
|
||||||
const hasManualRiskOverride = currentLabelNames.includes(manualRiskOverrideLabel);
|
const hasManualRiskOverride = currentLabelNames.includes(manualRiskOverrideLabel);
|
||||||
const keepNonManagedLabels = currentLabelNames.filter((label) => {
|
const keepNonManagedLabels = currentLabelNames.filter((label) => {
|
||||||
if (label === manualRiskOverrideLabel) return true;
|
if (label === manualRiskOverrideLabel) return true;
|
||||||
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 (managedPathLabelSet.has(label)) return false;
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ Merge-blocking checks should stay small and deterministic. Optional checks are u
|
||||||
- Guardrail: maintainers can apply `risk: manual` to freeze automated risk recalculation
|
- Guardrail: maintainers can apply `risk: manual` to freeze automated risk recalculation
|
||||||
- `.github/workflows/auto-response.yml` (`Auto Response`)
|
- `.github/workflows/auto-response.yml` (`Auto Response`)
|
||||||
- Purpose: first-time contributor onboarding + label-driven response routing (`r:support`, `r:needs-repro`, etc.)
|
- Purpose: first-time contributor onboarding + label-driven response routing (`r:support`, `r:needs-repro`, etc.)
|
||||||
- Additional behavior: applies contributor tiers on issues by merged PR count (`trusted` >=5, `experienced` >=10, `principal` >=20, `distinguished` >=50)
|
- Additional behavior: applies contributor tiers on issues by merged PR count (`trusted` >=5, `experienced` >=10, `principal` >=20, `distinguished` >=50), matching PR tier thresholds exactly
|
||||||
- Additional behavior: contributor-tier labels are treated as automation-managed (manual add/remove on PR/issue is auto-corrected)
|
- Additional behavior: contributor-tier labels are treated as automation-managed (manual add/remove on PR/issue is auto-corrected)
|
||||||
- Guardrail: label-based close routes are issue-only; PRs are never auto-closed by route labels
|
- Guardrail: label-based close routes are issue-only; PRs are never auto-closed by route labels
|
||||||
- `.github/workflows/stale.yml` (`Stale`)
|
- `.github/workflows/stale.yml` (`Stale`)
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ Maintain these branch protection rules on `main`:
|
||||||
- Label ordering is priority-first: `risk:*` -> `size:*` -> contributor tier -> module/path labels.
|
- 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).
|
- Hovering a label in GitHub shows its auto-managed description (rule/threshold summary).
|
||||||
- Managed label colors are arranged by display order to create a smooth gradient across long label rows.
|
- Managed label colors are arranged by display order to create a smooth gradient across long label rows.
|
||||||
- `Auto Response` posts first-time guidance and handles label-driven routing for low-signal items.
|
- `Auto Response` posts first-time guidance, handles label-driven routing for low-signal items, and auto-applies issue contributor tiers using the same thresholds as `PR Labeler` (`trusted` >=5, `experienced` >=10, `principal` >=20, `distinguished` >=50).
|
||||||
|
|
||||||
### Step B: Validation
|
### Step B: Validation
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue