fix: treat Go pseudo-versions as unknown server versions (#590)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Tommy Nevtelen
2026-07-13 00:18:21 +02:00
committed by GitHub
parent 17e7b35478
commit fb73181cba
2 changed files with 42 additions and 0 deletions
@@ -15,6 +15,13 @@
const SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/;
// Go pseudo-version prerelease segment: `<14-digit timestamp>-<12-hex sha>`,
// e.g. the prerelease part of `v0.0.0-20260703052708-048308511c72`. Untagged
// Headscale builds (per-commit `main-*` / `development` Docker images) report
// this instead of `dev`, and it parses as semver 0.0.0 — which would strip
// every capability from a server that actually runs the newest code.
const GO_PSEUDO_VERSION_PRERELEASE_RE = /^\d{14}-[0-9a-f]{12}$/;
export interface ServerVersion {
readonly major: number;
readonly minor: number;
@@ -47,6 +54,26 @@ export function parseServerVersion(raw: string): ServerVersion {
};
}
const [, maj, min, pat, pre, build] = match;
// A Go pseudo-version (v0.0.0-<timestamp>-<sha>) is an untagged dev build,
// not an ancient release: treat it like `dev` so capability checks assume
// the modern code paths instead of gating everything off.
if (
Number(maj) === 0 &&
Number(min) === 0 &&
Number(pat) === 0 &&
pre !== undefined &&
GO_PSEUDO_VERSION_PRERELEASE_RE.test(pre)
) {
return {
major: 0,
minor: 0,
patch: 0,
prerelease: pre,
build,
raw,
unknown: true,
};
}
return {
major: Number(maj),
minor: Number(min),
@@ -39,6 +39,21 @@ describe("parseServerVersion", () => {
expect(v.unknown).toBe(true);
expect(v.raw).toBe("dev");
});
test("flags Go pseudo-versions from untagged builds as unknown", () => {
// Headscale's per-commit Docker images (main-*, development) report a
// Go pseudo-version instead of `dev`. Parsing it as semver 0.0.0 would
// deny every capability to a server running the newest code.
const v = parseServerVersion("v0.0.0-20260703052708-048308511c72");
expect(v.unknown).toBe(true);
expect(v.raw).toBe("v0.0.0-20260703052708-048308511c72");
});
test("does not flag a genuine 0.0.0 prerelease as unknown", () => {
const v = parseServerVersion("v0.0.0-beta.1");
expect(v.unknown).toBe(false);
expect(v.prerelease).toBe("beta.1");
});
});
describe("gte", () => {