Files
williamfzc 2efadece34 feat: add scheduled issue labeler for type/domain triage (#251)
* ci: add issue labeler workflow

Add a manual GitHub Actions workflow and script to poll issues and apply type/domain labels.

* feat(issue-labels): refine heuristics and add docs

Improve domain detection and add safeguards to avoid overriding manual type triage by default. Refresh regression samples from real issues and document usage.

* ci(issue-labels): enable hourly scheduled labeling

Run hourly on schedule with write mode by default while keeping manual dispatch dry-run by default.

* ci(issue-labels): shorten lookback window to 6h

Reduce scheduled scan window while keeping overlap for missed runs.

* ci(issue-labels): opt into Node 24 actions runtime

Set FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 and use Node 24 for the script runtime to avoid upcoming Node 20 deprecation warnings.

* ci(issue-labels): restore lookback input for manual runs

Allow workflow_dispatch to override lookback_hours while keeping hourly schedule fixed.

* ci(issue-labels): upgrade checkout/setup-node to v6

Use actions/checkout@v6 and actions/setup-node@v6 to align with Node 24 runtime and avoid Node 20 deprecation warnings.

* fix(ci): label only unlabeled issues via search api

* fix(ci): refine issue labeling heuristics from live issues

* fix(ci): address remaining issue label review comments

* fix(ci): fix issue label arg parsing regression

* docs(issue-labels): clarify one-shot unlabeled triage scope
2026-04-07 10:35:40 +08:00

71 lines
2.1 KiB
JavaScript

const fs = require("fs");
const path = require("path");
const { classifyIssueText } = require("./index.js");
const samplesPath = path.join(__dirname, "samples.json");
const samples = JSON.parse(fs.readFileSync(samplesPath, "utf8"));
/**
* Convert an array-like value into a sorted string array.
*
* @param {Array<unknown>|undefined|null} arr
* @returns {string[]}
*/
function sortArray(arr) {
return (arr || []).map(String).sort();
}
/**
* Check whether every element in sub exists in sup.
*
* @param {string[]} sub
* @param {string[]} sup
* @returns {boolean}
*/
function isSubset(sub, sup) {
const set = new Set(sup || []);
for (const x of sub || []) {
if (!set.has(x)) return false;
}
return true;
}
let passed = 0;
let failed = 0;
for (const sample of samples) {
try {
const result = classifyIssueText(sample.title, sample.body);
const hasExpectedType = Object.prototype.hasOwnProperty.call(sample, "expected_type");
const expectedType = hasExpectedType ? sample.expected_type : undefined;
const matchType = hasExpectedType ? (result.type || null) === expectedType : true;
const actualDomains = sortArray(result.domains);
const expectedDomains = sortArray(sample.expected_domains);
const hasExpectedDomains = Object.prototype.hasOwnProperty.call(sample, "expected_domains");
const matchDomains = !hasExpectedDomains
? true
: expectedDomains.length === 0
? actualDomains.length === 0
: isSubset(expectedDomains, actualDomains);
if (matchType && matchDomains) {
console.log(`✅ Passed: ${sample.name}`);
passed += 1;
} else {
console.log(`❌ Failed: ${sample.name}`);
console.log(` Type expected: ${expectedType}, got: ${result.type}`);
console.log(` Domains expected(subset): ${expectedDomains}, got: ${actualDomains}`);
failed += 1;
}
} catch (e) {
console.log(`❌ Failed: ${sample.name} (Execution error)`);
console.error(e && e.message ? e.message : String(e));
failed += 1;
}
}
console.log(`\nTest Summary: ${passed} passed, ${failed} failed`);
if (failed > 0) process.exit(1);