diff --git a/codersdk/deployment.go b/codersdk/deployment.go index b529ee9e42..d277fba53f 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -1251,7 +1251,11 @@ func DefaultSupportLinks(docsURL string) []LinkConfig { } func removeTrailingVersionInfo(v string) string { - return strings.Split(strings.Split(v, "-")[0], "+")[0] + // Strip build metadata (everything after '+'). + v, _, _ = strings.Cut(v, "+") + // Strip '-devel' suffix if present. + v = strings.TrimSuffix(v, "-devel") + return v } func DefaultDocsURL() string { diff --git a/codersdk/deployment_internal_test.go b/codersdk/deployment_internal_test.go index d350447fd6..35d3b34771 100644 --- a/codersdk/deployment_internal_test.go +++ b/codersdk/deployment_internal_test.go @@ -25,10 +25,36 @@ func TestRemoveTrailingVersionInfo(t *testing.T) { Version: "v2.16.0+683a720-devel", ExpectedAfterStrippingInfo: "v2.16.0", }, + // RC versions: preserve the -rc.X suffix, strip build metadata. + { + Version: "v2.32.0-rc.1+abc123", + ExpectedAfterStrippingInfo: "v2.32.0-rc.1", + }, + { + Version: "v2.32.0-rc.0", + ExpectedAfterStrippingInfo: "v2.32.0-rc.0", + }, + { + Version: "v2.32.0-rc.1+683a720-devel", + ExpectedAfterStrippingInfo: "v2.32.0-rc.1", + }, + // Bare devel suffix, no build metadata. + { + Version: "v2.32.0-devel", + ExpectedAfterStrippingInfo: "v2.32.0", + }, + // Plain release, identity case. + { + Version: "v2.16.0", + ExpectedAfterStrippingInfo: "v2.16.0", + }, } for _, tc := range testCases { - stripped := removeTrailingVersionInfo(tc.Version) - require.Equal(t, tc.ExpectedAfterStrippingInfo, stripped) + t.Run(tc.Version, func(t *testing.T) { + t.Parallel() + stripped := removeTrailingVersionInfo(tc.Version) + require.Equal(t, tc.ExpectedAfterStrippingInfo, stripped) + }) } } diff --git a/site/src/utils/docs.test.ts b/site/src/utils/docs.test.ts new file mode 100644 index 0000000000..481522f9ac --- /dev/null +++ b/site/src/utils/docs.test.ts @@ -0,0 +1,67 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +describe("defaultDocsUrl", () => { + beforeEach(() => { + // Reset module-level caches (CACHED_DOCS_URL in docs.ts and + // CACHED_BUILD_INFO in buildInfo.ts) by forcing fresh imports. + vi.resetModules(); + // Clean up meta tags from previous tests so each case starts fresh. + document.querySelector('meta[property="docs-url"]')?.remove(); + document.querySelector('meta[property="build-info"]')?.remove(); + }); + + function setBuildInfoVersion(version: string) { + const meta = document.createElement("meta"); + meta.setAttribute("property", "build-info"); + meta.setAttribute("content", JSON.stringify({ version })); + document.head.appendChild(meta); + } + + async function getDocsUrl(path: string): Promise { + // Dynamic import so we get a fresh module with cleared caches. + const { docs } = await import("./docs"); + return docs(path); + } + + it("should preserve RC prerelease and strip build metadata", async () => { + setBuildInfoVersion("v2.32.0-rc.1+abc123"); + const url = await getDocsUrl("/admin/users"); + expect(url).toBe("https://coder.com/docs/@v2.32.0-rc.1/admin/users"); + }); + + it("should preserve RC prerelease when no build metadata present", async () => { + setBuildInfoVersion("v2.32.0-rc.0"); + const url = await getDocsUrl("/admin/users"); + expect(url).toBe("https://coder.com/docs/@v2.32.0-rc.0/admin/users"); + }); + + it("should strip devel suffix and build metadata", async () => { + setBuildInfoVersion("v2.16.0-devel+683a720"); + const url = await getDocsUrl("/admin/users"); + expect(url).toBe("https://coder.com/docs/@v2.16.0/admin/users"); + }); + + it("should strip build metadata from release version", async () => { + setBuildInfoVersion("v2.16.0+683a720"); + const url = await getDocsUrl("/admin/users"); + expect(url).toBe("https://coder.com/docs/@v2.16.0/admin/users"); + }); + + it("should strip bare devel suffix with no build metadata", async () => { + setBuildInfoVersion("v2.32.0-devel"); + const url = await getDocsUrl("/admin/users"); + expect(url).toBe("https://coder.com/docs/@v2.32.0/admin/users"); + }); + + it("should use plain release version as-is", async () => { + setBuildInfoVersion("v2.16.0"); + const url = await getDocsUrl("/admin/users"); + expect(url).toBe("https://coder.com/docs/@v2.16.0/admin/users"); + }); + + it("should produce unversioned URL for v0.0.0 dev builds", async () => { + setBuildInfoVersion("v0.0.0-devel+abc123"); + const url = await getDocsUrl("/admin/users"); + expect(url).toBe("https://coder.com/docs/admin/users"); + }); +}); diff --git a/site/src/utils/docs.ts b/site/src/utils/docs.ts index 162f94a354..70d63dd9ff 100644 --- a/site/src/utils/docs.ts +++ b/site/src/utils/docs.ts @@ -8,10 +8,12 @@ function defaultDocsUrl(): string { return docsUrl; } - // Strip the postfix version info that's not part of the link. - const i = version?.match(/[+-]/)?.index ?? -1; - if (i >= 0) { - version = version.slice(0, i); + // Strip build metadata after '+', then remove a '-devel' suffix + // if present. Preserve '-rc.X' suffixes so versioned docs links + // point at the correct release candidate. + version = version.split("+")[0].replace(/-devel$/, ""); + if (version === "v0.0.0") { + return docsUrl; } return `${docsUrl}/@${version}`; }