Compare commits

...
Author SHA1 Message Date
bincxz 4400273997 fix(ci): prefer maintainer @bot over author follow-up routing
When a trusted maintainer is also the issue author, route explicit
@netcatty-bot mentions as maintainer re-triage so auto-closed
already-available/unclear issues can re-enter classify.
2026-08-05 21:01:12 +08:00
bincxz 95ccb0ae66 fix(ci): let maintainer @bot reclassify auto-closed issues
Author reopen disputes still stay on issue_followup when
triage:already-available/unclear remain, but an explicit maintainer
@netcatty-bot mention must re-enter issue_classify so re-triage can
run without an open bot PR.
2026-08-05 20:54:29 +08:00
2 changed files with 95 additions and 11 deletions

No files matched your search

+20 -11
View File
@@ -2423,21 +2423,23 @@ function decideIssueCommentRoute({
}
const isManaged = names.some((name) => ISSUE_FOLLOWUP_LABELS.has(name));
if (isAuthor && isManaged) {
return {
kind: 'issue_followup',
reason: 'author follow-up on managed issue',
};
}
const trustedAssociation = ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(
String(commenterAssociation || '').toUpperCase(),
);
// Prefer maintainer @bot over plain author follow-up so maintainers who also
// filed the issue can still force re-triage (incl. auto-closed disputes).
if (isManaged && trustedAssociation && mentionsIssueBot(body, botLogins)) {
return {
kind: 'issue_followup',
reason: 'maintainer mentioned issue bot',
};
}
if (isAuthor && isManaged) {
return {
kind: 'issue_followup',
reason: 'author follow-up on managed issue',
};
}
return { kind: 'skip', reason: 'issue comment no follow-up signal' };
}
@@ -2451,10 +2453,15 @@ function refineIssueCommentRoute(decision, {
if (decision?.kind !== 'issue_followup' || hasOpenBotPull) return decision;
const names = normalizeIssueLabelNames(labels);
// Disputed auto-close outcomes (already_available / unclear) must stay on
// follow-up after reopen handoff. Do not gate on ready-for-human alone —
// that label also covers feature_defer / other / implement failures that
// should still be allowed to re-enter classify.
if (names.some((name) => ISSUE_AUTO_CLOSE_HANDOFF_LABELS.has(name))) {
// follow-up after author reopen handoff so classify cannot auto-close again.
// Maintainer @bot mentions are explicit re-triage requests and must still
// re-enter classify even when those dispute labels remain.
// Do not gate on ready-for-human alone — that label also covers
// feature_defer / other / implement failures that should reclassify.
if (
names.some((name) => ISSUE_AUTO_CLOSE_HANDOFF_LABELS.has(name))
&& decision.reason !== 'maintainer mentioned issue bot'
) {
return decision;
}
const simpleKind = classifySimpleIssueFollowup([{ body }]);
@@ -2463,7 +2470,9 @@ function refineIssueCommentRoute(decision, {
kind: 'issue_classify',
reason: hasOpenRelatedPull
? 'actionable author follow-up with trusted related PR'
: 'actionable author follow-up without open automation PR',
: decision.reason === 'maintainer mentioned issue bot'
? 'actionable maintainer bot mention without open automation PR'
: 'actionable author follow-up without open automation PR',
};
}
+75
View File
@@ -766,6 +766,81 @@ test('decideIssueCommentRoute accepts maintainer @bot and ignores untrusted byst
assert.equal(auto.mentionsIssueBot('补充:@netcatty-bot请再确认'), true);
});
test('maintainer @bot on auto-closed labels can reclassify without open bot PR', () => {
const maintainerDecision = auto.decideIssueCommentRoute({
labels: ['triage:already-available', 'triage:admitted', 'ready-for-human'],
commenterLogin: 'maintainer',
issueAuthorLogin: 'alice',
commenterAssociation: 'MEMBER',
body: '@netcatty-bot 请重新分流,这个能力其实还没有。',
});
assert.deepEqual(maintainerDecision, {
kind: 'issue_followup',
reason: 'maintainer mentioned issue bot',
});
assert.deepEqual(
auto.refineIssueCommentRoute(maintainerDecision, {
hasOpenBotPull: false,
labels: ['triage:already-available', 'triage:admitted', 'ready-for-human'],
body: '@netcatty-bot 请重新分流,这个能力其实还没有。',
}),
{
kind: 'issue_classify',
reason: 'actionable maintainer bot mention without open automation PR',
},
);
// Maintainer who is also the issue author still gets the maintainer reason.
const maintainerAuthor = auto.decideIssueCommentRoute({
labels: ['triage:already-available', 'ready-for-human'],
commenterLogin: 'maintainer',
issueAuthorLogin: 'maintainer',
commenterAssociation: 'OWNER',
body: '@netcatty-bot 请重新分流,这个能力其实还没有。',
});
assert.deepEqual(maintainerAuthor, {
kind: 'issue_followup',
reason: 'maintainer mentioned issue bot',
});
assert.deepEqual(
auto.refineIssueCommentRoute(maintainerAuthor, {
hasOpenBotPull: false,
labels: ['triage:already-available', 'ready-for-human'],
body: '@netcatty-bot 请重新分流,这个能力其实还没有。',
}),
{
kind: 'issue_classify',
reason: 'actionable maintainer bot mention without open automation PR',
},
);
// Author follow-ups on the same labels still stay on follow-up (no re-close loop).
const authorDecision = {
kind: 'issue_followup',
reason: 'author follow-up on managed issue',
};
assert.equal(
auto.refineIssueCommentRoute(authorDecision, {
hasOpenBotPull: false,
labels: ['triage:already-available', 'ready-for-human'],
body: '@netcatty-bot 请重新分流,这个能力其实还没有。',
}),
authorDecision,
);
// Non-maintainer author + @bot still routes as author follow-up (not reclassify).
assert.deepEqual(
auto.decideIssueCommentRoute({
labels: ['triage:already-available', 'ready-for-human'],
commenterLogin: 'alice',
issueAuthorLogin: 'alice',
commenterAssociation: 'NONE',
body: '@netcatty-bot 请重新分流,这个能力其实还没有。',
}),
{
kind: 'issue_followup',
reason: 'author follow-up on managed issue',
},
);
});
test('decideIssueCommentRoute ignores automation actors and unmanaged chatter', () => {
assert.equal(
auto.decideIssueCommentRoute({