diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index cd7b0b9..fd52635 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -81,17 +81,26 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata (tags, labels) + - name: Compute tags id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - type=raw,value=latest,enable={{is_default_branch}} + shell: bash + run: | + set -euo pipefail + IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" + SHA_TAG="${IMAGE}:sha-${GITHUB_SHA::12}" + + if [[ "${GITHUB_REF}" == refs/tags/* ]]; then + TAG_NAME="${GITHUB_REF#refs/tags/}" + TAGS="${IMAGE}:${TAG_NAME},${SHA_TAG}" + elif [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then + TAGS="${IMAGE}:latest,${SHA_TAG}" + else + BRANCH_NAME="${GITHUB_REF#refs/heads/}" + BRANCH_NAME="${BRANCH_NAME//\//-}" + TAGS="${IMAGE}:${BRANCH_NAME},${SHA_TAG}" + fi + + echo "tags=${TAGS}" >> "$GITHUB_OUTPUT" - name: Build and push Docker image uses: docker/build-push-action@v5 @@ -99,7 +108,6 @@ jobs: context: . push: true tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max platforms: ${{ startsWith(github.ref, 'refs/tags/') && 'linux/amd64,linux/arm64' || 'linux/amd64' }} diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index c1cdfcd..9b0a67f 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,693 +1,700 @@ name: PR Labeler on: - pull_request_target: - types: [opened, reopened, synchronize, edited, labeled, unlabeled] + pull_request_target: + types: [opened, reopened, synchronize, edited, labeled, unlabeled] + +concurrency: + group: pr-labeler-${{ github.event.pull_request.number }} + cancel-in-progress: true permissions: - contents: read - pull-requests: write - issues: write + contents: read + pull-requests: write + issues: write jobs: - label: - runs-on: ubuntu-latest - steps: - - name: Apply path labels - uses: actions/labeler@v5 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - sync-labels: true + label: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Apply path labels + uses: actions/labeler@v5 + continue-on-error: true + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + sync-labels: true - - name: Apply size/risk/module labels - uses: actions/github-script@v8 - with: - script: | - const pr = context.payload.pull_request; - const owner = context.repo.owner; - const repo = context.repo.repo; - const action = context.payload.action; - const changedLabel = context.payload.label?.name; + - name: Apply size/risk/module labels + uses: actions/github-script@v8 + continue-on-error: true + with: + script: | + const pr = context.payload.pull_request; + const owner = context.repo.owner; + const repo = context.repo.repo; + const action = context.payload.action; + const changedLabel = context.payload.label?.name; - const sizeLabels = ["size: XS", "size: S", "size: M", "size: L", "size: XL"]; - const computedRiskLabels = ["risk: low", "risk: medium", "risk: high"]; - const manualRiskOverrideLabel = "risk: manual"; - const managedEnforcedLabels = new Set([ - ...sizeLabels, - manualRiskOverrideLabel, - ...computedRiskLabels, - ]); - const legacyTrustedContributorLabel = "trusted contributor"; + const sizeLabels = ["size: XS", "size: S", "size: M", "size: L", "size: XL"]; + const computedRiskLabels = ["risk: low", "risk: medium", "risk: high"]; + const manualRiskOverrideLabel = "risk: manual"; + const managedEnforcedLabels = new Set([ + ...sizeLabels, + 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; - } + if ((action === "labeled" || action === "unlabeled") && !managedEnforcedLabels.has(changedLabel)) { + core.info(`skip non-size/risk label event: ${changedLabel || "unknown"}`); + return; + } - const contributorTierRules = [ - { label: "distinguished contributor", minMergedPRs: 50 }, - { label: "principal contributor", minMergedPRs: 20 }, - { label: "experienced contributor", minMergedPRs: 10 }, - ]; - const contributorTierLabels = contributorTierRules.map((rule) => rule.label); - const contributorTierColor = "C5D7A2"; + const contributorTierRules = [ + { label: "distinguished contributor", minMergedPRs: 50 }, + { label: "principal contributor", minMergedPRs: 20 }, + { label: "experienced contributor", minMergedPRs: 10 }, + ]; + const contributorTierLabels = contributorTierRules.map((rule) => rule.label); + const contributorTierColor = "C5D7A2"; - const managedPathLabels = [ - "docs", - "dependencies", - "ci", - "core", - "agent", - "channel", - "config", - "cron", - "daemon", - "doctor", - "gateway", - "health", - "heartbeat", - "integration", - "memory", - "observability", - "onboard", - "provider", - "runtime", - "security", - "service", - "skillforge", - "skills", - "tool", - "tunnel", - "tests", - "scripts", - "dev", - ]; - const managedPathLabelSet = new Set(managedPathLabels); + const managedPathLabels = [ + "docs", + "dependencies", + "ci", + "core", + "agent", + "channel", + "config", + "cron", + "daemon", + "doctor", + "gateway", + "health", + "heartbeat", + "integration", + "memory", + "observability", + "onboard", + "provider", + "runtime", + "security", + "service", + "skillforge", + "skills", + "tool", + "tunnel", + "tests", + "scripts", + "dev", + ]; + const managedPathLabelSet = new Set(managedPathLabels); - const moduleNamespaceRules = [ - { root: "src/agent/", prefix: "agent", coreEntries: new Set(["mod.rs"]) }, - { root: "src/channels/", prefix: "channel", coreEntries: new Set(["mod.rs", "traits.rs"]) }, - { root: "src/config/", prefix: "config", coreEntries: new Set(["mod.rs", "schema.rs"]) }, - { root: "src/cron/", prefix: "cron", coreEntries: new Set(["mod.rs"]) }, - { root: "src/daemon/", prefix: "daemon", coreEntries: new Set(["mod.rs"]) }, - { root: "src/doctor/", prefix: "doctor", coreEntries: new Set(["mod.rs"]) }, - { root: "src/gateway/", prefix: "gateway", coreEntries: new Set(["mod.rs"]) }, - { root: "src/health/", prefix: "health", coreEntries: new Set(["mod.rs"]) }, - { root: "src/heartbeat/", prefix: "heartbeat", coreEntries: new Set(["mod.rs"]) }, - { root: "src/integrations/", prefix: "integration", coreEntries: new Set(["mod.rs", "registry.rs"]) }, - { root: "src/memory/", prefix: "memory", coreEntries: new Set(["mod.rs", "traits.rs"]) }, - { root: "src/observability/", prefix: "observability", coreEntries: new Set(["mod.rs", "traits.rs"]) }, - { root: "src/onboard/", prefix: "onboard", coreEntries: new Set(["mod.rs"]) }, - { root: "src/providers/", prefix: "provider", coreEntries: new Set(["mod.rs", "traits.rs"]) }, - { root: "src/runtime/", prefix: "runtime", coreEntries: new Set(["mod.rs", "traits.rs"]) }, - { root: "src/security/", prefix: "security", coreEntries: new Set(["mod.rs"]) }, - { root: "src/service/", prefix: "service", coreEntries: new Set(["mod.rs"]) }, - { root: "src/skillforge/", prefix: "skillforge", coreEntries: new Set(["mod.rs"]) }, - { root: "src/skills/", prefix: "skills", coreEntries: new Set(["mod.rs"]) }, - { root: "src/tools/", prefix: "tool", coreEntries: new Set(["mod.rs", "traits.rs"]) }, - { root: "src/tunnel/", prefix: "tunnel", coreEntries: new Set(["mod.rs"]) }, - ]; - const managedModulePrefixes = [...new Set(moduleNamespaceRules.map((rule) => `${rule.prefix}:`))]; - const orderedOtherLabelStyles = [ - { label: "health", color: "A6D3C0" }, - { label: "tool", color: "A5D3BC" }, - { label: "agent", color: "A4D3B7" }, - { label: "memory", color: "A3D2B1" }, - { label: "channel", color: "A1D2AC" }, - { label: "service", color: "A0D2A7" }, - { label: "integration", color: "9FD2A1" }, - { label: "tunnel", color: "A0D19E" }, - { label: "config", color: "A4D19C" }, - { label: "observability", color: "A8D19B" }, - { label: "docs", color: "ACD09A" }, - { label: "dev", color: "B0D099" }, - { label: "tests", color: "B4D097" }, - { label: "skills", color: "B8D096" }, - { label: "skillforge", color: "BDCF95" }, - { label: "provider", color: "C2CF94" }, - { label: "runtime", color: "C7CF92" }, - { label: "heartbeat", color: "CCCF91" }, - { label: "daemon", color: "CFCB90" }, - { label: "doctor", color: "CEC58E" }, - { label: "onboard", color: "CEBF8D" }, - { label: "cron", color: "CEB98C" }, - { label: "ci", color: "CEB28A" }, - { label: "dependencies", color: "CDAB89" }, - { label: "gateway", color: "CDA488" }, - { label: "security", color: "CD9D87" }, - { label: "core", color: "CD9585" }, - { label: "scripts", color: "CD8E84" }, - ]; - const otherLabelDisplayOrder = orderedOtherLabelStyles.map((entry) => entry.label); - 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 = [ - "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 moduleNamespaceRules = [ + { root: "src/agent/", prefix: "agent", coreEntries: new Set(["mod.rs"]) }, + { root: "src/channels/", prefix: "channel", coreEntries: new Set(["mod.rs", "traits.rs"]) }, + { root: "src/config/", prefix: "config", coreEntries: new Set(["mod.rs", "schema.rs"]) }, + { root: "src/cron/", prefix: "cron", coreEntries: new Set(["mod.rs"]) }, + { root: "src/daemon/", prefix: "daemon", coreEntries: new Set(["mod.rs"]) }, + { root: "src/doctor/", prefix: "doctor", coreEntries: new Set(["mod.rs"]) }, + { root: "src/gateway/", prefix: "gateway", coreEntries: new Set(["mod.rs"]) }, + { root: "src/health/", prefix: "health", coreEntries: new Set(["mod.rs"]) }, + { root: "src/heartbeat/", prefix: "heartbeat", coreEntries: new Set(["mod.rs"]) }, + { root: "src/integrations/", prefix: "integration", coreEntries: new Set(["mod.rs", "registry.rs"]) }, + { root: "src/memory/", prefix: "memory", coreEntries: new Set(["mod.rs", "traits.rs"]) }, + { root: "src/observability/", prefix: "observability", coreEntries: new Set(["mod.rs", "traits.rs"]) }, + { root: "src/onboard/", prefix: "onboard", coreEntries: new Set(["mod.rs"]) }, + { root: "src/providers/", prefix: "provider", coreEntries: new Set(["mod.rs", "traits.rs"]) }, + { root: "src/runtime/", prefix: "runtime", coreEntries: new Set(["mod.rs", "traits.rs"]) }, + { root: "src/security/", prefix: "security", coreEntries: new Set(["mod.rs"]) }, + { root: "src/service/", prefix: "service", coreEntries: new Set(["mod.rs"]) }, + { root: "src/skillforge/", prefix: "skillforge", coreEntries: new Set(["mod.rs"]) }, + { root: "src/skills/", prefix: "skills", coreEntries: new Set(["mod.rs"]) }, + { root: "src/tools/", prefix: "tool", coreEntries: new Set(["mod.rs", "traits.rs"]) }, + { root: "src/tunnel/", prefix: "tunnel", coreEntries: new Set(["mod.rs"]) }, + ]; + const managedModulePrefixes = [...new Set(moduleNamespaceRules.map((rule) => `${rule.prefix}:`))]; + const orderedOtherLabelStyles = [ + { label: "health", color: "A6D3C0" }, + { label: "tool", color: "A5D3BC" }, + { label: "agent", color: "A4D3B7" }, + { label: "memory", color: "A3D2B1" }, + { label: "channel", color: "A1D2AC" }, + { label: "service", color: "A0D2A7" }, + { label: "integration", color: "9FD2A1" }, + { label: "tunnel", color: "A0D19E" }, + { label: "config", color: "A4D19C" }, + { label: "observability", color: "A8D19B" }, + { label: "docs", color: "ACD09A" }, + { label: "dev", color: "B0D099" }, + { label: "tests", color: "B4D097" }, + { label: "skills", color: "B8D096" }, + { label: "skillforge", color: "BDCF95" }, + { label: "provider", color: "C2CF94" }, + { label: "runtime", color: "C7CF92" }, + { label: "heartbeat", color: "CCCF91" }, + { label: "daemon", color: "CFCB90" }, + { label: "doctor", color: "CEC58E" }, + { label: "onboard", color: "CEBF8D" }, + { label: "cron", color: "CEB98C" }, + { label: "ci", color: "CEB28A" }, + { label: "dependencies", color: "CDAB89" }, + { label: "gateway", color: "CDA488" }, + { label: "security", color: "CD9D87" }, + { label: "core", color: "CD9585" }, + { label: "scripts", color: "CD8E84" }, + ]; + const otherLabelDisplayOrder = orderedOtherLabelStyles.map((entry) => entry.label); + 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 = [ + "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 otherLabelColors = Object.fromEntries( - orderedOtherLabelStyles.map((entry) => [entry.label, entry.color]) - ); - const staticLabelColors = { - "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.", - "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) { - staticLabelColors[label] = contributorTierColor; - const rule = contributorTierRules.find((entry) => entry.label === label); - if (rule) { - staticLabelDescriptions[label] = `Contributor with ${rule.minMergedPRs}+ merged PRs.`; - } - } + const otherLabelColors = Object.fromEntries( + orderedOtherLabelStyles.map((entry) => [entry.label, entry.color]) + ); + const staticLabelColors = { + "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.", + "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) { + staticLabelColors[label] = contributorTierColor; + const rule = contributorTierRules.find((entry) => entry.label === label); + if (rule) { + staticLabelDescriptions[label] = `Contributor with ${rule.minMergedPRs}+ merged PRs.`; + } + } - const modulePrefixColors = Object.fromEntries( - modulePrefixPriority.map((prefix) => [ - `${prefix}:`, - otherLabelColors[prefix] || "BFDADC", - ]) - ); + const modulePrefixColors = Object.fromEntries( + modulePrefixPriority.map((prefix) => [ + `${prefix}:`, + otherLabelColors[prefix] || "BFDADC", + ]) + ); - const providerKeywordHints = [ - "deepseek", - "moonshot", - "kimi", - "qwen", - "mistral", - "doubao", - "baichuan", - "yi", - "siliconflow", - "vertex", - "azure", - "perplexity", - "venice", - "vercel", - "cloudflare", - "synthetic", - "opencode", - "zai", - "glm", - "minimax", - "bedrock", - "qianfan", - "groq", - "together", - "fireworks", - "cohere", - "openai", - "openrouter", - "anthropic", - "gemini", - "ollama", - ]; + const providerKeywordHints = [ + "deepseek", + "moonshot", + "kimi", + "qwen", + "mistral", + "doubao", + "baichuan", + "yi", + "siliconflow", + "vertex", + "azure", + "perplexity", + "venice", + "vercel", + "cloudflare", + "synthetic", + "opencode", + "zai", + "glm", + "minimax", + "bedrock", + "qianfan", + "groq", + "together", + "fireworks", + "cohere", + "openai", + "openrouter", + "anthropic", + "gemini", + "ollama", + ]; - const channelKeywordHints = [ - "telegram", - "discord", - "slack", - "whatsapp", - "matrix", - "irc", - "imessage", - "email", - "cli", - ]; + const channelKeywordHints = [ + "telegram", + "discord", + "slack", + "whatsapp", + "matrix", + "irc", + "imessage", + "email", + "cli", + ]; - function isDocsLike(path) { - return ( - path.startsWith("docs/") || - path.endsWith(".md") || - path.endsWith(".mdx") || - path === "LICENSE" || - path === ".markdownlint-cli2.yaml" || - path === ".github/pull_request_template.md" || - path.startsWith(".github/ISSUE_TEMPLATE/") - ); - } + function isDocsLike(path) { + return ( + path.startsWith("docs/") || + path.endsWith(".md") || + path.endsWith(".mdx") || + path === "LICENSE" || + path === ".markdownlint-cli2.yaml" || + path === ".github/pull_request_template.md" || + path.startsWith(".github/ISSUE_TEMPLATE/") + ); + } - function normalizeLabelSegment(segment) { - return (segment || "") - .toLowerCase() - .replace(/\.rs$/g, "") - .replace(/[^a-z0-9_-]+/g, "-") - .replace(/^[-_]+|[-_]+$/g, "") - .slice(0, 40); - } + function normalizeLabelSegment(segment) { + return (segment || "") + .toLowerCase() + .replace(/\.rs$/g, "") + .replace(/[^a-z0-9_-]+/g, "-") + .replace(/^[-_]+|[-_]+$/g, "") + .slice(0, 40); + } - function containsKeyword(text, keyword) { - const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const pattern = new RegExp(`(^|[^a-z0-9_])${escaped}([^a-z0-9_]|$)`, "i"); - return pattern.test(text); - } + function containsKeyword(text, keyword) { + const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const pattern = new RegExp(`(^|[^a-z0-9_])${escaped}([^a-z0-9_]|$)`, "i"); + 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 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 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); + 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; + 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); - } + 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; + const leftIsCore = leftParsed.segment === "core"; + const rightIsCore = rightParsed.segment === "core"; + if (leftIsCore !== rightIsCore) return leftIsCore ? 1 : -1; - return leftParsed.segment.localeCompare(rightParsed.segment); - }); - } + return leftParsed.segment.localeCompare(rightParsed.segment); + }); + } - function refineModuleLabels(rawLabels) { - const refined = new Set(rawLabels); - const segmentsByPrefix = new Map(); + 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 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`); - } - } + for (const [prefix, segments] of segmentsByPrefix) { + const hasSpecificSegment = [...segments].some((segment) => segment !== "core"); + if (hasSpecificSegment) { + refined.delete(`${prefix}:core`); + } + } - return refined; - } + return refined; + } - function compactModuleLabels(labels) { - const groupedSegments = new Map(); - const compactedModuleLabels = new Set(); - const forcePathPrefixes = new Set(); + function compactModuleLabels(labels) { + const groupedSegments = new Map(); + const compactedModuleLabels = new Set(); + const forcePathPrefixes = new Set(); - for (const label of labels) { - const parsed = parseModuleLabel(label); - if (!parsed) { - compactedModuleLabels.add(label); - continue; - } - if (!groupedSegments.has(parsed.prefix)) { - groupedSegments.set(parsed.prefix, new Set()); - } - groupedSegments.get(parsed.prefix).add(parsed.segment); - } + for (const label of labels) { + const parsed = parseModuleLabel(label); + if (!parsed) { + compactedModuleLabels.add(label); + continue; + } + if (!groupedSegments.has(parsed.prefix)) { + groupedSegments.set(parsed.prefix, new Set()); + } + groupedSegments.get(parsed.prefix).add(parsed.segment); + } - for (const [prefix, segments] of groupedSegments) { - const uniqueSegments = [...new Set([...segments].filter(Boolean))]; - if (uniqueSegments.length === 0) continue; + for (const [prefix, segments] of groupedSegments) { + const uniqueSegments = [...new Set([...segments].filter(Boolean))]; + if (uniqueSegments.length === 0) continue; - if (uniqueSegments.length === 1) { - compactedModuleLabels.add(`${prefix}:${uniqueSegments[0]}`); - } else { - forcePathPrefixes.add(prefix); - } - } + if (uniqueSegments.length === 1) { + compactedModuleLabels.add(`${prefix}:${uniqueSegments[0]}`); + } else { + forcePathPrefixes.add(prefix); + } + } - return { - moduleLabels: compactedModuleLabels, - forcePathPrefixes, - }; - } + return { + moduleLabels: compactedModuleLabels, + forcePathPrefixes, + }; + } - function colorForLabel(label) { - if (staticLabelColors[label]) return staticLabelColors[label]; - const matchedPrefix = Object.keys(modulePrefixColors).find((prefix) => label.startsWith(prefix)); - if (matchedPrefix) return modulePrefixColors[matchedPrefix]; - return "BFDADC"; - } + function colorForLabel(label) { + if (staticLabelColors[label]) return staticLabelColors[label]; + const matchedPrefix = Object.keys(modulePrefixColors).find((prefix) => label.startsWith(prefix)); + if (matchedPrefix) return modulePrefixColors[matchedPrefix]; + return "BFDADC"; + } - function descriptionForLabel(label) { - if (staticLabelDescriptions[label]) return staticLabelDescriptions[label]; + 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.`; - } + 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."; - } + return "Auto-managed label."; + } - async function ensureLabel(name) { - 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(); - if (currentColor !== expectedColor || currentDescription !== expectedDescription) { - await github.rest.issues.updateLabel({ - owner, - repo, - name, - new_name: name, - color: expectedColor, - description: expectedDescription, - }); - } - } catch (error) { - if (error.status !== 404) throw error; - await github.rest.issues.createLabel({ - owner, - repo, - name, - color: expectedColor, - description: expectedDescription, - }); - } - } + async function ensureLabel(name) { + 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(); + if (currentColor !== expectedColor || currentDescription !== expectedDescription) { + await github.rest.issues.updateLabel({ + owner, + repo, + name, + new_name: name, + color: expectedColor, + description: expectedDescription, + }); + } + } catch (error) { + if (error.status !== 404) throw error; + await github.rest.issues.createLabel({ + owner, + repo, + name, + color: expectedColor, + description: expectedDescription, + }); + } + } - function selectContributorTier(mergedCount) { - const matchedTier = contributorTierRules.find((rule) => mergedCount >= rule.minMergedPRs); - return matchedTier ? matchedTier.label : null; - } + function selectContributorTier(mergedCount) { + const matchedTier = contributorTierRules.find((rule) => mergedCount >= rule.minMergedPRs); + return matchedTier ? matchedTier.label : null; + } - const files = await github.paginate(github.rest.pulls.listFiles, { - owner, - repo, - pull_number: pr.number, - per_page: 100, - }); + const files = await github.paginate(github.rest.pulls.listFiles, { + owner, + repo, + pull_number: pr.number, + per_page: 100, + }); - const detectedModuleLabels = new Set(); - for (const file of files) { - const path = (file.filename || "").toLowerCase(); - for (const rule of moduleNamespaceRules) { - if (!path.startsWith(rule.root)) continue; + const detectedModuleLabels = new Set(); + for (const file of files) { + const path = (file.filename || "").toLowerCase(); + for (const rule of moduleNamespaceRules) { + if (!path.startsWith(rule.root)) continue; - const relative = path.slice(rule.root.length); - if (!relative) continue; + const relative = path.slice(rule.root.length); + if (!relative) continue; - const first = relative.split("/")[0]; - const firstStem = first.endsWith(".rs") ? first.slice(0, -3) : first; - let segment = firstStem; + const first = relative.split("/")[0]; + const firstStem = first.endsWith(".rs") ? first.slice(0, -3) : first; + let segment = firstStem; - if (rule.coreEntries.has(first) || rule.coreEntries.has(firstStem)) { - segment = "core"; - } + if (rule.coreEntries.has(first) || rule.coreEntries.has(firstStem)) { + segment = "core"; + } - segment = normalizeLabelSegment(segment); - if (!segment) continue; + segment = normalizeLabelSegment(segment); + if (!segment) continue; - detectedModuleLabels.add(`${rule.prefix}:${segment}`); - } - } + detectedModuleLabels.add(`${rule.prefix}:${segment}`); + } + } - const providerRelevantFiles = files.filter((file) => { - const path = file.filename || ""; - return ( - path.startsWith("src/providers/") || - path.startsWith("src/integrations/") || - path.startsWith("src/onboard/") || - path.startsWith("src/config/") - ); - }); + const providerRelevantFiles = files.filter((file) => { + const path = file.filename || ""; + return ( + path.startsWith("src/providers/") || + path.startsWith("src/integrations/") || + path.startsWith("src/onboard/") || + path.startsWith("src/config/") + ); + }); - if (providerRelevantFiles.length > 0) { - const searchableText = [ - pr.title || "", - pr.body || "", - ...providerRelevantFiles.map((file) => file.filename || ""), - ...providerRelevantFiles.map((file) => file.patch || ""), - ] - .join("\n") - .toLowerCase(); + if (providerRelevantFiles.length > 0) { + const searchableText = [ + pr.title || "", + pr.body || "", + ...providerRelevantFiles.map((file) => file.filename || ""), + ...providerRelevantFiles.map((file) => file.patch || ""), + ] + .join("\n") + .toLowerCase(); - for (const keyword of providerKeywordHints) { - if (containsKeyword(searchableText, keyword)) { - detectedModuleLabels.add(`provider:${keyword}`); - } - } - } + for (const keyword of providerKeywordHints) { + if (containsKeyword(searchableText, keyword)) { + detectedModuleLabels.add(`provider:${keyword}`); + } + } + } - const channelRelevantFiles = files.filter((file) => { - const path = file.filename || ""; - return ( - path.startsWith("src/channels/") || - path.startsWith("src/onboard/") || - path.startsWith("src/config/") - ); - }); + const channelRelevantFiles = files.filter((file) => { + const path = file.filename || ""; + return ( + path.startsWith("src/channels/") || + path.startsWith("src/onboard/") || + path.startsWith("src/config/") + ); + }); - if (channelRelevantFiles.length > 0) { - const searchableText = [ - pr.title || "", - pr.body || "", - ...channelRelevantFiles.map((file) => file.filename || ""), - ...channelRelevantFiles.map((file) => file.patch || ""), - ] - .join("\n") - .toLowerCase(); + if (channelRelevantFiles.length > 0) { + const searchableText = [ + pr.title || "", + pr.body || "", + ...channelRelevantFiles.map((file) => file.filename || ""), + ...channelRelevantFiles.map((file) => file.patch || ""), + ] + .join("\n") + .toLowerCase(); - for (const keyword of channelKeywordHints) { - if (containsKeyword(searchableText, keyword)) { - detectedModuleLabels.add(`channel:${keyword}`); - } - } - } + for (const keyword of channelKeywordHints) { + if (containsKeyword(searchableText, keyword)) { + detectedModuleLabels.add(`channel:${keyword}`); + } + } + } - const refinedModuleLabels = refineModuleLabels(detectedModuleLabels); - const compactedModuleState = compactModuleLabels(refinedModuleLabels); - const selectedModuleLabels = compactedModuleState.moduleLabels; - const forcePathPrefixes = compactedModuleState.forcePathPrefixes; - const modulePrefixesWithLabels = new Set( - [...selectedModuleLabels] - .map((label) => parseModuleLabel(label)?.prefix) - .filter(Boolean) - ); + const refinedModuleLabels = refineModuleLabels(detectedModuleLabels); + const compactedModuleState = compactModuleLabels(refinedModuleLabels); + const selectedModuleLabels = compactedModuleState.moduleLabels; + const forcePathPrefixes = compactedModuleState.forcePathPrefixes; + const modulePrefixesWithLabels = new Set( + [...selectedModuleLabels] + .map((label) => parseModuleLabel(label)?.prefix) + .filter(Boolean) + ); - const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ - owner, - repo, - issue_number: pr.number, - }); - const currentLabelNames = currentLabels.map((label) => label.name); - const currentPathLabels = currentLabelNames.filter((label) => managedPathLabelSet.has(label)); - const candidatePathLabels = new Set([...currentPathLabels, ...forcePathPrefixes]); + const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ + owner, + repo, + issue_number: pr.number, + }); + const currentLabelNames = currentLabels.map((label) => label.name); + const currentPathLabels = currentLabelNames.filter((label) => managedPathLabelSet.has(label)); + const candidatePathLabels = new Set([...currentPathLabels, ...forcePathPrefixes]); - const dedupedPathLabels = [...candidatePathLabels].filter((label) => { - if (label === "core") return true; - if (forcePathPrefixes.has(label)) return true; - return !modulePrefixesWithLabels.has(label); - }); + const dedupedPathLabels = [...candidatePathLabels].filter((label) => { + if (label === "core") return true; + if (forcePathPrefixes.has(label)) return true; + return !modulePrefixesWithLabels.has(label); + }); - const excludedLockfiles = new Set(["Cargo.lock"]); - const changedLines = files.reduce((total, file) => { - const path = file.filename || ""; - if (isDocsLike(path) || excludedLockfiles.has(path)) { - return total; - } - return total + (file.additions || 0) + (file.deletions || 0); - }, 0); + const excludedLockfiles = new Set(["Cargo.lock"]); + const changedLines = files.reduce((total, file) => { + const path = file.filename || ""; + if (isDocsLike(path) || excludedLockfiles.has(path)) { + return total; + } + return total + (file.additions || 0) + (file.deletions || 0); + }, 0); - let sizeLabel = "size: XL"; - if (changedLines <= 80) sizeLabel = "size: XS"; - else if (changedLines <= 250) sizeLabel = "size: S"; - else if (changedLines <= 500) sizeLabel = "size: M"; - else if (changedLines <= 1000) sizeLabel = "size: L"; + let sizeLabel = "size: XL"; + if (changedLines <= 80) sizeLabel = "size: XS"; + else if (changedLines <= 250) sizeLabel = "size: S"; + else if (changedLines <= 500) sizeLabel = "size: M"; + else if (changedLines <= 1000) sizeLabel = "size: L"; - const hasHighRiskPath = files.some((file) => { - const path = file.filename || ""; - return ( - path.startsWith("src/security/") || - path.startsWith("src/runtime/") || - path.startsWith("src/gateway/") || - path.startsWith("src/tools/") || - path.startsWith(".github/workflows/") - ); - }); + const hasHighRiskPath = files.some((file) => { + const path = file.filename || ""; + return ( + path.startsWith("src/security/") || + path.startsWith("src/runtime/") || + path.startsWith("src/gateway/") || + path.startsWith("src/tools/") || + path.startsWith(".github/workflows/") + ); + }); - const hasMediumRiskPath = files.some((file) => { - const path = file.filename || ""; - return ( - path.startsWith("src/") || - path === "Cargo.toml" || - path === "Cargo.lock" || - path === "deny.toml" || - path.startsWith(".githooks/") - ); - }); + const hasMediumRiskPath = files.some((file) => { + const path = file.filename || ""; + return ( + path.startsWith("src/") || + path === "Cargo.toml" || + path === "Cargo.lock" || + path === "deny.toml" || + path.startsWith(".githooks/") + ); + }); - let riskLabel = "risk: low"; - if (hasHighRiskPath) { - riskLabel = "risk: high"; - } else if (hasMediumRiskPath) { - riskLabel = "risk: medium"; - } + let riskLabel = "risk: low"; + if (hasHighRiskPath) { + riskLabel = "risk: high"; + } else if (hasMediumRiskPath) { + riskLabel = "risk: medium"; + } - const labelsToEnsure = new Set([ - ...sizeLabels, - ...computedRiskLabels, - manualRiskOverrideLabel, - ...managedPathLabels, - ...contributorTierLabels, - ...selectedModuleLabels, - ]); + const labelsToEnsure = new Set([ + ...sizeLabels, + ...computedRiskLabels, + manualRiskOverrideLabel, + ...managedPathLabels, + ...contributorTierLabels, + ...selectedModuleLabels, + ]); - for (const label of labelsToEnsure) { - await ensureLabel(label); - } + for (const label of labelsToEnsure) { + await ensureLabel(label); + } - let contributorTierLabel = null; - const authorLogin = pr.user?.login; - if (authorLogin && pr.user?.type !== "Bot") { - try { - const { data: mergedSearch } = await github.rest.search.issuesAndPullRequests({ - q: `repo:${owner}/${repo} is:pr is:merged author:${authorLogin}`, - per_page: 1, - }); - const mergedCount = mergedSearch.total_count || 0; - contributorTierLabel = selectContributorTier(mergedCount); - } catch (error) { - core.warning(`failed to compute contributor tier label: ${error.message}`); - } - } + let contributorTierLabel = null; + const authorLogin = pr.user?.login; + if (authorLogin && pr.user?.type !== "Bot") { + try { + const { data: mergedSearch } = await github.rest.search.issuesAndPullRequests({ + q: `repo:${owner}/${repo} is:pr is:merged author:${authorLogin}`, + per_page: 1, + }); + const mergedCount = mergedSearch.total_count || 0; + contributorTierLabel = selectContributorTier(mergedCount); + } catch (error) { + core.warning(`failed to compute contributor tier label: ${error.message}`); + } + } - 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; - if (managedModulePrefixes.some((prefix) => label.startsWith(prefix))) return false; - return true; - }); + 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; + if (managedModulePrefixes.some((prefix) => label.startsWith(prefix))) return false; + return true; + }); - const manualRiskSelection = - currentLabelNames.find((label) => computedRiskLabels.includes(label)) || riskLabel; + const manualRiskSelection = + currentLabelNames.find((label) => computedRiskLabels.includes(label)) || riskLabel; - const moduleLabelList = sortModuleLabels([...selectedModuleLabels]); - const contributorLabelList = contributorTierLabel ? [contributorTierLabel] : []; - const selectedRiskLabels = hasManualRiskOverride - ? sortByPriority([manualRiskSelection, manualRiskOverrideLabel], riskPriorityIndex) - : 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 moduleLabelList = sortModuleLabels([...selectedModuleLabels]); + const contributorLabelList = contributorTierLabel ? [contributorTierLabel] : []; + const selectedRiskLabels = hasManualRiskOverride + ? sortByPriority([manualRiskSelection, manualRiskOverrideLabel], riskPriorityIndex) + : 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, - ]), - ]; + const nextLabels = [ + ...new Set([ + ...selectedRiskLabels, + ...selectedSizeLabels, + ...sortedContributorLabels, + ...moduleLabelList, + ...sortedPathLabels, + ...sortedKeepNonManagedLabels, + ]), + ]; - await github.rest.issues.setLabels({ - owner, - repo, - issue_number: pr.number, - labels: nextLabels, - }); + await github.rest.issues.setLabels({ + owner, + repo, + issue_number: pr.number, + labels: nextLabels, + }); diff --git a/src/channels/mod.rs b/src/channels/mod.rs index f0399da..2041e86 100644 --- a/src/channels/mod.rs +++ b/src/channels/mod.rs @@ -50,6 +50,7 @@ const CHANNEL_MAX_IN_FLIGHT_MESSAGES: usize = 64; struct ChannelRuntimeContext { channels_by_name: Arc>>, provider: Arc, + provider_name: Arc, memory: Arc, tools_registry: Arc>>, observer: Arc, @@ -185,6 +186,7 @@ async fn process_channel_message(ctx: Arc, msg: traits::C &mut history, ctx.tools_registry.as_ref(), ctx.observer.as_ref(), + ctx.provider_name.as_str(), ctx.model.as_str(), ctx.temperature, ), @@ -677,8 +679,12 @@ pub async fn doctor_channels(config: Config) -> Result<()> { /// Start all configured channels and route messages to the agent #[allow(clippy::too_many_lines)] pub async fn start_channels(config: Config) -> Result<()> { + let provider_name = config + .default_provider + .clone() + .unwrap_or_else(|| "openrouter".to_string()); let provider: Arc = Arc::from(providers::create_resilient_provider( - config.default_provider.as_deref().unwrap_or("openrouter"), + provider_name.as_str(), config.api_key.as_deref(), &config.reliability, )?); @@ -721,6 +727,7 @@ pub async fn start_channels(config: Config) -> Result<()> { composio_key, &config.browser, &config.http_request, + &config.workspace_dir, &config.agents, config.api_key.as_deref(), )); @@ -927,6 +934,7 @@ pub async fn start_channels(config: Config) -> Result<()> { let runtime_ctx = Arc::new(ChannelRuntimeContext { channels_by_name, provider: Arc::clone(&provider), + provider_name: Arc::new(provider_name), memory: Arc::clone(&mem), tools_registry: Arc::clone(&tools_registry), observer, @@ -1121,6 +1129,7 @@ mod tests { let runtime_ctx = Arc::new(ChannelRuntimeContext { channels_by_name: Arc::new(channels_by_name), provider: Arc::new(ToolCallingProvider), + provider_name: Arc::new("test-provider".to_string()), memory: Arc::new(NoopMemory), tools_registry: Arc::new(vec![Box::new(MockPriceTool)]), observer: Arc::new(NoopObserver), @@ -1211,6 +1220,7 @@ mod tests { provider: Arc::new(SlowProvider { delay: Duration::from_millis(250), }), + provider_name: Arc::new("test-provider".to_string()), memory: Arc::new(NoopMemory), tools_registry: Arc::new(vec![]), observer: Arc::new(NoopObserver), diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index af3b861..f1bc4a1 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -193,7 +193,8 @@ async fn run_heartbeat_worker(config: Config) -> Result<()> { for task in tasks { let prompt = format!("[Heartbeat Task] {task}"); let temp = config.default_temperature; - if let Err(e) = crate::agent::run(config.clone(), Some(prompt), None, None, temp).await + if let Err(e) = + crate::agent::run(config.clone(), Some(prompt), None, None, temp, false).await { crate::health::mark_component_error("heartbeat", e.to_string()); tracing::warn!("Heartbeat task failed: {e}");