From 26323774e48313971ddb394ff80deb75ab5d78c1 Mon Sep 17 00:00:00 2001 From: Chummy Date: Tue, 17 Feb 2026 15:32:49 +0800 Subject: [PATCH] fix(labels): unify issue contributor tiers and managed label metadata --- .github/workflows/auto-response.yml | 21 ++++++++++------- .github/workflows/labeler.yml | 36 +++++++++++++++++++++++------ docs/ci-map.md | 2 +- docs/pr-workflow.md | 2 +- 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/.github/workflows/auto-response.yml b/.github/workflows/auto-response.yml index 3c87ccf..4398085 100644 --- a/.github/workflows/auto-response.yml +++ b/.github/workflows/auto-response.yml @@ -28,7 +28,6 @@ jobs: const issue = context.payload.issue; const pullRequest = context.payload.pull_request; const target = issue ?? pullRequest; - const legacyTrustedContributorLabel = "trusted contributor"; const contributorTierRules = [ { label: "distinguished contributor", minMergedPRs: 50 }, { label: "principal contributor", minMergedPRs: 20 }, @@ -37,10 +36,7 @@ jobs: ]; const contributorTierLabels = contributorTierRules.map((rule) => rule.label); const contributorTierColor = "2ED9FF"; // Keep in sync with .github/workflows/labeler.yml - const managedContributorLabels = new Set([ - legacyTrustedContributorLabel, - ...contributorTierLabels, - ]); + const managedContributorLabels = new Set(contributorTierLabels); const action = context.payload.action; const changedLabel = context.payload.label?.name; @@ -52,18 +48,26 @@ jobs: const author = target.user; if (!author || author.type === "Bot") return; + function contributorTierDescription(rule) { + return `Contributor with ${rule.minMergedPRs}+ merged PRs.`; + } + async function ensureContributorTierLabels() { - for (const label of contributorTierLabels) { + for (const rule of contributorTierRules) { + const label = rule.label; + const expectedDescription = contributorTierDescription(rule); try { const { data: existing } = await github.rest.issues.getLabel({ owner, repo, name: label }); const currentColor = (existing.color || "").toUpperCase(); - if (currentColor !== contributorTierColor) { + const currentDescription = (existing.description || "").trim(); + if (currentColor !== contributorTierColor || currentDescription !== expectedDescription) { await github.rest.issues.updateLabel({ owner, repo, name: label, new_name: label, color: contributorTierColor, + description: expectedDescription, }); } } catch (error) { @@ -73,6 +77,7 @@ jobs: repo, name: label, color: contributorTierColor, + description: expectedDescription, }); } } @@ -105,7 +110,7 @@ jobs: }); const keepLabels = currentLabels .map((label) => label.name) - .filter((label) => label !== legacyTrustedContributorLabel && !contributorTierLabels.includes(label)); + .filter((label) => !contributorTierLabels.includes(label)); if (contributorTierLabel) { keepLabels.push(contributorTierLabel); diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index f27cebb..44371e5 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -44,8 +44,6 @@ jobs: manualRiskOverrideLabel, ...computedRiskLabels, ]); - const legacyTrustedContributorLabel = "trusted contributor"; - if ((action === "labeled" || action === "unlabeled") && !managedEnforcedLabels.has(changedLabel)) { core.info(`skip non-size/risk label event: ${changedLabel || "unknown"}`); return; @@ -442,13 +440,13 @@ jobs: return "Auto-managed label."; } - async function ensureLabel(name) { + async function ensureLabel(name, existing = null) { const expectedColor = colorForLabel(name); const expectedDescription = descriptionForLabel(name); try { - const { data: existing } = await github.rest.issues.getLabel({ owner, repo, name }); - const currentColor = (existing.color || "").toUpperCase(); - const currentDescription = (existing.description || "").trim(); + const current = existing || (await github.rest.issues.getLabel({ owner, repo, name })).data; + const currentColor = (current.color || "").toUpperCase(); + const currentDescription = (current.description || "").trim(); if (currentColor !== expectedColor || currentDescription !== expectedDescription) { await github.rest.issues.updateLabel({ 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) { const matchedTier = contributorTierRules.find((rule) => mergedCount >= rule.minMergedPRs); return matchedTier ? matchedTier.label : null; @@ -629,6 +650,8 @@ jobs: riskLabel = "risk: medium"; } + await ensureManagedRepoLabelsMetadata(); + const labelsToEnsure = new Set([ ...sizeLabels, ...computedRiskLabels, @@ -660,7 +683,6 @@ jobs: const hasManualRiskOverride = currentLabelNames.includes(manualRiskOverrideLabel); const keepNonManagedLabels = currentLabelNames.filter((label) => { if (label === manualRiskOverrideLabel) return true; - if (label === legacyTrustedContributorLabel) return false; if (contributorTierLabels.includes(label)) return false; if (sizeLabels.includes(label) || computedRiskLabels.includes(label)) return false; if (managedPathLabelSet.has(label)) return false; diff --git a/docs/ci-map.md b/docs/ci-map.md index 007d6fd..356f5c0 100644 --- a/docs/ci-map.md +++ b/docs/ci-map.md @@ -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 - `.github/workflows/auto-response.yml` (`Auto Response`) - 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) - Guardrail: label-based close routes are issue-only; PRs are never auto-closed by route labels - `.github/workflows/stale.yml` (`Stale`) diff --git a/docs/pr-workflow.md b/docs/pr-workflow.md index e9eba23..0838498 100644 --- a/docs/pr-workflow.md +++ b/docs/pr-workflow.md @@ -54,7 +54,7 @@ Maintain these branch protection rules on `main`: - 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). - 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