From 05ecb40181cf25fb0307d91d6698c56b0a886b73 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Sat, 1 Aug 2026 05:23:19 +0200 Subject: [PATCH 1/5] feat(issues): add orthogonal area labels for triage Map template Area fields and light heuristics to additive labels (provider, account-pool, catalog, gui, cli, proxy, platform, streaming, tools, install, service) beside existing kind/process labels, with open issue backfill via workflow_dispatch. --- .github/ISSUE_TEMPLATE/bug_report.yml | 4 + .github/ISSUE_TEMPLATE/feature_request.yml | 4 + .../ISSUE_TEMPLATE/provider_compatibility.yml | 1 + .github/scripts/issue-quality.cjs | 188 +++++++++++++++++ .github/scripts/issue-quality.test.cjs | 108 ++++++++++ .github/workflows/enforce-issue-quality.yml | 190 +++++++++++++++++- docs-site/src/content/docs/contributing.md | 8 + 7 files changed, 498 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 09734d7b5..4a2503137 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,8 +35,12 @@ body: - Dashboard - Provider adapter - Authentication and account pool + - Catalog / models + - Streaming + - Tools / MCP / web search - Installation or packaging - Service lifecycle + - Platform (Windows / macOS / Linux) - Documentation - Other validations: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index c5df37f43..5ac7b510c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -19,8 +19,12 @@ body: - Dashboard - Provider adapters - Authentication and account pool + - Catalog / models + - Streaming + - Tools / MCP / web search - Installation or packaging - Service lifecycle + - Platform (Windows / macOS / Linux) - Documentation - Multiple areas - Other diff --git a/.github/ISSUE_TEMPLATE/provider_compatibility.yml b/.github/ISSUE_TEMPLATE/provider_compatibility.yml index 6525cbed7..ab02cf379 100644 --- a/.github/ISSUE_TEMPLATE/provider_compatibility.yml +++ b/.github/ISSUE_TEMPLATE/provider_compatibility.yml @@ -2,6 +2,7 @@ name: Provider or API compatibility description: Report an incompatible provider, endpoint, request shape, response shape, or client integration. labels: - provider-compatibility + - provider body: - type: markdown attributes: diff --git a/.github/scripts/issue-quality.cjs b/.github/scripts/issue-quality.cjs index 9dccb742f..0672a6b11 100644 --- a/.github/scripts/issue-quality.cjs +++ b/.github/scripts/issue-quality.cjs @@ -250,6 +250,125 @@ const KIND_TO_LABEL = { "provider-compatibility": "provider-compatibility", }; +/** + * Orthogonal product-area labels (additive beside kind/process labels). + * Colors/descriptions are used when the workflow ensures labels exist. + */ +const AREA_LABELS = { + provider: { + color: "1D76DB", + description: "Provider adapters, OpenAI-compat presets, upstream API quirks", + }, + "account-pool": { + color: "5319E7", + description: "OAuth, credentials, Codex pool, quota, failover, plans", + }, + catalog: { + color: "006B75", + description: "Model catalog, slugs, visibility, routed entries", + }, + gui: { + color: "D93F0B", + description: "Dashboard, tray, settings UI", + }, + cli: { + color: "FBCA04", + description: "CLI, config inject, packaging flags", + }, + proxy: { + color: "0E8A16", + description: "HTTP proxy, routing, reverse-proxy / management auth", + }, + platform: { + color: "BFDADC", + description: "OS/service/tray/ACL (Windows-heavy, not Windows-only)", + }, + streaming: { + color: "C5DEF5", + description: "SSE, WebSocket, terminal stream frames", + }, + tools: { + color: "F9D0C4", + description: "tool_calls, MCP, web-search / sidecar tools", + }, + install: { + color: "EDEDED", + description: "Installation or packaging", + }, + service: { + color: "EDEDED", + description: "Service lifecycle (WinSW/launchd/scheduler)", + }, +}; + +/** Canonical Area dropdown text → area label(s). Keys are lowercased. */ +const AREA_FIELD_TO_LABELS = { + cli: ["cli"], + "proxy and routing": ["proxy"], + dashboard: ["gui"], + "provider adapter": ["provider"], + "provider adapters": ["provider"], + "authentication and account pool": ["account-pool"], + "catalog / models": ["catalog"], + streaming: ["streaming"], + "tools / mcp / web search": ["tools"], + "installation or packaging": ["install"], + "service lifecycle": ["service"], + "service lifecycle (config injection)": ["service"], + "platform (windows / macos / linux)": ["platform"], + documentation: ["documentation"], + // No dedicated label; heuristics still run in detectAreaLabels. + "multiple areas": [], + other: [], +}; + +const AREA_HEURISTICS = [ + { + label: "account-pool", + re: /\b(oauth|reauth|needsreauth|quota|account pool|codex.?auth|auto[- ]?switch|failover|refresh token|plan_type|chatgpt[- ]account|reset credit)\b/i, + }, + { + label: "catalog", + re: /\b(catalog|model list|model visibility|virtual model|routed (catalog|entries|slug)|model slug)\b/i, + }, + { + label: "gui", + re: /\b(dashboard|\bgui\b|tray|sidebar|settings tab)\b/i, + }, + { + label: "cli", + re: /\b(ocx\b|config\.toml|config inject)\b/i, + }, + { + label: "proxy", + re: /\b(reverse[- ]proxy|management api|admin[- ]token|\/api\/\*|bind(s)? the (old )?port|proxy running)\b/i, + }, + { + label: "platform", + re: /\b(windows|macos|mac os|win32|darwin|wsl|winsw|launchd|systemd|icacls|\bacl\b)\b/i, + }, + { + label: "streaming", + re: /\b(sse|websocket|\bws\b|stream(ing)?\b.{0,40}\b(truncat|terminal)|terminal (sse )?frame|without a terminal)\b/i, + }, + { + label: "tools", + re: /\b(tool_calls?|tool[- ]calls?|\bmcp\b|web[- ]search|tool[- ]recall)\b/i, + }, + { + label: "install", + re: /\b(npm (global )?install|packaging|release asset|npx ocx)\b/i, + }, + { + label: "service", + re: /\b(ocx service|winsw|scheduler backend|launchd service)\b/i, + }, + { + label: "provider", + re: /\b(provider adapter|openai[- ]compatible|provider[- ]compat|adapter quirk|upstream api|built[- ]in provider|provider preset)\b/i, + }, +]; + /** * Map a detected issue kind to its triage label. Returns null when unknown. */ @@ -258,6 +377,70 @@ function labelForKind(kind) { return KIND_TO_LABEL[kind] || null; } +/** + * Map a template Area dropdown value to orthogonal area label names. + * Returns [] for Other / Multiple areas / unknown / empty. + * + * @param {unknown} areaText + * @returns {string[]} + */ +function mapAreaFieldToLabels(areaText) { + if (typeof areaText !== "string") return []; + const key = areaText.replace(/\s+/g, " ").trim().toLowerCase(); + if (!key) return []; + return AREA_FIELD_TO_LABELS[key] ? [...AREA_FIELD_TO_LABELS[key]] : []; +} + +/** + * Conservative title/body heuristics for orthogonal area labels. + * + * @param {string} title + * @param {string} body + * @returns {string[]} + */ +function heuristicAreaLabels(title, body) { + const text = `${title || ""}\n${body || ""}`; + const out = []; + for (const { label, re } of AREA_HEURISTICS) { + if (re.test(text)) out.push(label); + } + return out; +} + +/** + * Detect additive product-area labels from Area field, form defaults, and + * title/body heuristics. Never invents per-provider labels. + * + * @param {{ title?: string, body?: string, labels?: string[] }} issue + * @returns {string[]} + */ +function detectAreaLabels(issue) { + const title = typeof issue?.title === "string" ? issue.title : ""; + const body = typeof issue?.body === "string" ? issue.body : ""; + const labels = Array.isArray(issue?.labels) ? issue.labels : []; + + const areaSection = extractSection(body, "Area"); + const fromArea = mapAreaFieldToLabels(areaSection); + const fromHeur = heuristicAreaLabels(title, body); + const fromForm = []; + if (labels.includes("provider-compatibility")) fromForm.push("provider"); + // Provider-compat form uses this heading instead of Area. + if (extractSection(body, "Provider or upstream service") !== null) { + fromForm.push("provider"); + } + + const seen = new Set(); + const out = []; + for (const label of [...fromArea, ...fromForm, ...fromHeur]) { + if (!label || seen.has(label)) continue; + // `documentation` is a kind label and also an Area mapping target. + if (label !== "documentation" && !AREA_LABELS[label]) continue; + seen.add(label); + out.push(label); + } + return out; +} + function countHeadings(body, headings) { let n = 0; for (const h of headings) { @@ -952,6 +1135,11 @@ module.exports = { hasConcreteDetail, labelForKind, KIND_TO_LABEL, + AREA_LABELS, + AREA_FIELD_TO_LABELS, + mapAreaFieldToLabels, + heuristicAreaLabels, + detectAreaLabels, hasSubstantialStructuredContent, rejectsWorkflowDispatchPullRequest, rejectsWorkflowDispatchNonDefaultBranch, diff --git a/.github/scripts/issue-quality.test.cjs b/.github/scripts/issue-quality.test.cjs index c629a3d78..782beb7f0 100644 --- a/.github/scripts/issue-quality.test.cjs +++ b/.github/scripts/issue-quality.test.cjs @@ -13,6 +13,9 @@ const { shouldReopen, shouldEnforceClosure, labelForKind, + AREA_LABELS, + mapAreaFieldToLabels, + detectAreaLabels, isPlaceholderOnlyValue, isPlaceholder, isRawPlaceholder, @@ -1650,3 +1653,108 @@ describe("rejectsWorkflowDispatchNonDefaultBranch", () => { ); }); }); + +// --------------------------------------------------------------------------- +// Orthogonal area labels +// --------------------------------------------------------------------------- + +describe("mapAreaFieldToLabels", () => { + it("maps canonical Area dropdown values", () => { + assert.deepEqual(mapAreaFieldToLabels("CLI"), ["cli"]); + assert.deepEqual(mapAreaFieldToLabels("Proxy and routing"), ["proxy"]); + assert.deepEqual(mapAreaFieldToLabels("Dashboard"), ["gui"]); + assert.deepEqual(mapAreaFieldToLabels("Provider adapter"), ["provider"]); + assert.deepEqual(mapAreaFieldToLabels("Provider adapters"), ["provider"]); + assert.deepEqual(mapAreaFieldToLabels("Authentication and account pool"), ["account-pool"]); + assert.deepEqual(mapAreaFieldToLabels("Catalog / models"), ["catalog"]); + assert.deepEqual(mapAreaFieldToLabels("Streaming"), ["streaming"]); + assert.deepEqual(mapAreaFieldToLabels("Tools / MCP / web search"), ["tools"]); + assert.deepEqual(mapAreaFieldToLabels("Installation or packaging"), ["install"]); + assert.deepEqual(mapAreaFieldToLabels("Service lifecycle"), ["service"]); + assert.deepEqual(mapAreaFieldToLabels("Platform (Windows / macOS / Linux)"), ["platform"]); + assert.deepEqual(mapAreaFieldToLabels("Documentation"), ["documentation"]); + }); + + it("maps legacy Service lifecycle wording and ignores Other / Multiple areas", () => { + assert.deepEqual(mapAreaFieldToLabels("Service lifecycle (config injection)"), ["service"]); + assert.deepEqual(mapAreaFieldToLabels("Other"), []); + assert.deepEqual(mapAreaFieldToLabels("Multiple areas"), []); + assert.deepEqual(mapAreaFieldToLabels(""), []); + assert.deepEqual(mapAreaFieldToLabels(null), []); + }); + + it("exposes metadata for every non-documentation area label", () => { + for (const name of Object.keys(AREA_LABELS)) { + assert.ok(AREA_LABELS[name].color, name); + assert.ok(AREA_LABELS[name].description, name); + } + }); +}); + +describe("detectAreaLabels", () => { + it("applies Area mapping plus orthogonal heuristics", () => { + const labels = detectAreaLabels({ + title: "Pool failover stalls on SSE without terminal frame", + body: [ + "### Area", + "Authentication and account pool", + "### Summary", + "Account pool failover waits forever when the upstream SSE stream ends without a terminal frame.", + ].join("\n"), + labels: ["bug"], + }); + assert.ok(labels.includes("account-pool")); + assert.ok(labels.includes("streaming")); + }); + + it("adds provider for provider-compatibility form and label", () => { + const fromLabel = detectAreaLabels({ + title: "AgentRouter Anthropic streams can end without terminal SSE frames", + body: "### Summary\nStream ends early.", + labels: ["provider-compatibility"], + }); + assert.ok(fromLabel.includes("provider")); + assert.ok(fromLabel.includes("streaming")); + + const fromHeading = detectAreaLabels({ + title: "Custom relay rejects tool_calls", + body: [ + "### Provider or upstream service", + "Volcengine Ark", + "### Current behaviour", + "tool_calls with empty content return 400.", + ].join("\n"), + labels: [], + }); + assert.ok(fromHeading.includes("provider")); + assert.ok(fromHeading.includes("tools")); + }); + + it("runs heuristics for Multiple areas / Other without inventing per-provider labels", () => { + const labels = detectAreaLabels({ + title: "Dashboard ACL hardening blocks management API on Windows", + body: [ + "### Area", + "Multiple areas", + "### Summary", + "OpenCodex dashboard shows no data when ACL/icacls hardening cannot be verified.", + ].join("\n"), + labels: ["bug"], + }); + assert.ok(labels.includes("gui")); + assert.ok(labels.includes("platform")); + assert.equal(labels.includes("kiro"), false); + assert.equal(labels.includes("gemini"), false); + assert.equal(labels.includes("windows"), false); + }); + + it("maps Documentation Area to documentation and does not invent docs", () => { + const labels = detectAreaLabels({ + title: "Codex Auth UI/docs conflate usage-based switching", + body: ["### Area", "Documentation", "### Summary", "Docs misdefine new session."].join("\n"), + labels: [], + }); + assert.ok(labels.includes("documentation")); + assert.equal(labels.includes("docs"), false); + }); +}); diff --git a/.github/workflows/enforce-issue-quality.yml b/.github/workflows/enforce-issue-quality.yml index 2ef8ddc59..edf67f636 100644 --- a/.github/workflows/enforce-issue-quality.yml +++ b/.github/workflows/enforce-issue-quality.yml @@ -17,18 +17,27 @@ on: workflow_dispatch: inputs: issue_number: - description: Issue number to validate and enforce - required: true + description: Issue number to validate and enforce (omit when backfilling open area labels) + required: false type: number + backfill_open_areas: + description: Apply orthogonal area labels to all open issues (area pass only; no quality closure) + required: false + type: boolean + default: false concurrency: - group: issue-quality-${{ github.event.issue.number || inputs.issue_number }} + group: issue-quality-${{ github.event.issue.number || inputs.issue_number || (inputs.backfill_open_areas && 'backfill-open-areas') || 'manual' }} cancel-in-progress: false jobs: translate: name: Translate non-English issues - if: github.event_name == 'issues' || github.event_name == 'workflow_dispatch' + if: > + github.event_name == 'issues' || + (github.event_name == 'workflow_dispatch' && + inputs.backfill_open_areas != true && + inputs.issue_number != '') runs-on: ubuntu-latest # Serialize per-issue control-state RMW; workflow concurrency still cancels # superseded runs, but this queue avoids interleaved comment upserts. @@ -686,7 +695,11 @@ jobs: } validate: - if: github.event_name == 'issues' || github.event_name == 'workflow_dispatch' + if: > + github.event_name == 'issues' || + (github.event_name == 'workflow_dispatch' && + inputs.backfill_open_areas != true && + inputs.issue_number != '') runs-on: ubuntu-latest permissions: contents: read @@ -716,6 +729,8 @@ jobs: shouldReopen, shouldEnforceClosure, labelForKind, + detectAreaLabels, + AREA_LABELS, rejectsWorkflowDispatchPullRequest, rejectsWorkflowDispatchNonDefaultBranch, } = require(path.join(process.cwd(), ".github", "scripts", "issue-quality.cjs")); @@ -880,6 +895,60 @@ jobs: } } + // ----------------------------------------------------------------- + // Orthogonal area labels (additive only) + // ----------------------------------------------------------------- + async function ensureLabel(name) { + try { + await github.rest.issues.getLabel({ owner, repo, name }); + return; + } catch (err) { + if (err.status !== 404) { + core.warning(`Failed to look up label "${name}": ${err.message || err}`); + return; + } + } + const meta = AREA_LABELS[name]; + const color = meta?.color || (name === "documentation" ? "0075ca" : "EDEDED"); + const description = meta?.description + || (name === "documentation" + ? "Improvements or additions to documentation" + : name); + try { + await github.rest.issues.createLabel({ + owner, repo, name, color, description, + }); + core.info(`Created label "${name}".`); + } catch (err) { + if (err.status === 422) { + core.info(`Label "${name}" appeared concurrently; continuing.`); + return; + } + core.warning(`Failed to create label "${name}": ${err.message || err}`); + } + } + + const areaLabels = detectAreaLabels({ + title: issue.title, + body: issueBody, + labels, + }); + const areaLabelsToAdd = areaLabels.filter((name) => !labels.includes(name)); + for (const name of areaLabelsToAdd) { + await ensureLabel(name); + } + if (areaLabelsToAdd.length > 0) { + try { + await github.rest.issues.addLabels({ + owner, repo, issue_number, labels: areaLabelsToAdd, + }); + labels.push(...areaLabelsToAdd); + core.info(`Applied area label(s): ${areaLabelsToAdd.join(", ")}`); + } catch (err) { + core.warning(`Failed to apply area labels: ${err.message || err}`); + } + } + // ----------------------------------------------------------------- // Validate // ----------------------------------------------------------------- @@ -1044,3 +1113,114 @@ jobs: "", "Once the report passes the automated checks, it will be reopened automatically unless a maintainer has changed its state.", ].join("\n")); + + backfill-open-areas: + name: Backfill open issue area labels + if: github.event_name == 'workflow_dispatch' && inputs.backfill_open_areas == true + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + steps: + - name: Checkout trusted workflow code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.repository.default_branch }} + persist-credentials: false + sparse-checkout: .github/scripts + + - name: Apply area labels to open issues + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const path = require("path"); + const { + detectAreaLabels, + AREA_LABELS, + rejectsWorkflowDispatchNonDefaultBranch, + } = require(path.join(process.cwd(), ".github", "scripts", "issue-quality.cjs")); + const { stripTranslationBlock } = require( + path.join(process.cwd(), ".github", "scripts", "issue-translation.cjs"), + ); + + const nonDefaultBranchFailure = rejectsWorkflowDispatchNonDefaultBranch( + context.eventName, + context.ref, + context.payload.repository?.default_branch, + ); + if (nonDefaultBranchFailure) { + core.setFailed(nonDefaultBranchFailure); + return; + } + + const { owner, repo } = context.repo; + + async function ensureLabel(name) { + try { + await github.rest.issues.getLabel({ owner, repo, name }); + return; + } catch (err) { + if (err.status !== 404) { + core.warning(`Failed to look up label "${name}": ${err.message || err}`); + return; + } + } + const meta = AREA_LABELS[name]; + const color = meta?.color || (name === "documentation" ? "0075ca" : "EDEDED"); + const description = meta?.description + || (name === "documentation" + ? "Improvements or additions to documentation" + : name); + try { + await github.rest.issues.createLabel({ + owner, repo, name, color, description, + }); + core.info(`Created label "${name}".`); + } catch (err) { + if (err.status === 422) return; + core.warning(`Failed to create label "${name}": ${err.message || err}`); + } + } + + const openIssues = await github.paginate(github.rest.issues.listForRepo, { + owner, + repo, + state: "open", + per_page: 100, + }); + // listForRepo includes PRs; skip pull requests. + const issues = openIssues.filter((item) => !item.pull_request); + core.info(`Backfilling area labels on ${issues.length} open issue(s).`); + + let updated = 0; + for (const issue of issues) { + const labels = (issue.labels || []).map((l) => + typeof l === "string" ? l : l.name, + ); + const issueBody = stripTranslationBlock(issue.body || ""); + const areaLabels = detectAreaLabels({ + title: issue.title, + body: issueBody, + labels, + }); + const toAdd = areaLabels.filter((name) => !labels.includes(name)); + if (toAdd.length === 0) continue; + for (const name of toAdd) { + await ensureLabel(name); + } + try { + await github.rest.issues.addLabels({ + owner, + repo, + issue_number: issue.number, + labels: toAdd, + }); + updated += 1; + core.info(`#${issue.number}: +${toAdd.join(", ")}`); + } catch (err) { + core.warning(`#${issue.number}: failed to label: ${err.message || err}`); + } + // Soft rate-limit pacing for large open queues. + await new Promise((resolve) => setTimeout(resolve, 200)); + } + core.info(`Backfill complete: updated ${updated} / ${issues.length} open issue(s).`); diff --git a/docs-site/src/content/docs/contributing.md b/docs-site/src/content/docs/contributing.md index bc285f7a7..6fb7d6639 100644 --- a/docs-site/src/content/docs/contributing.md +++ b/docs-site/src/content/docs/contributing.md @@ -75,6 +75,14 @@ GitHub Actions intentionally stay small: Open issues labeled `needs-info` with no activity for 14 days get a warning; after 7 more idle days they close as not planned. Any update clears the stale warning. To keep long-lived work open, remove `needs-info` (for example when promoting an issue to `roadmap`). +- **Issue quality** (`.github/workflows/enforce-issue-quality.yml`) validates template structure on + new and edited issues, applies kind labels (`bug`, `enhancement`, `provider-compatibility`, + `documentation`), and adds orthogonal **area** labels from the form Area field plus light + title/body heuristics: `provider`, `account-pool`, `catalog`, `gui`, `cli`, `proxy`, `platform`, + `streaming`, `tools`, `install`, and `service`. Kind/process labels stay separate so you can + filter `bug` + `account-pool` without collapsing those axes. Prefer the Area dropdown over + inventing per-provider labels. Maintainers can re-apply area labels to all open issues with + workflow_dispatch `backfill_open_areas` after the workflow is on the default branch. Use the helper for releases: From 526aa03a76aed9f60d85e0f74ab1405e48c86cde Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Sat, 1 Aug 2026 05:26:14 +0200 Subject: [PATCH 2/5] fix(issues): tighten area-label heuristics against template noise Prefer title-scoped matches for platform/gui/cli and stronger full-body tokens so Environment OS fields do not blanket-label every bug report. --- .github/scripts/issue-quality.cjs | 63 ++++++++++++++++++++++---- .github/scripts/issue-quality.test.cjs | 7 +-- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/.github/scripts/issue-quality.cjs b/.github/scripts/issue-quality.cjs index 0672a6b11..49180199a 100644 --- a/.github/scripts/issue-quality.cjs +++ b/.github/scripts/issue-quality.cjs @@ -322,50 +322,90 @@ const AREA_FIELD_TO_LABELS = { other: [], }; +/** + * Heuristic rules. `scope: "title"` avoids false hits from template Environment / + * OS fields in the body; `scope: "full"` is for distinctive technical tokens. + */ const AREA_HEURISTICS = [ { label: "account-pool", - re: /\b(oauth|reauth|needsreauth|quota|account pool|codex.?auth|auto[- ]?switch|failover|refresh token|plan_type|chatgpt[- ]account|reset credit)\b/i, + scope: "full", + re: /\b(oauth|reauth|needsreauth|account pool|codex.?auth|auto[- ]?switch|account failover|refresh token|plan_type|chatgpt[- ]account|reset credit)\b/i, + }, + { + label: "account-pool", + scope: "title", + re: /\b(quota|failover|pool account|account switch)\b/i, }, { label: "catalog", - re: /\b(catalog|model list|model visibility|virtual model|routed (catalog|entries|slug)|model slug)\b/i, + scope: "full", + re: /\b(model catalog|opencodex-catalog|model list|model visibility|virtual model|routed (catalog|entries|slug)|model slug)\b/i, + }, + { + label: "catalog", + scope: "title", + re: /\bcatalog\b/i, }, { label: "gui", - re: /\b(dashboard|\bgui\b|tray|sidebar|settings tab)\b/i, + scope: "title", + re: /\b(dashboard|\bgui\b|tray|sidebar|settings (page|tab|ui))\b/i, }, { label: "cli", + scope: "title", re: /\b(ocx\b|config\.toml|config inject)\b/i, }, { label: "proxy", - re: /\b(reverse[- ]proxy|management api|admin[- ]token|\/api\/\*|bind(s)? the (old )?port|proxy running)\b/i, + scope: "full", + re: /\b(reverse[- ]proxy|management api|admin[- ]token|\/api\/\*|bind(s)? the (old )?port)\b/i, + }, + { + label: "proxy", + scope: "title", + re: /\b(reverse[- ]proxy|management api|admin[- ]token)\b/i, }, { label: "platform", - re: /\b(windows|macos|mac os|win32|darwin|wsl|winsw|launchd|systemd|icacls|\bacl\b)\b/i, + scope: "full", + re: /\b(winsw|launchd|schtasks|icacls|windows-latest|tray host|scheduler backend)\b/i, + }, + { + label: "platform", + scope: "title", + re: /\b(\[windows\]|\[macos\]|windows|macos|darwin|win32|wsl)\b/i, }, { label: "streaming", + scope: "full", re: /\b(sse|websocket|\bws\b|stream(ing)?\b.{0,40}\b(truncat|terminal)|terminal (sse )?frame|without a terminal)\b/i, }, { label: "tools", + scope: "full", re: /\b(tool_calls?|tool[- ]calls?|\bmcp\b|web[- ]search|tool[- ]recall)\b/i, }, { label: "install", + scope: "full", re: /\b(npm (global )?install|packaging|release asset|npx ocx)\b/i, }, { label: "service", + scope: "full", re: /\b(ocx service|winsw|scheduler backend|launchd service)\b/i, }, { label: "provider", - re: /\b(provider adapter|openai[- ]compatible|provider[- ]compat|adapter quirk|upstream api|built[- ]in provider|provider preset)\b/i, + scope: "full", + re: /\b(provider adapter|openai[- ]compatible|provider[- ]compat|adapter quirk|built[- ]in provider|provider preset)\b/i, + }, + { + label: "provider", + scope: "title", + re: /\b(\[provider\]|provider compat|openai[- ]compatible)\b/i, }, ]; @@ -399,10 +439,15 @@ function mapAreaFieldToLabels(areaText) { * @returns {string[]} */ function heuristicAreaLabels(title, body) { - const text = `${title || ""}\n${body || ""}`; + const titleText = title || ""; + const fullText = `${titleText}\n${body || ""}`; + const seen = new Set(); const out = []; - for (const { label, re } of AREA_HEURISTICS) { - if (re.test(text)) out.push(label); + for (const { label, re, scope } of AREA_HEURISTICS) { + const text = scope === "title" ? titleText : fullText; + if (!re.test(text) || seen.has(label)) continue; + seen.add(label); + out.push(label); } return out; } diff --git a/.github/scripts/issue-quality.test.cjs b/.github/scripts/issue-quality.test.cjs index 782beb7f0..b0d81d0cc 100644 --- a/.github/scripts/issue-quality.test.cjs +++ b/.github/scripts/issue-quality.test.cjs @@ -1737,12 +1737,13 @@ describe("detectAreaLabels", () => { "### Area", "Multiple areas", "### Summary", - "OpenCodex dashboard shows no data when ACL/icacls hardening cannot be verified.", + "Management API fails closed when icacls hardening cannot be verified.", ].join("\n"), labels: ["bug"], }); - assert.ok(labels.includes("gui")); - assert.ok(labels.includes("platform")); + assert.ok(labels.includes("gui"), `got ${labels.join(",")}`); + assert.ok(labels.includes("platform"), `got ${labels.join(",")}`); + assert.ok(labels.includes("proxy"), `got ${labels.join(",")}`); assert.equal(labels.includes("kiro"), false); assert.equal(labels.includes("gemini"), false); assert.equal(labels.includes("windows"), false); From 20401d3f3e70ce170fa943d333b361dd283a8116 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Sat, 1 Aug 2026 05:34:06 +0200 Subject: [PATCH 3/5] fix(issues): address area-label review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Omit Documentation Area→kind collision, restrict heuristics to semantic sections, feed translation text into area detection, and update the issue-quality workflow invariant test for the backfill guard. --- .github/scripts/issue-quality.cjs | 56 +++++++++++++++++--- .github/scripts/issue-quality.test.cjs | 42 +++++++++++++-- .github/workflows/enforce-issue-quality.yml | 58 +++++++++++++++------ docs-site/src/content/docs/contributing.md | 9 ++-- tests/ci-workflows.test.ts | 13 +++-- 5 files changed, 145 insertions(+), 33 deletions(-) diff --git a/.github/scripts/issue-quality.cjs b/.github/scripts/issue-quality.cjs index 49180199a..62f9ecb13 100644 --- a/.github/scripts/issue-quality.cjs +++ b/.github/scripts/issue-quality.cjs @@ -316,12 +316,31 @@ const AREA_FIELD_TO_LABELS = { "service lifecycle": ["service"], "service lifecycle (config injection)": ["service"], "platform (windows / macos / linux)": ["platform"], - documentation: ["documentation"], + // Do not map to kind label `documentation` — that collides with labelBasedKind + // when a feature/bug form picks Area: Documentation. Docs form already seeds + // the kind label; Area selection alone does not add an area tag. + documentation: [], // No dedicated label; heuristics still run in detectAreaLabels. "multiple areas": [], other: [], }; +/** Body headings used for area heuristics (excludes Environment / OS metadata). */ +const AREA_HEURISTIC_BODY_HEADINGS = [ + "Summary", + "Reproduction", + "What are you trying to accomplish?", + "What prevents this today?", + "What should OpenCodex do?", + "Example usage or interface", + "Current behaviour", + "Expected behaviour", + "Minimal redacted request or reproduction", + "What is wrong or missing?", + "Documentation problem type", + "Documentation location", +]; + /** * Heuristic rules. `scope: "title"` avoids false hits from template Environment / * OS fields in the body; `scope: "full"` is for distinctive technical tokens. @@ -431,11 +450,28 @@ function mapAreaFieldToLabels(areaText) { return AREA_FIELD_TO_LABELS[key] ? [...AREA_FIELD_TO_LABELS[key]] : []; } +/** + * Build heuristic text from title-relevant semantic sections only — never from + * Operating system / Version / Checks metadata that every template includes. + * + * @param {string} body + * @returns {string} + */ +function bodyForAreaHeuristics(body) { + if (typeof body !== "string" || !body.trim()) return ""; + const parts = []; + for (const heading of AREA_HEURISTIC_BODY_HEADINGS) { + const section = extractSection(body, heading); + if (section) parts.push(section); + } + return parts.join("\n\n"); +} + /** * Conservative title/body heuristics for orthogonal area labels. * * @param {string} title - * @param {string} body + * @param {string} body semantic body text (already filtered) * @returns {string[]} */ function heuristicAreaLabels(title, body) { @@ -456,17 +492,25 @@ function heuristicAreaLabels(title, body) { * Detect additive product-area labels from Area field, form defaults, and * title/body heuristics. Never invents per-provider labels. * - * @param {{ title?: string, body?: string, labels?: string[] }} issue + * @param {{ + * title?: string, + * body?: string, + * labels?: string[], + * heuristicBody?: string, + * }} issue + * `body` is the source form (for Area / provider headings). + * `heuristicBody` may include English translation text for heuristics only. * @returns {string[]} */ function detectAreaLabels(issue) { const title = typeof issue?.title === "string" ? issue.title : ""; const body = typeof issue?.body === "string" ? issue.body : ""; const labels = Array.isArray(issue?.labels) ? issue.labels : []; + const heuristicSource = typeof issue?.heuristicBody === "string" ? issue.heuristicBody : body; const areaSection = extractSection(body, "Area"); const fromArea = mapAreaFieldToLabels(areaSection); - const fromHeur = heuristicAreaLabels(title, body); + const fromHeur = heuristicAreaLabels(title, bodyForAreaHeuristics(heuristicSource)); const fromForm = []; if (labels.includes("provider-compatibility")) fromForm.push("provider"); // Provider-compat form uses this heading instead of Area. @@ -478,8 +522,7 @@ function detectAreaLabels(issue) { const out = []; for (const label of [...fromArea, ...fromForm, ...fromHeur]) { if (!label || seen.has(label)) continue; - // `documentation` is a kind label and also an Area mapping target. - if (label !== "documentation" && !AREA_LABELS[label]) continue; + if (!AREA_LABELS[label]) continue; seen.add(label); out.push(label); } @@ -1183,6 +1226,7 @@ module.exports = { AREA_LABELS, AREA_FIELD_TO_LABELS, mapAreaFieldToLabels, + bodyForAreaHeuristics, heuristicAreaLabels, detectAreaLabels, hasSubstantialStructuredContent, diff --git a/.github/scripts/issue-quality.test.cjs b/.github/scripts/issue-quality.test.cjs index b0d81d0cc..5b25c7e6d 100644 --- a/.github/scripts/issue-quality.test.cjs +++ b/.github/scripts/issue-quality.test.cjs @@ -1672,7 +1672,7 @@ describe("mapAreaFieldToLabels", () => { assert.deepEqual(mapAreaFieldToLabels("Installation or packaging"), ["install"]); assert.deepEqual(mapAreaFieldToLabels("Service lifecycle"), ["service"]); assert.deepEqual(mapAreaFieldToLabels("Platform (Windows / macOS / Linux)"), ["platform"]); - assert.deepEqual(mapAreaFieldToLabels("Documentation"), ["documentation"]); + assert.deepEqual(mapAreaFieldToLabels("Documentation"), []); }); it("maps legacy Service lifecycle wording and ignores Other / Multiple areas", () => { @@ -1749,13 +1749,47 @@ describe("detectAreaLabels", () => { assert.equal(labels.includes("windows"), false); }); - it("maps Documentation Area to documentation and does not invent docs", () => { + it("does not map Documentation Area onto the documentation kind label", () => { const labels = detectAreaLabels({ title: "Codex Auth UI/docs conflate usage-based switching", body: ["### Area", "Documentation", "### Summary", "Docs misdefine new session."].join("\n"), - labels: [], + labels: ["enhancement"], }); - assert.ok(labels.includes("documentation")); + assert.equal(labels.includes("documentation"), false); assert.equal(labels.includes("docs"), false); }); + + it("ignores Operating system metadata for platform heuristics", () => { + const labels = detectAreaLabels({ + title: "Dashboard shows empty providers tab", + body: [ + "### Area", + "Dashboard", + "### Summary", + "Providers tab is blank after login.", + "### Operating system", + "Windows 11", + "### Reproduction", + "1. Open the dashboard", + ].join("\n"), + labels: ["bug"], + }); + assert.ok(labels.includes("gui")); + assert.equal(labels.includes("platform"), false); + }); + + it("uses heuristicBody translation text when Area is Other", () => { + const labels = detectAreaLabels({ + title: "问题报告", + body: ["### Area", "Other", "### Summary", "原始描述"].join("\n"), + heuristicBody: [ + "### Area", + "Other", + "### Summary", + "Account pool failover fails when refresh token is already used.", + ].join("\n"), + labels: ["bug"], + }); + assert.ok(labels.includes("account-pool"), `got ${labels.join(",")}`); + }); }); diff --git a/.github/workflows/enforce-issue-quality.yml b/.github/workflows/enforce-issue-quality.yml index edf67f636..a0498bc84 100644 --- a/.github/workflows/enforce-issue-quality.yml +++ b/.github/workflows/enforce-issue-quality.yml @@ -734,7 +734,7 @@ jobs: rejectsWorkflowDispatchPullRequest, rejectsWorkflowDispatchNonDefaultBranch, } = require(path.join(process.cwd(), ".github", "scripts", "issue-quality.cjs")); - const { stripTranslationBlock } = require( + const { stripTranslationBlock, splitTranslationBlock } = require( path.join(process.cwd(), ".github", "scripts", "issue-translation.cjs"), ); @@ -756,6 +756,17 @@ jobs: } else { issue_number = context.payload.issue.number; } + + function translationPlainText(block) { + if (!block) return ""; + return String(block) + .replace(//g, " ") + .replace(/<\/?details[^>]*>/gi, "\n") + .replace(/<\/?summary[^>]*>/gi, "\n") + .replace(/<[^>]+>/g, " ") + .replace(/[ \t]+\n/g, "\n") + .trim(); + } const actor = context.actor; const eventType = context.eventName === "issues" ? context.payload.action @@ -791,7 +802,12 @@ jobs: return; } - const issueBody = stripTranslationBlock(issue.body || ""); + const translationSplit = splitTranslationBlock(issue.body || ""); + const issueBody = translationSplit.sourceBody; + const areaHeuristicBody = [ + issueBody, + translationPlainText(translationSplit.block), + ].filter(Boolean).join("\n\n"); // ----------------------------------------------------------------- // Trusted-user exemption @@ -909,14 +925,10 @@ jobs: } } const meta = AREA_LABELS[name]; - const color = meta?.color || (name === "documentation" ? "0075ca" : "EDEDED"); - const description = meta?.description - || (name === "documentation" - ? "Improvements or additions to documentation" - : name); + if (!meta) return; try { await github.rest.issues.createLabel({ - owner, repo, name, color, description, + owner, repo, name, color: meta.color, description: meta.description, }); core.info(`Created label "${name}".`); } catch (err) { @@ -931,6 +943,7 @@ jobs: const areaLabels = detectAreaLabels({ title: issue.title, body: issueBody, + heuristicBody: areaHeuristicBody, labels, }); const areaLabelsToAdd = areaLabels.filter((name) => !labels.includes(name)); @@ -1139,7 +1152,7 @@ jobs: AREA_LABELS, rejectsWorkflowDispatchNonDefaultBranch, } = require(path.join(process.cwd(), ".github", "scripts", "issue-quality.cjs")); - const { stripTranslationBlock } = require( + const { splitTranslationBlock } = require( path.join(process.cwd(), ".github", "scripts", "issue-translation.cjs"), ); @@ -1155,6 +1168,17 @@ jobs: const { owner, repo } = context.repo; + function translationPlainText(block) { + if (!block) return ""; + return String(block) + .replace(//g, " ") + .replace(/<\/?details[^>]*>/gi, "\n") + .replace(/<\/?summary[^>]*>/gi, "\n") + .replace(/<[^>]+>/g, " ") + .replace(/[ \t]+\n/g, "\n") + .trim(); + } + async function ensureLabel(name) { try { await github.rest.issues.getLabel({ owner, repo, name }); @@ -1166,14 +1190,10 @@ jobs: } } const meta = AREA_LABELS[name]; - const color = meta?.color || (name === "documentation" ? "0075ca" : "EDEDED"); - const description = meta?.description - || (name === "documentation" - ? "Improvements or additions to documentation" - : name); + if (!meta) return; try { await github.rest.issues.createLabel({ - owner, repo, name, color, description, + owner, repo, name, color: meta.color, description: meta.description, }); core.info(`Created label "${name}".`); } catch (err) { @@ -1197,10 +1217,16 @@ jobs: const labels = (issue.labels || []).map((l) => typeof l === "string" ? l : l.name, ); - const issueBody = stripTranslationBlock(issue.body || ""); + const translationSplit = splitTranslationBlock(issue.body || ""); + const issueBody = translationSplit.sourceBody; + const areaHeuristicBody = [ + issueBody, + translationPlainText(translationSplit.block), + ].filter(Boolean).join("\n\n"); const areaLabels = detectAreaLabels({ title: issue.title, body: issueBody, + heuristicBody: areaHeuristicBody, labels, }); const toAdd = areaLabels.filter((name) => !labels.includes(name)); diff --git a/docs-site/src/content/docs/contributing.md b/docs-site/src/content/docs/contributing.md index 6fb7d6639..c505881c0 100644 --- a/docs-site/src/content/docs/contributing.md +++ b/docs-site/src/content/docs/contributing.md @@ -78,10 +78,11 @@ GitHub Actions intentionally stay small: - **Issue quality** (`.github/workflows/enforce-issue-quality.yml`) validates template structure on new and edited issues, applies kind labels (`bug`, `enhancement`, `provider-compatibility`, `documentation`), and adds orthogonal **area** labels from the form Area field plus light - title/body heuristics: `provider`, `account-pool`, `catalog`, `gui`, `cli`, `proxy`, `platform`, - `streaming`, `tools`, `install`, and `service`. Kind/process labels stay separate so you can - filter `bug` + `account-pool` without collapsing those axes. Prefer the Area dropdown over - inventing per-provider labels. Maintainers can re-apply area labels to all open issues with + title/Summary heuristics: `provider`, `account-pool`, `catalog`, `gui`, `cli`, `proxy`, + `platform`, `streaming`, `tools`, `install`, and `service`. Kind/process labels stay separate so + you can filter `bug` + `account-pool` without collapsing those axes. Prefer the Area dropdown + over inventing per-provider labels. Area: Documentation does not add a second area tag (the docs + form already seeds `documentation`). Maintainers can re-apply area labels to all open issues with workflow_dispatch `backfill_open_areas` after the workflow is on the default branch. Use the helper for releases: diff --git a/tests/ci-workflows.test.ts b/tests/ci-workflows.test.ts index f5b6374d6..d9c6ae8cb 100644 --- a/tests/ci-workflows.test.ts +++ b/tests/ci-workflows.test.ts @@ -1974,11 +1974,16 @@ describe("GitHub Actions hardening", () => { expect(workflow).toContain("group: issue-translation-${{ github.event.issue.number }}"); expect(workflow).not.toContain("issue-comment-translation-${{ github.event.comment.id }}"); expect(workflow).toContain("if: github.event_name == 'issue_comment'"); + // translate/validate skip open-area backfill; backfill job is area-only. + expect(workflow).toContain("backfill_open_areas"); + expect(workflow).toContain("backfill-open-areas:"); + expect(workflow).toMatch(/inputs\.backfill_open_areas != true/); + expect(workflow).toMatch(/inputs\.backfill_open_areas == true/); expect(workflow).toMatch( - /translate:\s*\n\s*name: Translate non-English issues\s*\n\s*if: github\.event_name == 'issues' \|\| github\.event_name == 'workflow_dispatch'/, + /translate:\s*\n\s*name: Translate non-English issues\s*\n\s*if: >\s*\n\s*github\.event_name == 'issues' \|\|\s*\n\s*\(github\.event_name == 'workflow_dispatch' &&\s*\n\s*inputs\.backfill_open_areas != true &&\s*\n\s*inputs\.issue_number != ''\)/, ); expect(workflow).toMatch( - /validate:\s*\n\s*if: github\.event_name == 'issues' \|\| github\.event_name == 'workflow_dispatch'/, + /validate:\s*\n\s*if: >\s*\n\s*github\.event_name == 'issues' \|\|\s*\n\s*\(github\.event_name == 'workflow_dispatch' &&\s*\n\s*inputs\.backfill_open_areas != true &&\s*\n\s*inputs\.issue_number != ''\)/, ); const commentJob = workflow.split(/\n {2}translate-comment:\n/)[1]!.split(/\n {2}[a-zA-Z]/)[0]!; @@ -2024,7 +2029,9 @@ describe("GitHub Actions hardening", () => { expect(beforeJobs).not.toMatch(/^\s*permissions:/m); // Non-cancelling per-issue concurrency at workflow and translate-job scope. - expect(workflow).toContain("group: issue-quality-${{ github.event.issue.number || inputs.issue_number }}"); + expect(workflow).toContain( + "group: issue-quality-${{ github.event.issue.number || inputs.issue_number || (inputs.backfill_open_areas && 'backfill-open-areas') || 'manual' }}", + ); expect(workflow).toContain("group: issue-translation-${{ github.event.issue.number || inputs.issue_number }}"); const workflowConcurrency = workflow.split(/jobs:\s*\n/)[0]!; expect(workflowConcurrency).toMatch( From 5f5b2c4be88634be3ee39be0bf21f9591ecd3723 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Sat, 1 Aug 2026 05:50:25 +0200 Subject: [PATCH 4/5] fix(issues): harden area-label apply path after CodeRabbit review Fix streaming truncat stem matching, only addLabels for labels ensureLabel confirmed available, and document issues:write on the backfill job. --- .github/scripts/issue-quality.cjs | 2 +- .github/scripts/issue-quality.test.cjs | 9 ++++++ .github/workflows/enforce-issue-quality.yml | 36 ++++++++++++--------- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/.github/scripts/issue-quality.cjs b/.github/scripts/issue-quality.cjs index 62f9ecb13..b2657f655 100644 --- a/.github/scripts/issue-quality.cjs +++ b/.github/scripts/issue-quality.cjs @@ -399,7 +399,7 @@ const AREA_HEURISTICS = [ { label: "streaming", scope: "full", - re: /\b(sse|websocket|\bws\b|stream(ing)?\b.{0,40}\b(truncat|terminal)|terminal (sse )?frame|without a terminal)\b/i, + re: /\b(sse|websocket|\bws\b|stream(ing)?\b.{0,40}\btruncat\w*|stream(ing)?\b.{0,40}\bterminal\b|terminal (sse )?frame|without a terminal)\b/i, }, { label: "tools", diff --git a/.github/scripts/issue-quality.test.cjs b/.github/scripts/issue-quality.test.cjs index 5b25c7e6d..fedb496fe 100644 --- a/.github/scripts/issue-quality.test.cjs +++ b/.github/scripts/issue-quality.test.cjs @@ -1792,4 +1792,13 @@ describe("detectAreaLabels", () => { }); assert.ok(labels.includes("account-pool"), `got ${labels.join(",")}`); }); + + it("matches truncated streaming wording via truncat stem", () => { + const labels = detectAreaLabels({ + title: "Upstream streaming response truncated mid-turn", + body: ["### Area", "Other", "### Summary", "The streaming response was truncated."].join("\n"), + labels: ["bug"], + }); + assert.ok(labels.includes("streaming"), `got ${labels.join(",")}`); + }); }); diff --git a/.github/workflows/enforce-issue-quality.yml b/.github/workflows/enforce-issue-quality.yml index a0498bc84..850aea6bb 100644 --- a/.github/workflows/enforce-issue-quality.yml +++ b/.github/workflows/enforce-issue-quality.yml @@ -917,26 +917,28 @@ jobs: async function ensureLabel(name) { try { await github.rest.issues.getLabel({ owner, repo, name }); - return; + return true; } catch (err) { if (err.status !== 404) { core.warning(`Failed to look up label "${name}": ${err.message || err}`); - return; + return false; } } const meta = AREA_LABELS[name]; - if (!meta) return; + if (!meta) return false; try { await github.rest.issues.createLabel({ owner, repo, name, color: meta.color, description: meta.description, }); core.info(`Created label "${name}".`); + return true; } catch (err) { if (err.status === 422) { core.info(`Label "${name}" appeared concurrently; continuing.`); - return; + return true; } core.warning(`Failed to create label "${name}": ${err.message || err}`); + return false; } } @@ -946,9 +948,9 @@ jobs: heuristicBody: areaHeuristicBody, labels, }); - const areaLabelsToAdd = areaLabels.filter((name) => !labels.includes(name)); - for (const name of areaLabelsToAdd) { - await ensureLabel(name); + const areaLabelsToAdd = []; + for (const name of areaLabels.filter((candidate) => !labels.includes(candidate))) { + if (await ensureLabel(name)) areaLabelsToAdd.push(name); } if (areaLabelsToAdd.length > 0) { try { @@ -1132,7 +1134,9 @@ jobs: if: github.event_name == 'workflow_dispatch' && inputs.backfill_open_areas == true runs-on: ubuntu-latest permissions: + # Read-only checkout of trusted scripts from the default branch. contents: read + # Required to list open issues, create missing area labels, and apply them. issues: write steps: - name: Checkout trusted workflow code @@ -1182,23 +1186,25 @@ jobs: async function ensureLabel(name) { try { await github.rest.issues.getLabel({ owner, repo, name }); - return; + return true; } catch (err) { if (err.status !== 404) { core.warning(`Failed to look up label "${name}": ${err.message || err}`); - return; + return false; } } const meta = AREA_LABELS[name]; - if (!meta) return; + if (!meta) return false; try { await github.rest.issues.createLabel({ owner, repo, name, color: meta.color, description: meta.description, }); core.info(`Created label "${name}".`); + return true; } catch (err) { - if (err.status === 422) return; + if (err.status === 422) return true; core.warning(`Failed to create label "${name}": ${err.message || err}`); + return false; } } @@ -1229,11 +1235,11 @@ jobs: heuristicBody: areaHeuristicBody, labels, }); - const toAdd = areaLabels.filter((name) => !labels.includes(name)); - if (toAdd.length === 0) continue; - for (const name of toAdd) { - await ensureLabel(name); + const toAdd = []; + for (const name of areaLabels.filter((candidate) => !labels.includes(candidate))) { + if (await ensureLabel(name)) toAdd.push(name); } + if (toAdd.length === 0) continue; try { await github.rest.issues.addLabels({ owner, From cc0ac777df4e01588bbf9f0d53bb2cccf4604760 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Sat, 1 Aug 2026 06:00:01 +0200 Subject: [PATCH 5/5] fix(issues): run validate after translate for area heuristics Add needs: translate so area labeling can read the inline English translation block on the same issue event, while still running validate when translate is skipped or fails. --- .github/workflows/enforce-issue-quality.yml | 14 ++++++++++---- tests/ci-workflows.test.ts | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/enforce-issue-quality.yml b/.github/workflows/enforce-issue-quality.yml index 850aea6bb..1afef369e 100644 --- a/.github/workflows/enforce-issue-quality.yml +++ b/.github/workflows/enforce-issue-quality.yml @@ -695,11 +695,17 @@ jobs: } validate: + # Wait for translate so area heuristics can read the inline English block + # when translation wrote one. Still run if translate was skipped or failed — + # quality closure must not depend on translation success. + needs: translate if: > - github.event_name == 'issues' || - (github.event_name == 'workflow_dispatch' && - inputs.backfill_open_areas != true && - inputs.issue_number != '') + always() && + needs.translate.result != 'cancelled' && + (github.event_name == 'issues' || + (github.event_name == 'workflow_dispatch' && + inputs.backfill_open_areas != true && + inputs.issue_number != '')) runs-on: ubuntu-latest permissions: contents: read diff --git a/tests/ci-workflows.test.ts b/tests/ci-workflows.test.ts index d9c6ae8cb..172267589 100644 --- a/tests/ci-workflows.test.ts +++ b/tests/ci-workflows.test.ts @@ -1983,7 +1983,7 @@ describe("GitHub Actions hardening", () => { /translate:\s*\n\s*name: Translate non-English issues\s*\n\s*if: >\s*\n\s*github\.event_name == 'issues' \|\|\s*\n\s*\(github\.event_name == 'workflow_dispatch' &&\s*\n\s*inputs\.backfill_open_areas != true &&\s*\n\s*inputs\.issue_number != ''\)/, ); expect(workflow).toMatch( - /validate:\s*\n\s*if: >\s*\n\s*github\.event_name == 'issues' \|\|\s*\n\s*\(github\.event_name == 'workflow_dispatch' &&\s*\n\s*inputs\.backfill_open_areas != true &&\s*\n\s*inputs\.issue_number != ''\)/, + /validate:\s*\n\s*# Wait for translate[\s\S]*?\n\s*needs: translate\s*\n\s*if: >\s*\n\s*always\(\) &&\s*\n\s*needs\.translate\.result != 'cancelled' &&\s*\n\s*\(github\.event_name == 'issues' \|\|\s*\n\s*\(github\.event_name == 'workflow_dispatch' &&\s*\n\s*inputs\.backfill_open_areas != true &&\s*\n\s*inputs\.issue_number != ''\)\)/, ); const commentJob = workflow.split(/\n {2}translate-comment:\n/)[1]!.split(/\n {2}[a-zA-Z]/)[0]!;