mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Merge candidate 20260908T105011Z-delivery-trust
Change-source: pulse-maintainer
This commit is contained in:
@@ -232,6 +232,28 @@ function keepOnlyReportedVersionLabel(nextLabels, reportedVersion) {
|
||||
}
|
||||
}
|
||||
|
||||
// Apply only this event's classification delta. Replacing the complete label
|
||||
// set would overwrite Community changes made after the event was queued.
|
||||
async function applyLabelDelta(github, context, issue, before, after) {
|
||||
const target = {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
};
|
||||
const additions = [...after].filter((name) => !before.has(name)).sort();
|
||||
if (additions.length) {
|
||||
await github.rest.issues.addLabels({ ...target, labels: additions });
|
||||
}
|
||||
for (const name of [...before].filter((name) => !after.has(name)).sort()) {
|
||||
try {
|
||||
await github.rest.issues.removeLabel({ ...target, name });
|
||||
} catch (error) {
|
||||
// Another synchronizer may already have removed this label.
|
||||
if (error.status !== 404) throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function syncLabels({ github, context, core }) {
|
||||
const issue = context.payload.issue;
|
||||
const latestVersion = await getLatestStableVersion(github, context, core);
|
||||
@@ -255,17 +277,7 @@ async function syncLabels({ github, context, core }) {
|
||||
|
||||
if (!isBugLike) {
|
||||
core.info("Issue is not bug-like after classification. Skipping version triage.");
|
||||
const labelsChanged =
|
||||
labelNames.size !== nextLabels.size ||
|
||||
[...labelNames].some((label) => !nextLabels.has(label));
|
||||
if (labelsChanged) {
|
||||
await github.rest.issues.setLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: [...nextLabels].sort(),
|
||||
});
|
||||
}
|
||||
await applyLabelDelta(github, context, issue, labelNames, nextLabels);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -293,12 +305,7 @@ async function syncLabels({ github, context, core }) {
|
||||
|
||||
// Retest labels are community-owned: version metadata cannot establish
|
||||
// relevant-fix availability or reconcile the live reporter conversation.
|
||||
await github.rest.issues.setLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: [...nextLabels].sort(),
|
||||
});
|
||||
await applyLabelDelta(github, context, issue, labelNames, nextLabels);
|
||||
}
|
||||
|
||||
// Compatibility entry points for callers using the old helper API. Version
|
||||
|
||||
@@ -17,7 +17,8 @@ function createGithub({
|
||||
getLabel: [],
|
||||
getLatestRelease: [],
|
||||
paginate: [],
|
||||
setLabels: [],
|
||||
addLabels: [],
|
||||
removeLabel: [],
|
||||
};
|
||||
|
||||
const github = {
|
||||
@@ -37,8 +38,12 @@ function createGithub({
|
||||
existingLabels.add(payload.name);
|
||||
return { data: payload };
|
||||
},
|
||||
async setLabels(payload) {
|
||||
calls.setLabels.push(payload);
|
||||
async addLabels(payload) {
|
||||
calls.addLabels.push(payload);
|
||||
return { data: payload };
|
||||
},
|
||||
async removeLabel(payload) {
|
||||
calls.removeLabel.push(payload);
|
||||
return { data: payload };
|
||||
},
|
||||
async createComment(payload) {
|
||||
@@ -99,8 +104,8 @@ test("syncLabels classifies older bug reports without requesting a retest", asyn
|
||||
core: createCore(),
|
||||
});
|
||||
|
||||
assert.equal(calls.setLabels.length, 1);
|
||||
assert.deepEqual(calls.setLabels[0].labels, [
|
||||
assert.equal(calls.addLabels.length, 1);
|
||||
assert.deepEqual(calls.addLabels[0].labels, [
|
||||
"affects-6.0.0-rc.1",
|
||||
"bug",
|
||||
]);
|
||||
@@ -121,8 +126,8 @@ test("syncLabels only adds documentation classification for non-bug v6 feedback"
|
||||
core: createCore(),
|
||||
});
|
||||
|
||||
assert.equal(calls.setLabels.length, 1);
|
||||
assert.deepEqual(calls.setLabels[0].labels, ["documentation"]);
|
||||
assert.equal(calls.addLabels.length, 1);
|
||||
assert.deepEqual(calls.addLabels[0].labels, ["documentation"]);
|
||||
});
|
||||
|
||||
test("syncLabels marks declared secondary topics for decomposition", async () => {
|
||||
@@ -149,8 +154,7 @@ test("syncLabels marks declared secondary topics for decomposition", async () =>
|
||||
assert.deepEqual(calls.createLabel.map((call) => call.name), [
|
||||
"needs-decomposition",
|
||||
]);
|
||||
assert.deepEqual(calls.setLabels[0].labels, [
|
||||
"enhancement",
|
||||
assert.deepEqual(calls.addLabels[0].labels, [
|
||||
"needs-decomposition",
|
||||
]);
|
||||
});
|
||||
@@ -171,7 +175,8 @@ test("syncLabels clears decomposition after every declared topic has a dispositi
|
||||
});
|
||||
|
||||
assert.equal(calls.createLabel.length, 0);
|
||||
assert.deepEqual(calls.setLabels[0].labels, ["enhancement"]);
|
||||
assert.equal(calls.addLabels.length, 0);
|
||||
assert.deepEqual(calls.removeLabel.map(call => call.name), ["needs-decomposition"]);
|
||||
});
|
||||
|
||||
test("additional topic classification is explicit and fail-quiet for legacy forms", () => {
|
||||
@@ -259,7 +264,8 @@ test("label sync preserves community-owned retest state regardless of version",
|
||||
labels: [{ name: "bug" }, { name: "needs-retest-on-latest" }],
|
||||
} }),
|
||||
});
|
||||
assert.ok(calls.setLabels[0].labels.includes("needs-retest-on-latest"));
|
||||
assert.ok(!calls.addLabels[0].labels.includes("needs-retest-on-latest"));
|
||||
assert.ok(!calls.removeLabel.some(call => call.name === "needs-retest-on-latest"));
|
||||
assert.equal(calls.createComment.length, 0);
|
||||
}
|
||||
});
|
||||
@@ -352,9 +358,47 @@ test("ambiguous upgrade version requests information without a retest comment",
|
||||
const args = { github, context: createContext({ issue }), core: createCore() };
|
||||
await triage.syncLabels(args);
|
||||
await triage.postRetestComment(args);
|
||||
const labels = calls.setLabels.at(-1).labels;
|
||||
const labels = calls.addLabels.at(-1).labels;
|
||||
assert.ok(labels.includes("needs-version-info"));
|
||||
assert.ok(!labels.includes("affects-5.1.35"));
|
||||
assert.ok(!labels.includes("needs-retest-on-latest"));
|
||||
assert.equal(calls.createComment.length, 0);
|
||||
});
|
||||
|
||||
test("queued label sync cannot overwrite newer Community label decisions", async () => {
|
||||
for (const communityAdded of [true, false]) {
|
||||
const { github } = createGithub();
|
||||
const live = new Set(["bug", "affects-6.0.0", "needs-version-info"]);
|
||||
if (communityAdded) live.add("needs-retest-on-latest");
|
||||
live.add("operator-reviewed");
|
||||
github.rest.issues.setLabels = async ({ labels }) => {
|
||||
live.clear();
|
||||
labels.forEach(label => live.add(label));
|
||||
};
|
||||
github.rest.issues.addLabels = async ({ labels }) => labels.forEach(label => live.add(label));
|
||||
github.rest.issues.removeLabel = async ({ name }) => live.delete(name);
|
||||
await triage.syncLabels({ github, core: createCore(), context: createContext({ issue: {
|
||||
number: 1200, title: "Bug", body: "## Pulse version\n6.0.1\n",
|
||||
labels: ["bug", "affects-6.0.0", "needs-version-info",
|
||||
...(!communityAdded ? ["needs-retest-on-latest"] : [])].map(name => ({ name })),
|
||||
} }) });
|
||||
assert.equal(live.has("needs-retest-on-latest"), communityAdded);
|
||||
assert.ok(live.has("operator-reviewed"));
|
||||
assert.ok(live.has("affects-6.0.1"));
|
||||
assert.ok(!live.has("affects-6.0.0"));
|
||||
assert.ok(!live.has("needs-version-info"));
|
||||
}
|
||||
});
|
||||
|
||||
test("classification removal tolerates an absent label but propagates access failures", async () => {
|
||||
for (const status of [404, 403]) {
|
||||
const { github } = createGithub();
|
||||
github.rest.issues.removeLabel = async () => { throw Object.assign(new Error("API failure"), { status }); };
|
||||
const run = triage.syncLabels({ github, core: createCore(), context: createContext({ issue: {
|
||||
number: 1200, title: "Feedback", body: "## Additional actionable topics\nNone.\n",
|
||||
labels: [{ name: "enhancement" }, { name: "needs-decomposition" }],
|
||||
} }) });
|
||||
if (status === 404) await assert.doesNotReject(run);
|
||||
else await assert.rejects(run, { status: 403 });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -226,7 +226,7 @@ verify downloaded files offline against the candidate-builder identity.
|
||||
|
||||
- The silent metadata path runs on `opened`, `edited`, and `reopened` issue events. The shared helper at `.github/scripts/issue-version-triage.cjs` maintains version labels, `needs-version-info`, feedback classification and `needs-decomposition`.
|
||||
- Version-only retest posting and marked-request timeout closure workflows are retired. An older reported version or silence is not evidence of resolution.
|
||||
- Community owns public follow-up and closure after whole-thread review, relevant-fix verification and confirmed released-source inclusion. The synchronizer neither adds nor removes the community-owned `needs-retest-on-latest` label. Existing labels and comments are context, not proof that a reporter has or has not replied.
|
||||
- Community owns public follow-up and closure after whole-thread review, relevant-fix verification and confirmed released-source inclusion. The synchronizer neither adds nor removes the community-owned `needs-retest-on-latest` label. Classification uses individual label additions/removals rather than replacing the event snapshot, preserving concurrent Community label decisions. Existing labels and comments are context, not proof that a reporter has or has not replied.
|
||||
- `needs-decomposition` is driven only by the structured **Additional actionable topics** form field. The [triage contract](../../docs/ISSUE_TRIAGE.md) requires human or agent judgment to create linked dispositions; the workflow does not infer or auto-create issues from free text.
|
||||
|
||||
## Update Demo Server
|
||||
|
||||
Reference in New Issue
Block a user