mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Clarify continuous maintenance provenance
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user