Skip to content

Commit 20401d3

Browse files
committed
fix(issues): address area-label review findings
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.
1 parent 526aa03 commit 20401d3

5 files changed

Lines changed: 145 additions & 33 deletions

File tree

.github/scripts/issue-quality.cjs

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -316,12 +316,31 @@ const AREA_FIELD_TO_LABELS = {
316316
"service lifecycle": ["service"],
317317
"service lifecycle (config injection)": ["service"],
318318
"platform (windows / macos / linux)": ["platform"],
319-
documentation: ["documentation"],
319+
// Do not map to kind label `documentation` — that collides with labelBasedKind
320+
// when a feature/bug form picks Area: Documentation. Docs form already seeds
321+
// the kind label; Area selection alone does not add an area tag.
322+
documentation: [],
320323
// No dedicated label; heuristics still run in detectAreaLabels.
321324
"multiple areas": [],
322325
other: [],
323326
};
324327

328+
/** Body headings used for area heuristics (excludes Environment / OS metadata). */
329+
const AREA_HEURISTIC_BODY_HEADINGS = [
330+
"Summary",
331+
"Reproduction",
332+
"What are you trying to accomplish?",
333+
"What prevents this today?",
334+
"What should OpenCodex do?",
335+
"Example usage or interface",
336+
"Current behaviour",
337+
"Expected behaviour",
338+
"Minimal redacted request or reproduction",
339+
"What is wrong or missing?",
340+
"Documentation problem type",
341+
"Documentation location",
342+
];
343+
325344
/**
326345
* Heuristic rules. `scope: "title"` avoids false hits from template Environment /
327346
* OS fields in the body; `scope: "full"` is for distinctive technical tokens.
@@ -431,11 +450,28 @@ function mapAreaFieldToLabels(areaText) {
431450
return AREA_FIELD_TO_LABELS[key] ? [...AREA_FIELD_TO_LABELS[key]] : [];
432451
}
433452

453+
/**
454+
* Build heuristic text from title-relevant semantic sections only — never from
455+
* Operating system / Version / Checks metadata that every template includes.
456+
*
457+
* @param {string} body
458+
* @returns {string}
459+
*/
460+
function bodyForAreaHeuristics(body) {
461+
if (typeof body !== "string" || !body.trim()) return "";
462+
const parts = [];
463+
for (const heading of AREA_HEURISTIC_BODY_HEADINGS) {
464+
const section = extractSection(body, heading);
465+
if (section) parts.push(section);
466+
}
467+
return parts.join("\n\n");
468+
}
469+
434470
/**
435471
* Conservative title/body heuristics for orthogonal area labels.
436472
*
437473
* @param {string} title
438-
* @param {string} body
474+
* @param {string} body semantic body text (already filtered)
439475
* @returns {string[]}
440476
*/
441477
function heuristicAreaLabels(title, body) {
@@ -456,17 +492,25 @@ function heuristicAreaLabels(title, body) {
456492
* Detect additive product-area labels from Area field, form defaults, and
457493
* title/body heuristics. Never invents per-provider labels.
458494
*
459-
* @param {{ title?: string, body?: string, labels?: string[] }} issue
495+
* @param {{
496+
* title?: string,
497+
* body?: string,
498+
* labels?: string[],
499+
* heuristicBody?: string,
500+
* }} issue
501+
* `body` is the source form (for Area / provider headings).
502+
* `heuristicBody` may include English translation text for heuristics only.
460503
* @returns {string[]}
461504
*/
462505
function detectAreaLabels(issue) {
463506
const title = typeof issue?.title === "string" ? issue.title : "";
464507
const body = typeof issue?.body === "string" ? issue.body : "";
465508
const labels = Array.isArray(issue?.labels) ? issue.labels : [];
509+
const heuristicSource = typeof issue?.heuristicBody === "string" ? issue.heuristicBody : body;
466510

467511
const areaSection = extractSection(body, "Area");
468512
const fromArea = mapAreaFieldToLabels(areaSection);
469-
const fromHeur = heuristicAreaLabels(title, body);
513+
const fromHeur = heuristicAreaLabels(title, bodyForAreaHeuristics(heuristicSource));
470514
const fromForm = [];
471515
if (labels.includes("provider-compatibility")) fromForm.push("provider");
472516
// Provider-compat form uses this heading instead of Area.
@@ -478,8 +522,7 @@ function detectAreaLabels(issue) {
478522
const out = [];
479523
for (const label of [...fromArea, ...fromForm, ...fromHeur]) {
480524
if (!label || seen.has(label)) continue;
481-
// `documentation` is a kind label and also an Area mapping target.
482-
if (label !== "documentation" && !AREA_LABELS[label]) continue;
525+
if (!AREA_LABELS[label]) continue;
483526
seen.add(label);
484527
out.push(label);
485528
}
@@ -1183,6 +1226,7 @@ module.exports = {
11831226
AREA_LABELS,
11841227
AREA_FIELD_TO_LABELS,
11851228
mapAreaFieldToLabels,
1229+
bodyForAreaHeuristics,
11861230
heuristicAreaLabels,
11871231
detectAreaLabels,
11881232
hasSubstantialStructuredContent,

.github/scripts/issue-quality.test.cjs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1672,7 +1672,7 @@ describe("mapAreaFieldToLabels", () => {
16721672
assert.deepEqual(mapAreaFieldToLabels("Installation or packaging"), ["install"]);
16731673
assert.deepEqual(mapAreaFieldToLabels("Service lifecycle"), ["service"]);
16741674
assert.deepEqual(mapAreaFieldToLabels("Platform (Windows / macOS / Linux)"), ["platform"]);
1675-
assert.deepEqual(mapAreaFieldToLabels("Documentation"), ["documentation"]);
1675+
assert.deepEqual(mapAreaFieldToLabels("Documentation"), []);
16761676
});
16771677

16781678
it("maps legacy Service lifecycle wording and ignores Other / Multiple areas", () => {
@@ -1749,13 +1749,47 @@ describe("detectAreaLabels", () => {
17491749
assert.equal(labels.includes("windows"), false);
17501750
});
17511751

1752-
it("maps Documentation Area to documentation and does not invent docs", () => {
1752+
it("does not map Documentation Area onto the documentation kind label", () => {
17531753
const labels = detectAreaLabels({
17541754
title: "Codex Auth UI/docs conflate usage-based switching",
17551755
body: ["### Area", "Documentation", "### Summary", "Docs misdefine new session."].join("\n"),
1756-
labels: [],
1756+
labels: ["enhancement"],
17571757
});
1758-
assert.ok(labels.includes("documentation"));
1758+
assert.equal(labels.includes("documentation"), false);
17591759
assert.equal(labels.includes("docs"), false);
17601760
});
1761+
1762+
it("ignores Operating system metadata for platform heuristics", () => {
1763+
const labels = detectAreaLabels({
1764+
title: "Dashboard shows empty providers tab",
1765+
body: [
1766+
"### Area",
1767+
"Dashboard",
1768+
"### Summary",
1769+
"Providers tab is blank after login.",
1770+
"### Operating system",
1771+
"Windows 11",
1772+
"### Reproduction",
1773+
"1. Open the dashboard",
1774+
].join("\n"),
1775+
labels: ["bug"],
1776+
});
1777+
assert.ok(labels.includes("gui"));
1778+
assert.equal(labels.includes("platform"), false);
1779+
});
1780+
1781+
it("uses heuristicBody translation text when Area is Other", () => {
1782+
const labels = detectAreaLabels({
1783+
title: "问题报告",
1784+
body: ["### Area", "Other", "### Summary", "原始描述"].join("\n"),
1785+
heuristicBody: [
1786+
"### Area",
1787+
"Other",
1788+
"### Summary",
1789+
"Account pool failover fails when refresh token is already used.",
1790+
].join("\n"),
1791+
labels: ["bug"],
1792+
});
1793+
assert.ok(labels.includes("account-pool"), `got ${labels.join(",")}`);
1794+
});
17611795
});

.github/workflows/enforce-issue-quality.yml

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -734,7 +734,7 @@ jobs:
734734
rejectsWorkflowDispatchPullRequest,
735735
rejectsWorkflowDispatchNonDefaultBranch,
736736
} = require(path.join(process.cwd(), ".github", "scripts", "issue-quality.cjs"));
737-
const { stripTranslationBlock } = require(
737+
const { stripTranslationBlock, splitTranslationBlock } = require(
738738
path.join(process.cwd(), ".github", "scripts", "issue-translation.cjs"),
739739
);
740740
@@ -756,6 +756,17 @@ jobs:
756756
} else {
757757
issue_number = context.payload.issue.number;
758758
}
759+
760+
function translationPlainText(block) {
761+
if (!block) return "";
762+
return String(block)
763+
.replace(/<!--[\s\S]*?-->/g, " ")
764+
.replace(/<\/?details[^>]*>/gi, "\n")
765+
.replace(/<\/?summary[^>]*>/gi, "\n")
766+
.replace(/<[^>]+>/g, " ")
767+
.replace(/[ \t]+\n/g, "\n")
768+
.trim();
769+
}
759770
const actor = context.actor;
760771
const eventType = context.eventName === "issues"
761772
? context.payload.action
@@ -791,7 +802,12 @@ jobs:
791802
return;
792803
}
793804
794-
const issueBody = stripTranslationBlock(issue.body || "");
805+
const translationSplit = splitTranslationBlock(issue.body || "");
806+
const issueBody = translationSplit.sourceBody;
807+
const areaHeuristicBody = [
808+
issueBody,
809+
translationPlainText(translationSplit.block),
810+
].filter(Boolean).join("\n\n");
795811
796812
// -----------------------------------------------------------------
797813
// Trusted-user exemption
@@ -909,14 +925,10 @@ jobs:
909925
}
910926
}
911927
const meta = AREA_LABELS[name];
912-
const color = meta?.color || (name === "documentation" ? "0075ca" : "EDEDED");
913-
const description = meta?.description
914-
|| (name === "documentation"
915-
? "Improvements or additions to documentation"
916-
: name);
928+
if (!meta) return;
917929
try {
918930
await github.rest.issues.createLabel({
919-
owner, repo, name, color, description,
931+
owner, repo, name, color: meta.color, description: meta.description,
920932
});
921933
core.info(`Created label "${name}".`);
922934
} catch (err) {
@@ -931,6 +943,7 @@ jobs:
931943
const areaLabels = detectAreaLabels({
932944
title: issue.title,
933945
body: issueBody,
946+
heuristicBody: areaHeuristicBody,
934947
labels,
935948
});
936949
const areaLabelsToAdd = areaLabels.filter((name) => !labels.includes(name));
@@ -1139,7 +1152,7 @@ jobs:
11391152
AREA_LABELS,
11401153
rejectsWorkflowDispatchNonDefaultBranch,
11411154
} = require(path.join(process.cwd(), ".github", "scripts", "issue-quality.cjs"));
1142-
const { stripTranslationBlock } = require(
1155+
const { splitTranslationBlock } = require(
11431156
path.join(process.cwd(), ".github", "scripts", "issue-translation.cjs"),
11441157
);
11451158
@@ -1155,6 +1168,17 @@ jobs:
11551168
11561169
const { owner, repo } = context.repo;
11571170
1171+
function translationPlainText(block) {
1172+
if (!block) return "";
1173+
return String(block)
1174+
.replace(/<!--[\s\S]*?-->/g, " ")
1175+
.replace(/<\/?details[^>]*>/gi, "\n")
1176+
.replace(/<\/?summary[^>]*>/gi, "\n")
1177+
.replace(/<[^>]+>/g, " ")
1178+
.replace(/[ \t]+\n/g, "\n")
1179+
.trim();
1180+
}
1181+
11581182
async function ensureLabel(name) {
11591183
try {
11601184
await github.rest.issues.getLabel({ owner, repo, name });
@@ -1166,14 +1190,10 @@ jobs:
11661190
}
11671191
}
11681192
const meta = AREA_LABELS[name];
1169-
const color = meta?.color || (name === "documentation" ? "0075ca" : "EDEDED");
1170-
const description = meta?.description
1171-
|| (name === "documentation"
1172-
? "Improvements or additions to documentation"
1173-
: name);
1193+
if (!meta) return;
11741194
try {
11751195
await github.rest.issues.createLabel({
1176-
owner, repo, name, color, description,
1196+
owner, repo, name, color: meta.color, description: meta.description,
11771197
});
11781198
core.info(`Created label "${name}".`);
11791199
} catch (err) {
@@ -1197,10 +1217,16 @@ jobs:
11971217
const labels = (issue.labels || []).map((l) =>
11981218
typeof l === "string" ? l : l.name,
11991219
);
1200-
const issueBody = stripTranslationBlock(issue.body || "");
1220+
const translationSplit = splitTranslationBlock(issue.body || "");
1221+
const issueBody = translationSplit.sourceBody;
1222+
const areaHeuristicBody = [
1223+
issueBody,
1224+
translationPlainText(translationSplit.block),
1225+
].filter(Boolean).join("\n\n");
12011226
const areaLabels = detectAreaLabels({
12021227
title: issue.title,
12031228
body: issueBody,
1229+
heuristicBody: areaHeuristicBody,
12041230
labels,
12051231
});
12061232
const toAdd = areaLabels.filter((name) => !labels.includes(name));

docs-site/src/content/docs/contributing.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,11 @@ GitHub Actions intentionally stay small:
7878
- **Issue quality** (`.github/workflows/enforce-issue-quality.yml`) validates template structure on
7979
new and edited issues, applies kind labels (`bug`, `enhancement`, `provider-compatibility`,
8080
`documentation`), and adds orthogonal **area** labels from the form Area field plus light
81-
title/body heuristics: `provider`, `account-pool`, `catalog`, `gui`, `cli`, `proxy`, `platform`,
82-
`streaming`, `tools`, `install`, and `service`. Kind/process labels stay separate so you can
83-
filter `bug` + `account-pool` without collapsing those axes. Prefer the Area dropdown over
84-
inventing per-provider labels. Maintainers can re-apply area labels to all open issues with
81+
title/Summary heuristics: `provider`, `account-pool`, `catalog`, `gui`, `cli`, `proxy`,
82+
`platform`, `streaming`, `tools`, `install`, and `service`. Kind/process labels stay separate so
83+
you can filter `bug` + `account-pool` without collapsing those axes. Prefer the Area dropdown
84+
over inventing per-provider labels. Area: Documentation does not add a second area tag (the docs
85+
form already seeds `documentation`). Maintainers can re-apply area labels to all open issues with
8586
workflow_dispatch `backfill_open_areas` after the workflow is on the default branch.
8687

8788
Use the helper for releases:

tests/ci-workflows.test.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1974,11 +1974,16 @@ describe("GitHub Actions hardening", () => {
19741974
expect(workflow).toContain("group: issue-translation-${{ github.event.issue.number }}");
19751975
expect(workflow).not.toContain("issue-comment-translation-${{ github.event.comment.id }}");
19761976
expect(workflow).toContain("if: github.event_name == 'issue_comment'");
1977+
// translate/validate skip open-area backfill; backfill job is area-only.
1978+
expect(workflow).toContain("backfill_open_areas");
1979+
expect(workflow).toContain("backfill-open-areas:");
1980+
expect(workflow).toMatch(/inputs\.backfill_open_areas != true/);
1981+
expect(workflow).toMatch(/inputs\.backfill_open_areas == true/);
19771982
expect(workflow).toMatch(
1978-
/translate:\s*\n\s*name: Translate non-English issues\s*\n\s*if: github\.event_name == 'issues' \|\| github\.event_name == 'workflow_dispatch'/,
1983+
/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 != ''\)/,
19791984
);
19801985
expect(workflow).toMatch(
1981-
/validate:\s*\n\s*if: github\.event_name == 'issues' \|\| github\.event_name == 'workflow_dispatch'/,
1986+
/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 != ''\)/,
19821987
);
19831988

19841989
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", () => {
20242029
expect(beforeJobs).not.toMatch(/^\s*permissions:/m);
20252030

20262031
// Non-cancelling per-issue concurrency at workflow and translate-job scope.
2027-
expect(workflow).toContain("group: issue-quality-${{ github.event.issue.number || inputs.issue_number }}");
2032+
expect(workflow).toContain(
2033+
"group: issue-quality-${{ github.event.issue.number || inputs.issue_number || (inputs.backfill_open_areas && 'backfill-open-areas') || 'manual' }}",
2034+
);
20282035
expect(workflow).toContain("group: issue-translation-${{ github.event.issue.number || inputs.issue_number }}");
20292036
const workflowConcurrency = workflow.split(/jobs:\s*\n/)[0]!;
20302037
expect(workflowConcurrency).toMatch(

0 commit comments

Comments
 (0)