From d972257ce110aaddd99e58e2c6fa0c993418e5cf Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 23:49:45 +0100 Subject: [PATCH] Read declared issue versions from visible release-link text A version supplied as a Markdown release link currently becomes needs-version-info even though its visible label states the version. Read only that label and ordinary inline formatting; never mine destinations, image text or literal Markdown for a version. Keep current-report checks, silent metadata deltas and Community labels unchanged. Contract-Neutral: Recognise visible inline version values within the existing issue metadata contract; no product runtime, API or label ownership changes. Change-source: pulse-maintainer --- .github/scripts/issue-version-triage.cjs | 20 ++++++- .github/scripts/issue-version-triage.test.cjs | 57 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/.github/scripts/issue-version-triage.cjs b/.github/scripts/issue-version-triage.cjs index 82f032dac7..4af8321c9d 100644 --- a/.github/scripts/issue-version-triage.cjs +++ b/.github/scripts/issue-version-triage.cjs @@ -139,8 +139,26 @@ function normalizeVersion(value) { return match ? match[1] : null; } +function inlineVersionText(tokens) { + return (tokens || []).map((token) => { + // A release link's label is evidence; its destination and title are not. + // Unwrap only ordinary inline formatting, not images, struck-out values + // or HTML. Code spans keep their literal text rather than parsing links + // or comments inside them. + if (["link", "strong", "em"].includes(token.type)) { + return inlineVersionText(token.tokens); + } + if (["codespan", "escape"].includes(token.type)) return token.text; + return token.raw || ""; + }).join(""); +} + function normalizeLegacyVersionValue(value) { - const visible = String(value).trim().replace(/^[`*_]+/, ""); + const raw = String(value).trim(); + const blocks = lexer(raw, { gfm: true }); + const text = blocks.length === 1 && blocks[0].type === "paragraph" + ? inlineVersionText(blocks[0].tokens) : raw; + const visible = text.trim().replace(/^[`*_]+/, ""); // Failed-install forms explicitly accept an attempted server archive. Its // packaging suffix is not part of the release version. Recognise only the // server archive names; companion binaries and signatures cannot establish diff --git a/.github/scripts/issue-version-triage.test.cjs b/.github/scripts/issue-version-triage.test.cjs index dfd5fef372..5c6db3ed5c 100644 --- a/.github/scripts/issue-version-triage.test.cjs +++ b/.github/scripts/issue-version-triage.test.cjs @@ -1774,3 +1774,60 @@ for (const [filename, version] of [ assert.equal(calls.getIssue.length, 2); }); } + +test("declared versions use visible release-link text, including nested inline formatting", () => { + const { extractPulseVersion } = triage.internals; + for (const [value, expected] of [ + ["[v6.5.0](https://github.com/rcourtman/Pulse/releases/tag/v6.4.5)", "6.5.0"], + ["**[6.5.0](https://github.com/rcourtman/Pulse/releases/tag/v6.5.0)**", "6.5.0"], + ["_[`v6.5.0`](https://github.com/rcourtman/Pulse/releases/tag/v6.5.0)_", "6.5.0"], + ["[6\\.5\\.0](https://github.com/rcourtman/Pulse/releases/tag/v6.5.0)", "6.5.0"], + ["[pulse-v6.6.0-beta.3-linux-amd64.tar.gz](https://github.com/rcourtman/Pulse/releases/tag/v6.6.0-beta.3)", "6.6.0-beta.3"], + ]) { + for (const body of [ + `### Pulse version\n${value}\n### Agent version\n6.4.5`, + `Pulse version: ${value}`, + `- **Pulse version**: ${value}`, + ]) { + for (const newline of ["\n", "\r\n"]) { + assert.equal(extractPulseVersion("Upgrade from v6.4.5", body.replaceAll("\n", newline)), expected, body); + } + } + } +}); + +test("link targets, titles, images and literal Markdown cannot invent a declared version", () => { + const { extractPulseVersion } = triage.internals; + for (const value of [ + "[unknown](https://github.com/rcourtman/Pulse/releases/tag/v6.5.0)", + '[unknown](https://github.com/rcourtman/Pulse "6.5.0")', + "![6.5.0](https://github.com/rcourtman/Pulse/releases/tag/v6.5.0)", + "~~[6.5.0](https://github.com/rcourtman/Pulse/releases/tag/v6.5.0)~~", + "`[6.5.0](https://github.com/rcourtman/Pulse/releases/tag/v6.5.0)`", + "6.5.0", + "[release](https://github.com/rcourtman/Pulse/releases/tag/v6.5.0) 6.4.5", + "[6.5.0](unterminated", + ]) { + assert.equal(extractPulseVersion("Upgrade from v6.4.5", + `### Pulse version\n${value}\n### Agent version\n6.5.0`), null, value); + } +}); + +test("the label consumer accepts a linked version without fetching links or contacting the reporter", async () => { + for (const [visible, expected] of [["**`v6.5.0`**", "6.5.0"], ["unknown", null]]) { + const { github, calls } = createGithub(); + const issue = { number: 2922, title: "[Bug]: Upgrade from v6.4.5", + body: `### Pulse version\n[${visible}](https://github.com/rcourtman/Pulse/releases/tag/v6.6.0-rc.5)\n### Agent version\n6.4.5`, + labels: ["bug", "affects-6.4.5", "needs-version-info", "needs-human", "release-blocker", + "needs-decomposition", "needs-retest-on-latest"].map(name => ({ name })) }; + await syncLabels({ github, context: createContext({ issue }), core: createCore() }); + assert.deepEqual(calls.addLabels.map(call => call.labels), expected ? [["affects-6.5.0"]] : []); + assert.deepEqual(calls.removeLabel.map(call => call.name), expected + ? ["affects-6.4.5", "needs-version-info"] : ["affects-6.4.5"]); + assert.deepEqual(calls.createLabel.map(call => call.name), expected ? ["affects-6.5.0"] : ["needs-version-info"]); + assert.deepEqual(calls.createComment, []); + assert.deepEqual(calls.getLatestRelease, []); + assert.deepEqual(calls.paginate, []); + assert.equal(calls.getIssue.length, 2); + } +});