From b85e8fc5bb9bcc9818069722b00aa78d5dbc4a9a Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 04:24:18 +0000 Subject: [PATCH 1/2] Keep concurrent reports from losing issue metadata Reconcile only GitHub's explicit label-name creation conflict with one exact readback. Parallel reports can then retain version and decomposition metadata without replaying a write or hiding access and validation failures. Preserve the final live-report comparison before any issue mutation. Contract-Neutral: Existing issue classification, label ownership and silent-mutation boundaries are unchanged; reconcile a concurrent creation of the same repository label. Change-source: pulse-maintainer --- .github/scripts/issue-version-triage.cjs | 36 ++-- .github/scripts/issue-version-triage.test.cjs | 167 ++++++++++++++++++ docs/ISSUE_TRIAGE.md | 6 + 3 files changed, 197 insertions(+), 12 deletions(-) diff --git a/.github/scripts/issue-version-triage.cjs b/.github/scripts/issue-version-triage.cjs index 8e0c6a699b..26a7436569 100644 --- a/.github/scripts/issue-version-triage.cjs +++ b/.github/scripts/issue-version-triage.cjs @@ -365,21 +365,33 @@ function compareCore(a, b) { } async function ensureLabel(github, context, name, color, description) { + const label = { + owner: context.repo.owner, + repo: context.repo.repo, + name, + }; try { - await github.rest.issues.getLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name, - }); + await github.rest.issues.getLabel(label); } catch (error) { if (error.status !== 404) throw error; - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name, - color, - description, - }); + try { + await github.rest.issues.createLabel({ ...label, color, description }); + } catch (creationError) { + // Concurrent reports can both observe a missing repository-wide label. + // Reconcile only GitHub's explicit name conflict, without replaying the + // write or treating other validation/access failures as success. + const errors = creationError.response?.data?.errors; + if (creationError.status !== 422 || !Array.isArray(errors) || + !errors.some((item) => item?.resource === "Label" && + item.field === "name" && item.code === "already_exists")) { + throw creationError; + } + const { data: current } = await github.rest.issues.getLabel(label); + if (typeof current?.name !== "string" || + current.name.toLowerCase() !== name.toLowerCase()) { + throw creationError; + } + } } } diff --git a/.github/scripts/issue-version-triage.test.cjs b/.github/scripts/issue-version-triage.test.cjs index f43d54c96c..9ddbe08769 100644 --- a/.github/scripts/issue-version-triage.test.cjs +++ b/.github/scripts/issue-version-triage.test.cjs @@ -2667,3 +2667,170 @@ test("literal reference-definition examples remain visible evidence for topic re ), true, example); } }); + +function labelNameConflict() { + return Object.assign(new Error("Validation Failed"), { + status: 422, + response: { data: { errors: [{ resource: "Label", field: "name", code: "already_exists" }] } }, + }); +} + +for (const [label, classification, body] of [ + ["affects-6.7.0", "bug", "### Pulse version\n6.7.0\n"], + ["needs-version-info", "bug", "### Pulse version\nunknown\n"], + ["needs-decomposition", "enhancement", "### Additional actionable topics\nAlso retain failed attempts.\n"], +]) { + test(`label-creation race preserves both reports' metadata: ${label}`, async () => { + const existingLabels = new Set(); + let missingReads = 0; + let releaseReads; + const bothMissing = new Promise(resolve => { releaseReads = resolve; }); + const preserved = [classification, "needs-human", "release-blocker", + "needs-retest-on-latest", "operator-reviewed"]; + const workers = [2933, 2934].map(number => { + const { github, calls } = createGithub({ existingLabels }); + const getLabel = github.rest.issues.getLabel; + const createLabel = github.rest.issues.createLabel; + github.rest.issues.getLabel = async payload => { + if (!existingLabels.has(payload.name)) { + // Both jobs must have observed absence before either can create it. + calls.getLabel.push(payload.name); + missingReads += 1; + if (missingReads === 2) releaseReads(); + await bothMissing; + throw Object.assign(new Error("Not Found"), { status: 404 }); + } + return getLabel(payload); + }; + github.rest.issues.createLabel = async payload => { + if (existingLabels.has(payload.name)) { + calls.createLabel.push(payload); + throw labelNameConflict(); + } + return createLabel(payload); + }; + const issue = { number, title: "Monitoring report", body, + labels: preserved.map(name => ({ name })) }; + return { github, calls, issue }; + }); + const results = await Promise.allSettled(workers.map(({ github, issue }) => + syncLabels({ github, context: createContext({ issue }), core: createCore() }))); + assert.deepEqual(results.map(result => result.status), ["fulfilled", "fulfilled"]); + assert.equal(missingReads, 2); + assert.deepEqual([...existingLabels], [label]); + assert.equal(workers.reduce((sum, worker) => sum + worker.calls.getLabel.length, 0), 3); + for (const { calls } of workers) { + assert.deepEqual(calls.addLabels.map(call => call.labels), [[label]]); + assert.equal(calls.createLabel.length, 1, "never replay label creation"); + assert.deepEqual(calls.removeLabel, []); + assert.deepEqual(calls.createComment, []); + assert.deepEqual(calls.getLatestRelease, []); + assert.deepEqual(calls.paginate, []); + assert.equal(calls.getIssue.length, 2, "recheck report before applying metadata"); + } + }); +} + +test("label-creation reconciliation requires the exact current label", async () => { + for (const current of [{ name: "affects-6.7.0" }, { name: "AFFECTS-6.7.0" }, + { name: "affects-6.6.0" }, {}, null]) { + const { github, calls } = createGithub(); + const conflict = labelNameConflict(); + github.rest.issues.getLabel = async ({ name }) => { + calls.getLabel.push(name); + if (calls.getLabel.length === 1) throw Object.assign(new Error("Not Found"), { status: 404 }); + return { data: current }; + }; + github.rest.issues.createLabel = async payload => { + calls.createLabel.push(payload); + throw conflict; + }; + const issue = { number: 2935, title: "Monitoring report", body: "### Pulse version\n6.7.0\n", + labels: [{ name: "bug" }] }; + const result = syncLabels({ github, context: createContext({ issue }), core: createCore() }); + const matches = current?.name?.toLowerCase() === "affects-6.7.0"; + if (matches) await result; + else await assert.rejects(result, error => error === conflict); + assert.equal(calls.getLabel.length, 2); + assert.equal(calls.createLabel.length, 1); + assert.deepEqual(calls.addLabels.map(call => call.labels), matches ? [["affects-6.7.0"]] : []); + assert.deepEqual(calls.removeLabel, []); + assert.deepEqual(calls.createComment, []); + } +}); + +test("label-creation non-conflict failures are not retried or hidden", async () => { + for (const [status, errors] of [ + [401], [403], [429], [500], [422], [422, "unavailable"], + [422, [{ resource: "Label", field: "name", code: "invalid" }]], + [422, [{ resource: "Issue", field: "name", code: "already_exists" }]], + [422, [{ resource: "Label", field: "description", code: "already_exists" }]], + ]) { + const { github, calls } = createGithub(); + const failure = Object.assign(new Error("Creation failed"), { + status, response: { data: { errors } }, + }); + github.rest.issues.createLabel = async payload => { + calls.createLabel.push(payload); + throw failure; + }; + const issue = { number: 2936, title: "Monitoring report", body: "### Pulse version\n6.7.0\n", + labels: [{ name: "bug" }] }; + await assert.rejects(syncLabels({ github, context: createContext({ issue }), core: createCore() }), + error => error === failure); + assert.equal(calls.getLabel.length, 1); + assert.equal(calls.createLabel.length, 1); + assert.deepEqual(calls.addLabels, []); + assert.deepEqual(calls.removeLabel, []); + assert.deepEqual(calls.createComment, []); + } +}); + +test("label-creation reconciliation read failures stop report mutations", async () => { + for (const status of [403, 404, 503]) { + const { github, calls } = createGithub(); + const failure = Object.assign(new Error("Read failed"), { status }); + github.rest.issues.getLabel = async ({ name }) => { + calls.getLabel.push(name); + if (calls.getLabel.length === 1) throw Object.assign(new Error("Not Found"), { status: 404 }); + throw failure; + }; + github.rest.issues.createLabel = async payload => { + calls.createLabel.push(payload); + throw labelNameConflict(); + }; + const issue = { number: 2937, title: "Monitoring report", body: "### Pulse version\n6.7.0\n", + labels: [{ name: "bug" }] }; + await assert.rejects(syncLabels({ github, context: createContext({ issue }), core: createCore() }), + error => error === failure); + assert.equal(calls.getLabel.length, 2); + assert.equal(calls.createLabel.length, 1); + assert.deepEqual(calls.addLabels, []); + assert.deepEqual(calls.removeLabel, []); + assert.deepEqual(calls.createComment, []); + } +}); + +test("label-creation reconciliation still respects a newer Community decision", async () => { + const issue = { number: 2938, title: "Monitoring report", body: "### Pulse version\n6.7.0\n", + state: "open", updated_at: "2026-10-08T10:00:00Z", labels: [{ name: "bug" }] }; + const { github, calls } = createGithub({ currentIssue: issue }); + github.rest.issues.getLabel = async ({ name }) => { + calls.getLabel.push(name); + if (calls.getLabel.length === 1) throw Object.assign(new Error("Not Found"), { status: 404 }); + github.currentIssue = { ...issue, updated_at: "2026-10-08T11:00:00Z", + labels: [...issue.labels, { name: "needs-human" }] }; + return { data: { name } }; + }; + github.rest.issues.createLabel = async payload => { + calls.createLabel.push(payload); + throw labelNameConflict(); + }; + await syncLabels({ github, context: createContext({ issue }), core: createCore() }); + assert.equal(calls.getLabel.length, 2); + assert.equal(calls.createLabel.length, 1); + assert.equal(calls.getIssue.length, 2); + assert.deepEqual(calls.addLabels, []); + assert.deepEqual(calls.removeLabel, []); + assert.deepEqual(calls.createComment, []); +}); diff --git a/docs/ISSUE_TRIAGE.md b/docs/ISSUE_TRIAGE.md index 2a17c2825c..3d5ffa5c2d 100644 --- a/docs/ISSUE_TRIAGE.md +++ b/docs/ISSUE_TRIAGE.md @@ -262,6 +262,12 @@ Version classification does not need the latest release and cannot establish relevant-fix availability. The synchronizer makes no release lookup, public comment, retest request or closure. +Labels are shared across reports. If concurrent reports both try to create the +same missing label, the synchronizer reconciles only an explicit already-exists +name conflict with one read of that label. It does not repeat the creation, +ignore access or other validation failures, or apply a prepared label decision +after the report or its labels have changed. + An explicit **Pulse version** field is authoritative even in a legacy inline or standalone report. An unknown or incomplete value must not be replaced by an upgrade's starting version in the title, a nearby agent/platform version or From d6bbbc64f3a3c28eba8fd735ef88b2fac9d7ef14 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 04:41:03 +0000 Subject: [PATCH 2/2] Keep label-race recovery narrow and published guidance unchanged Require every returned validation detail to be the label-name conflict, so a mixed validation failure cannot be hidden by readback. Keep unchanged intake guidance aligned with its shipped mirror; this internal metadata correction changes no reporting instructions or rendered surface. Contract-Neutral: Existing classification and documentation contracts remain unchanged; tighten the same label-creation conflict recovery and preserve the published guide bytes. Change-source: pulse-maintainer --- .github/scripts/issue-version-triage.cjs | 4 ++-- .github/scripts/issue-version-triage.test.cjs | 4 +++- docs/ISSUE_TRIAGE.md | 6 ------ 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/scripts/issue-version-triage.cjs b/.github/scripts/issue-version-triage.cjs index 26a7436569..e65fca9b00 100644 --- a/.github/scripts/issue-version-triage.cjs +++ b/.github/scripts/issue-version-triage.cjs @@ -381,8 +381,8 @@ async function ensureLabel(github, context, name, color, description) { // Reconcile only GitHub's explicit name conflict, without replaying the // write or treating other validation/access failures as success. const errors = creationError.response?.data?.errors; - if (creationError.status !== 422 || !Array.isArray(errors) || - !errors.some((item) => item?.resource === "Label" && + if (creationError.status !== 422 || !Array.isArray(errors) || !errors.length || + !errors.every((item) => item?.resource === "Label" && item.field === "name" && item.code === "already_exists")) { throw creationError; } diff --git a/.github/scripts/issue-version-triage.test.cjs b/.github/scripts/issue-version-triage.test.cjs index 9ddbe08769..1ef8768840 100644 --- a/.github/scripts/issue-version-triage.test.cjs +++ b/.github/scripts/issue-version-triage.test.cjs @@ -2761,10 +2761,12 @@ test("label-creation reconciliation requires the exact current label", async () test("label-creation non-conflict failures are not retried or hidden", async () => { for (const [status, errors] of [ - [401], [403], [429], [500], [422], [422, "unavailable"], + [401], [403], [429], [500], [422], [422, "unavailable"], [422, []], [422, [{ resource: "Label", field: "name", code: "invalid" }]], [422, [{ resource: "Issue", field: "name", code: "already_exists" }]], [422, [{ resource: "Label", field: "description", code: "already_exists" }]], + [422, [{ resource: "Label", field: "name", code: "already_exists" }, + { resource: "Label", field: "color", code: "invalid" }]], ]) { const { github, calls } = createGithub(); const failure = Object.assign(new Error("Creation failed"), { diff --git a/docs/ISSUE_TRIAGE.md b/docs/ISSUE_TRIAGE.md index 3d5ffa5c2d..2a17c2825c 100644 --- a/docs/ISSUE_TRIAGE.md +++ b/docs/ISSUE_TRIAGE.md @@ -262,12 +262,6 @@ Version classification does not need the latest release and cannot establish relevant-fix availability. The synchronizer makes no release lookup, public comment, retest request or closure. -Labels are shared across reports. If concurrent reports both try to create the -same missing label, the synchronizer reconciles only an explicit already-exists -name conflict with one read of that label. It does not repeat the creation, -ignore access or other validation failures, or apply a prepared label decision -after the report or its labels have changed. - An explicit **Pulse version** field is authoritative even in a legacy inline or standalone report. An unknown or incomplete value must not be replaced by an upgrade's starting version in the title, a nearby agent/platform version or