From 3a25f4fa3a30af03aaedfb8a3fa7f808befecb2f Mon Sep 17 00:00:00 2001 From: Chummy Date: Mon, 16 Feb 2026 19:52:14 +0800 Subject: [PATCH] ci(labeler): enforce ordered gradient palette and compact module labels --- .github/workflows/labeler.yml | 177 ++++++++++++++++++---------------- docs/ci-map.md | 1 + docs/pr-workflow.md | 1 + 3 files changed, 96 insertions(+), 83 deletions(-) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index a05b3f6..e7cfa27 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -108,39 +108,39 @@ jobs: { root: "src/tunnel/", prefix: "tunnel", coreEntries: new Set(["mod.rs"]) }, ]; const managedModulePrefixes = [...new Set(moduleNamespaceRules.map((rule) => `${rule.prefix}:`))]; - const modulePrefixPriority = [ - "security", - "runtime", - "gateway", + const otherLabelDisplayOrder = [ + "health", "tool", - "provider", - "channel", - "config", - "memory", "agent", - "integration", - "observability", - "onboard", + "memory", + "channel", "service", + "integration", "tunnel", - "cron", + "config", + "observability", + "docs", + "dev", + "tests", + "skills", + "skillforge", + "provider", + "runtime", + "heartbeat", "daemon", "doctor", - "health", - "heartbeat", - "skillforge", - "skills", - ]; - const pathLabelPriority = [ - ...modulePrefixPriority, - "core", + "onboard", + "cron", "ci", "dependencies", - "tests", + "gateway", + "security", + "core", "scripts", - "dev", - "docs", ]; + const modulePrefixSet = new Set(moduleNamespaceRules.map((rule) => rule.prefix)); + const modulePrefixPriority = otherLabelDisplayOrder.filter((label) => modulePrefixSet.has(label)); + const pathLabelPriority = [...otherLabelDisplayOrder]; const riskDisplayOrder = ["risk: high", "risk: medium", "risk: low", "risk: manual"]; const sizeDisplayOrder = ["size: XS", "size: S", "size: M", "size: L", "size: XL"]; const contributorDisplayOrder = [ @@ -164,44 +164,72 @@ jobs: contributorDisplayOrder.map((label, index) => [label, index]) ); + function hslToHex(h, s, l) { + const saturation = s / 100; + const lightness = l / 100; + const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation; + const huePrime = ((h % 360) + 360) % 360 / 60; + const x = chroma * (1 - Math.abs((huePrime % 2) - 1)); + let red = 0; + let green = 0; + let blue = 0; + + if (huePrime >= 0 && huePrime < 1) { + red = chroma; + green = x; + } else if (huePrime < 2) { + red = x; + green = chroma; + } else if (huePrime < 3) { + green = chroma; + blue = x; + } else if (huePrime < 4) { + green = x; + blue = chroma; + } else if (huePrime < 5) { + red = x; + blue = chroma; + } else { + red = chroma; + blue = x; + } + + const match = lightness - chroma / 2; + const toHex = (value) => + Math.round((value + match) * 255) + .toString(16) + .padStart(2, "0"); + + return `${toHex(red)}${toHex(green)}${toHex(blue)}`.toUpperCase(); + } + + function buildGradientColorMap(labels) { + const colorMap = {}; + const lastIndex = Math.max(labels.length - 1, 1); + + for (let index = 0; index < labels.length; index += 1) { + const ratio = index / lastIndex; + const hue = 155 - ratio * 147; + const saturation = 34 + ratio * 8; + const lightness = 74 - ratio * 8; + colorMap[labels[index]] = hslToHex(hue, saturation, lightness); + } + + return colorMap; + } + + const otherLabelColors = buildGradientColorMap(otherLabelDisplayOrder); const staticLabelColors = { - "size: XS": "E9F0F3", - "size: S": "DDE8EE", - "size: M": "CEDBE4", - "size: L": "BDCEDB", - "size: XL": "AEBFCD", - "risk: low": "B8D8B0", - "risk: medium": "E2D391", - "risk: high": "E0A090", - "risk: manual": "B7AFCF", - docs: "B7CAD6", - dependencies: "D8C99A", - ci: "AFA2CF", - core: "4A4F4A", - agent: "9FC4B8", - channel: "AFC4D6", - config: "C3BCD8", - cron: "C7D6A5", - daemon: "7C7F95", - doctor: "A8D6CD", - gateway: "D8A58F", - health: "A7DCCB", - heartbeat: "B7ACE0", - integration: "8CAFC4", - memory: "7F96B2", - observability: "6D7482", - onboard: "E6E0C8", - provider: "8A7896", - runtime: "8E88AF", - security: "D99084", - service: "B3C7D6", - skillforge: "B9B2DA", - skills: "C8C2E0", - tool: "9BCFBF", - tunnel: "8DAEC0", - tests: "DCE9EE", - scripts: "E7DFC6", - dev: "C4D3DE", + "size: XS": "EAF1F4", + "size: S": "DEE9EF", + "size: M": "D0DDE6", + "size: L": "C1D0DC", + "size: XL": "B2C3D1", + "risk: low": "BFD8B5", + "risk: medium": "E4D39B", + "risk: high": "E1A39A", + "risk: manual": "B9B1D2", + ...otherLabelColors, }; const staticLabelDescriptions = { "size: XS": "Auto size: <=80 non-doc changed lines.", @@ -250,29 +278,12 @@ jobs: } } - const modulePrefixColors = { - "agent:": "9FC4B8", - "channel:": "AFC4D6", - "config:": "C3BCD8", - "cron:": "C7D6A5", - "daemon:": "7C7F95", - "doctor:": "A8D6CD", - "gateway:": "D8A58F", - "health:": "A7DCCB", - "heartbeat:": "B7ACE0", - "integration:": "8CAFC4", - "memory:": "7F96B2", - "observability:": "6D7482", - "onboard:": "E6E0C8", - "provider:": "8A7896", - "runtime:": "8E88AF", - "security:": "D99084", - "service:": "B3C7D6", - "skillforge:": "B9B2DA", - "skills:": "C8C2E0", - "tool:": "9BCFBF", - "tunnel:": "8DAEC0", - }; + const modulePrefixColors = Object.fromEntries( + modulePrefixPriority.map((prefix) => [ + `${prefix}:`, + otherLabelColors[prefix] || "BFDADC", + ]) + ); const providerKeywordHints = [ "deepseek", diff --git a/docs/ci-map.md b/docs/ci-map.md index f455ee1..d3880b5 100644 --- a/docs/ci-map.md +++ b/docs/ci-map.md @@ -34,6 +34,7 @@ Merge-blocking checks should stay small and deterministic. Optional checks are u - Additional behavior: noisy namespaces (`tool`, `provider`, `channel`) are compacted — one specific module keeps `prefix:component`; multiple specifics collapse to just `prefix` - 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: managed label colors follow display order to produce a smooth left-to-right gradient when many labels are present - 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/**` - Guardrail: maintainers can apply `risk: manual` to freeze automated risk recalculation diff --git a/docs/pr-workflow.md b/docs/pr-workflow.md index c894652..b2cb4ea 100644 --- a/docs/pr-workflow.md +++ b/docs/pr-workflow.md @@ -53,6 +53,7 @@ Maintain these branch protection rules on `main`: - For `tool` / `provider` / `channel`, module labels are compacted to reduce noise: one specific module keeps `prefix:component`, but multiple specifics collapse to the base scope label. - 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. ### Step B: Validation