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",
]);
});
@@ -9,13 +9,19 @@ on:
permissions:
contents: read
issues: write
jobs:
sync:
if: ${{ github.event.issue.pull_request == null }}
runs-on: ubuntu-24.04
steps:
- name: Mint triage bot token
id: triage-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: 2484142
private-key: ${{ secrets.PULSE_TRIAGE_APP_PRIVATE_KEY }}
- name: Check out triage helper
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
@@ -26,6 +32,7 @@ jobs:
- name: Sync issue version metadata
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
github-token: ${{ steps.triage-token.outputs.token }}
script: |
const triage = require(`${process.env.GITHUB_WORKSPACE}/.github/scripts/issue-version-triage.cjs`);
await triage.syncLabels({ github, context, core });
@@ -1,16 +1,19 @@
name: Issue Version Retest Comment
on:
issues:
types:
- opened
schedule:
- cron: "*/5 * * * *"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: issue-version-retest-comment
cancel-in-progress: false
jobs:
comment:
if: ${{ github.event.issue.pull_request == null }}
runs-on: ubuntu-24.04
steps:
- name: Mint triage bot token
@@ -33,4 +36,4 @@ jobs:
github-token: ${{ steps.triage-token.outputs.token }}
script: |
const triage = require(`${process.env.GITHUB_WORKSPACE}/.github/scripts/issue-version-triage.cjs`);
await triage.postRetestComment({ github, context, core });
await triage.postEligibleRetestComments({ github, context, core });
+12 -7
View File
@@ -8,9 +8,10 @@ or issue thread.
## The short version
Automation contributes to code, tests, documentation, release notes, issue
triage, and routine repository maintenance. A continuously running automated
maintainer may investigate reports, implement changes, run verification, and
land routine work within boundaries I define.
triage, and routine repository maintenance. Routine changes may be
investigated, implemented, tested, and merged to `main` without line-by-line
human review. The continuously running maintainer does this within boundaries
I define.
I set the product direction, control releases, and remain responsible for
everything that ships. I do not claim to have personally written every line.
@@ -28,7 +29,11 @@ released builds still go through Pulse's release qualification process.
and the evidence used to qualify a release.
- **Issue triage and support.** Automated issue and discussion replies post
under the dedicated `pulse-triage` bot identity and link back to this page.
Automated support replies are sent as Pulse Triage and link here as well.
Automated issue state changes use that identity as well. Automated support
replies are sent as Pulse Triage and link here as well.
- **Change provenance.** Commits made by the continuously running maintainer
carry a dedicated bot author and committer identity. Issue-driven changes
link back to the originating report where applicable.
The link on an automated reply is intentionally understated. It makes the
process discoverable without turning every technical exchange into a banner
@@ -42,9 +47,9 @@ does not have unrestricted authority. Product direction, architecture,
acceptable risk, capability boundaries, and release decisions remain mine.
Release publication and other high-impact actions require explicit approval.
My role is to direct the project, design and maintain those boundaries, review
outcomes, and answer for the result. If an automated change is wrong, it is
still my bug and my responsibility to correct it.
My role is to direct the project, design and maintain those boundaries,
monitor outcomes, and answer for the result. If an automated change is wrong,
it is still my bug and my responsibility to correct it.
## How the work should be judged