Clarify continuous maintenance provenance

This commit is contained in:
Richard Courtman
2026-08-26 11:23:05 +01:00
parent 1943a1e97f
commit 55192bf835
6 changed files with 201 additions and 47 deletions
+99 -27
View File
@@ -9,6 +9,8 @@ const BUG_LABEL = "bug";
const DOCS_LABEL = "documentation";
const ENHANCEMENT_LABEL = "enhancement";
const MAINTAINER_AUTHOR_ASSOCIATIONS = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
const RETEST_COMMENT_GRACE_MS = 5 * 60 * 1000;
const RETEST_COMMENT_LOOKBACK_MS = 24 * 60 * 60 * 1000;
function escapeRegExp(value) {
return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -297,6 +299,45 @@ async function syncLabels({ github, context, core }) {
});
}
async function postRetestCommentForIssue({
github,
context,
core,
issue,
latestVersion,
}) {
const { reportedVersion, isBugLike, comparison } = buildTriageState(
issue,
core,
latestVersion
);
if (!isBugLike) {
core.info("Issue is not bug-like after classification. Skipping public retest guidance.");
return false;
}
if (!reportedVersion) {
core.info("Issue is missing Pulse version metadata. Skipping public retest guidance.");
return false;
}
if (comparison === null || comparison >= 0) {
core.info("Issue is already on the latest stable core or newer. Skipping public retest guidance.");
return false;
}
if (await hasRetestComment(github, context, issue.number)) {
core.info("Retest guidance comment already exists.");
return false;
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: buildRetestCommentBody(reportedVersion, latestVersion),
});
return true;
}
async function postRetestComment({ github, context, core }) {
const issue = context.payload.issue;
const action = context.payload.action || "";
@@ -306,38 +347,67 @@ async function postRetestComment({ github, context, core }) {
}
const latestVersion = await getLatestStableVersion(github, context, core);
const { reportedVersion, isBugLike, comparison } = buildTriageState(
issue,
await postRetestCommentForIssue({
github,
context,
core,
latestVersion
);
if (!isBugLike) {
core.info("Issue is not bug-like after classification. Skipping public retest guidance.");
return;
}
if (!reportedVersion) {
core.info("Issue is missing Pulse version metadata. Skipping public retest guidance.");
return;
}
if (comparison === null || comparison >= 0) {
core.info("Issue is already on the latest stable core or newer. Skipping public retest guidance.");
return;
}
if (await hasRetestComment(github, context, issue.number)) {
core.info("Retest guidance comment already exists.");
return;
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: buildRetestCommentBody(reportedVersion, latestVersion),
issue,
latestVersion,
});
}
async function postEligibleRetestComments({
github,
context,
core,
nowMs = Date.now(),
graceMs = RETEST_COMMENT_GRACE_MS,
lookbackMs = RETEST_COMMENT_LOOKBACK_MS,
}) {
const issues = await github.paginate(github.rest.issues.listForRepo, {
owner: context.repo.owner,
repo: context.repo.repo,
state: "open",
sort: "created",
direction: "desc",
since: new Date(nowMs - lookbackMs).toISOString(),
per_page: 100,
});
const latestVersion = await getLatestStableVersion(github, context, core);
let eligibleCount = 0;
let postedCount = 0;
for (const issue of issues) {
if (issue.pull_request || !canPostRetestComment(issue, "opened")) continue;
const createdMs = new Date(issue.created_at || "").getTime();
if (!Number.isFinite(createdMs)) {
core.warning(`Issue #${issue.number} has an invalid created_at value.`);
continue;
}
const ageMs = nowMs - createdMs;
if (ageMs < graceMs || ageMs > lookbackMs) continue;
eligibleCount += 1;
const posted = await postRetestCommentForIssue({
github,
context,
core,
issue,
latestVersion,
});
if (posted) postedCount += 1;
}
core.info(
`Retest guidance sweep complete: eligible=${eligibleCount}, posted=${postedCount}.`
);
return { eligibleCount, postedCount };
}
module.exports = {
postEligibleRetestComments,
syncLabels,
postRetestComment,
internals: {
@@ -347,6 +417,8 @@ module.exports = {
ENHANCEMENT_LABEL,
NEEDS_VERSION_LABEL,
RETEST_COMMENT_MARKER,
RETEST_COMMENT_GRACE_MS,
RETEST_COMMENT_LOOKBACK_MS,
RETEST_LABEL,
TRIAGE_FOOTER,
VERSION_LABEL_PREFIX,
+68 -3
View File
@@ -7,6 +7,7 @@ function createGithub({
latestVersion = "6.0.1",
existingLabels = new Set(),
existingComments = [],
issues = [],
} = {}) {
const calls = {
createComment: [],
@@ -42,6 +43,7 @@ function createGithub({
calls.createComment.push(payload);
return { data: payload };
},
listForRepo: Symbol("listForRepo"),
listComments: Symbol("listComments"),
},
repos: {
@@ -51,9 +53,9 @@ function createGithub({
},
},
},
async paginate() {
calls.paginate.push(true);
return existingComments;
async paginate(endpoint) {
calls.paginate.push(endpoint);
return endpoint === github.rest.issues.listForRepo ? issues : existingComments;
},
};
@@ -152,6 +154,69 @@ test("postRetestComment comments once for older non-maintainer bug reports", asy
);
});
test("scheduled retest guidance waits five minutes and reads the current issue", async () => {
const nowMs = Date.parse("2026-08-26T09:06:00Z");
const issues = [
{
number: 1780,
title: "Agent token scope",
body: "## Feedback type\nBug / regression\n\n## Pulse version\n6.3.2\n",
labels: [{ name: "bug" }],
author_association: "NONE",
created_at: "2026-08-26T09:01:51Z",
},
];
const { github, calls } = createGithub({ latestVersion: "6.3.2", issues });
const result = await triage.postEligibleRetestComments({
github,
context: createContext({ issue: null }),
core: createCore(),
nowMs,
});
assert.deepEqual(result, { eligibleCount: 0, postedCount: 0 });
assert.equal(calls.createComment.length, 0);
const laterResult = await triage.postEligibleRetestComments({
github,
context: createContext({ issue: null }),
core: createCore(),
nowMs: Date.parse("2026-08-26T09:07:00Z"),
});
assert.deepEqual(laterResult, { eligibleCount: 1, postedCount: 0 });
assert.equal(calls.createComment.length, 0);
});
test("scheduled retest guidance posts once after the grace window", async () => {
const issues = [
{
number: 1200,
title: "Upgrade regression",
body: "## Feedback type\nRegression\n\n## Pulse version\n5.1.9\n",
labels: [{ name: "bug" }],
author_association: "NONE",
created_at: "2026-08-26T08:55:00Z",
},
];
const { github, calls } = createGithub({ latestVersion: "6.3.2", issues });
const result = await triage.postEligibleRetestComments({
github,
context: createContext({ issue: null }),
core: createCore(),
nowMs: Date.parse("2026-08-26T09:01:00Z"),
});
assert.deepEqual(result, { eligibleCount: 1, postedCount: 1 });
assert.equal(calls.createComment.length, 1);
assert.equal(calls.createComment[0].issue_number, 1200);
assert.ok(
calls.createComment[0].body.endsWith(`\n\n${triage.internals.TRIAGE_FOOTER}`)
);
});
test("timeout close comments use the canonical triage footer", () => {
const { buildTimeoutCloseCommentBody, CLOSE_COMMENT_MARKER, TRIAGE_FOOTER } =
triage.internals;
@@ -4,20 +4,21 @@ const fs = require("node:fs");
const path = require("node:path");
const workflowDir = path.resolve(__dirname, "../workflows");
const commentPublisherPatterns = [
const issueMutationPatterns = [
/github\.rest\.issues\.createComment/,
/github\.rest\.pulls\.createReviewComment/,
/addDiscussionComment/,
/postRetestComment/,
/post(?:Eligible)?RetestComments?/,
/syncLabels/,
];
test("automated workflow comments use the dedicated triage identity", () => {
test("automated workflow issue mutations use the dedicated triage identity", () => {
const publishers = [];
for (const name of fs.readdirSync(workflowDir)) {
if (!name.endsWith(".yml") && !name.endsWith(".yaml")) continue;
const workflow = fs.readFileSync(path.join(workflowDir, name), "utf8");
if (!commentPublisherPatterns.some((pattern) => pattern.test(workflow))) continue;
if (!issueMutationPatterns.some((pattern) => pattern.test(workflow))) continue;
publishers.push(name);
assert.match(
@@ -39,6 +40,7 @@ test("automated workflow comments use the dedicated triage identity", () => {
assert.deepEqual(publishers.sort(), [
"close-needs-retest-timeout.yml",
"issue-version-label-sync.yml",
"issue-version-retest-comment.yml",
]);
});