diff --git a/.github/scripts/issue-version-triage.cjs b/.github/scripts/issue-version-triage.cjs index 96a0e04baa..82f032dac7 100644 --- a/.github/scripts/issue-version-triage.cjs +++ b/.github/scripts/issue-version-triage.cjs @@ -141,6 +141,19 @@ function normalizeVersion(value) { function normalizeLegacyVersionValue(value) { const visible = String(value).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 + // the attempted server executable's version. + const filename = visible.match(/^pulse-[^\s`*_]+/i)?.[0]; + if (filename && /\.(?:tar\.gz|tgz|zip|exe|sha256|sig|sshsig)(?:\.|$)/i.test(filename)) { + const archive = filename.match( + /^pulse-v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\.(?:tar\.gz|tgz)$/i + ); + return archive ? normalizeVersion(archive[1].replace( + /-linux-(?:386|amd64|arm64|armv6|armv7)$/i, "" + )) : null; + } // Keep an explicitly shared server/agent version (as in #1788), without // mining a sentence such as "unknown; agent version 6.5.0" for a number. return /^(?:v?\d+\.\d+\.\d+\b|(?:pulse|server(?:[ \t]*\+[ \t]*pulse-agent)?)[ \t]+v?\d+\.\d+\.\d+\b|(?:[a-z0-9._/-]+\/)?pulse:|pulse[-_])/i.test(visible) diff --git a/.github/scripts/issue-version-triage.test.cjs b/.github/scripts/issue-version-triage.test.cjs index 9e79cd0eec..dfd5fef372 100644 --- a/.github/scripts/issue-version-triage.test.cjs +++ b/.github/scripts/issue-version-triage.test.cjs @@ -1701,3 +1701,76 @@ test("already unknown-version metadata is a no-op without an extra live read", a assert.equal(calls.removeLabel.length, 0); assert.equal(calls.getIssue.length, 1); }); + +test("attempted server archives separate release versions from packaging", () => { + const { extractPulseVersion } = triage.internals; + for (const version of ["6.5.0", "6.5.0-rc.1", "6.6.0-beta.3", "6.6.0-dev.branch-2"]) { + for (const filename of [ + ...["386", "amd64", "arm64", "armv6", "armv7"].map(arch => + `pulse-v${version}-linux-${arch}.tar.gz`), + `pulse-v${version}.tar.gz`, + `pulse-${version}.tgz`, + ]) { + for (const body of [ + `### Pulse version\n${filename}\n### Agent version\nnone`, + `Pulse version: **\`${filename}\`**`, + ]) { + assert.equal(extractPulseVersion("Failed install", body), version, filename); + } + } + } +}); + +test("non-server artifacts cannot supply an attempted server version", () => { + const { extractPulseVersion } = triage.internals; + for (const filename of [ + "pulse-agent-v6.6.0-beta.3-linux-amd64.tar.gz", + "pulse-agent-helper-v6.6.0-beta.3-linux-amd64.tar.gz", + "pulse-agent-runner-v6.6.0-beta.3-linux-amd64.tar.gz", + "pulse-mcp-v6.6.0-beta.3-windows-amd64.zip", + "pulse-provider-msp-v6.6.0-beta.3.tar.gz", + "pulse-v6.6.0-beta.3-linux-amd64.tar.gz.sha256", + "pulse-v6.6.0-beta.3-linux-amd64.tar.gz.sig", + "pulse-v6.6.0-beta.3-linux-amd64.tar.gz.sshsig", + "pulse-v6.6.0-beta.3-release.sbom.spdx.json.sha256", + "install.sh", + ]) { + assert.equal(extractPulseVersion("Upgrade from v6.4.5", + `### Pulse version\n${filename}\n### Agent version\n6.5.0`), null, filename); + } +}); + +test("archive recognition neither borrows a later filename nor shortens bare prereleases", () => { + const { extractPulseVersion } = triage.internals; + assert.equal(extractPulseVersion("Upgrade from v6.4.5", + "### Pulse version\nunknown; downloaded pulse-v6.6.0-beta.3-linux-amd64.tar.gz"), null); + for (const version of ["6.6.0-dev.branch-2", "6.6.0-beta.3-linux-amd64", "6.6.0-custom.tgz"]) { + assert.equal(extractPulseVersion("Bug", `### Pulse version\n${version}`), version); + } +}); + +for (const [filename, version] of [ + ["pulse-v6.6.0-beta.3-linux-amd64.tar.gz", "6.6.0-beta.3"], + ["pulse-v6.6.0-beta.3-linux-armv7.tar.gz", "6.6.0-beta.3"], + ["pulse-v6.6.0-beta.3.tar.gz", "6.6.0-beta.3"], + ["pulse-6.6.0-beta.3.tgz", "6.6.0-beta.3"], + ["pulse-v6.5.0-linux-amd64.tar.gz", "6.5.0"], +]) { + test(`archive intake reconciles exact-version labels silently: ${filename}`, async () => { + const { github, calls } = createGithub(); + const oldArtifactLabel = `affects-${filename.replace(/^pulse-v?/, "")}`; + const issue = { number: 2921, title: "[Bug]: Failed install", + body: `### Pulse version\n${filename}\n### Agent version\nnone`, + labels: ["bug", "affects-6.4.5", oldArtifactLabel, "needs-version-info", "needs-human", + "needs-retest-on-latest"].map(name => ({ name })) }; + await syncLabels({ github, context: createContext({ issue }), core: createCore() }); + assert.deepEqual(calls.addLabels.map(call => call.labels), [[`affects-${version}`]]); + assert.deepEqual(calls.removeLabel.map(call => call.name), + ["affects-6.4.5", oldArtifactLabel, "needs-version-info"]); + assert.deepEqual(calls.createLabel.map(call => call.name), [`affects-${version}`]); + assert.deepEqual(calls.createComment, []); + assert.deepEqual(calls.getLatestRelease, []); + assert.deepEqual(calls.paginate, []); + assert.equal(calls.getIssue.length, 2); + }); +}