diff --git a/changes/37597-ignore-build-version-compare b/changes/37597-ignore-build-version-compare new file mode 100644 index 0000000000..77a479bda2 --- /dev/null +++ b/changes/37597-ignore-build-version-compare @@ -0,0 +1 @@ +- Fleet UI: Ignore parenthesized build numbers when comparing versions for update availability (e.g. 5.0 (build 3400)) diff --git a/frontend/pages/hosts/details/cards/Software/helpers.tests.ts b/frontend/pages/hosts/details/cards/Software/helpers.tests.ts index 7465c58445..d5c1ffdcba 100644 --- a/frontend/pages/hosts/details/cards/Software/helpers.tests.ts +++ b/frontend/pages/hosts/details/cards/Software/helpers.tests.ts @@ -59,8 +59,19 @@ describe("compareVersions", () => { expect(compareVersions("v2.0.0", "v1.0.0")).toBe(1); }); - it("treats build metadata as equal (if supported)", () => { + it("treats build metadata as equal", () => { expect(compareVersions("1.0.0+20130313144700", "1.0.0")).toBe(0); + expect(compareVersions("1.0.0+exp.sha.5114f85", "1.0.0+21AF26D3")).toBe(0); + }); + + it("treats build metadata as equal even with prerelease", () => { + expect(compareVersions("1.0.0-alpha+001", "1.0.0-alpha+exp.sha")).toBe(0); + }); + + it("orders prerelease regardless of build metadata", () => { + // same core, prerelease different, builds ignored + expect(compareVersions("1.0.0-alpha+001", "1.0.0-beta+exp.sha")).toBe(-1); + expect(compareVersions("1.0.0-rc+build.1", "1.0.0+build.2")).toBe(-1); }); it("is case-insensitive for pre-release tags", () => { @@ -73,8 +84,31 @@ describe("compareVersions", () => { expect(compareVersions("01.1.0", "1.1.0")).toBe(0); }); - it("compares build number in parentheses", () => { - expect(compareVersions("6.1.11 (39163)", "6.1.11 (30000)")).toBe(1); + // Parenthesized build metadata cases + it("treats parenthesized build number as metadata (same core)", () => { + expect(compareVersions("8.0 (build 6300)", "8.0")).toBe(0); + expect(compareVersions("8.0 (build 6300)", "8.0 (build 7000)")).toBe(0); + }); + + it("compares different core versions ignoring parenthesized build", () => { + expect(compareVersions("8.1 (build 6300)", "8.0")).toBe(1); + expect(compareVersions("8.0", "8.1 (build 6300)")).toBe(-1); + expect(compareVersions("6.1.12 (39163)", "6.1.11 (30000)")).toBe(1); + }); + + it("handles mixed +build and parenthesized build metadata", () => { + expect(compareVersions("1.0.0+20130313144700", "1.0.0 (build 1234)")).toBe( + 0 + ); + expect( + compareVersions("2.3.4-beta+exp.sha", "2.3.4-beta (build 9999)") + ).toBe(0); + }); + + it("handles unexpected parenthesis content as build metadata suffix", () => { + // ignore any trailing `(build ...)` suffix + expect(compareVersions("1.2.3 (Build 5)", "1.2.3")).toBe(0); + expect(compareVersions("1.2.3 (build foo-bar)", "1.2.3")).toBe(0); }); }); diff --git a/frontend/pages/hosts/details/cards/Software/helpers.tsx b/frontend/pages/hosts/details/cards/Software/helpers.tsx index 88dc9bb9e0..6d78d9367f 100644 --- a/frontend/pages/hosts/details/cards/Software/helpers.tsx +++ b/frontend/pages/hosts/details/cards/Software/helpers.tsx @@ -30,9 +30,23 @@ export const getHostSoftwareFilterFromQueryParams = ( const PRE_RELEASE_ORDER = ["alpha", "beta", "rc", ""]; /** - * Removes build metadata from a version string (e.g., "1.0.0+build" -> "1.0.0"). + * Removes build metadata and parenthesized build info from a version string. + * Examples: + * "1.0.0+build.1" -> "1.0.0" + * "8.0 (build 6300)" -> "8.0" + * "8.1.2 (Build 6300)" -> "8.1.2" */ -const stripBuildMetadata = (version: string): string => version.split("+")[0]; +const stripBuildMetadata = (version: string): string => { + if (typeof version !== "string") { + return ""; + } + + // First drop any parenthesized "build ..." suffix, e.g. "8.0 (build 6300)" -> "8.0" + const withoutParenBuild = version.replace(/\s*\(build\s+[^)]+\)\s*$/i, ""); + + // Then drop standard SemVer +build metadata, e.g. "1.0.0+build.1" -> "1.0.0" + return withoutParenBuild.split("+")[0]; +}; /** * Splits a version string into an array of numeric and string segments.