Compare commits

...
1 Commits
4 changed files with 128 additions and 42 deletions

No files matched your search

+19 -34
View File
@@ -7,6 +7,7 @@ import * as React from "react";
import Skeleton from "react-loading-skeleton";
import useFetchApi from "@utils/api";
import { isNetBirdCloud } from "@utils/netbird";
import { isNewerVersion } from "@utils/version";
import { useApplicationContext } from "@/contexts/ApplicationProvider";
import { VersionInfo as VersionInfoType } from "@/interfaces/Instance";
@@ -17,36 +18,14 @@ function formatVersion(version: string): string {
return version;
}
// Self-hosted builds can carry a numeric build suffix (e.g. "0.76.3-31256681241")
// that overflows the sidebar. Show the semver only and keep the rest for the
// Builds can carry a suffix that overflows the sidebar: semver build metadata
// ("0.77.0+enterprise.1" on enterprise builds) or a numeric CI build number
// ("0.76.3-31256681241"). Show the release only and keep the full string for the
// tooltip. Pre-release labels like "-rc.1" are left intact so they stay visible.
function formatShortVersion(version: string): string {
return formatVersion(version).replace(/^(v?\d+(?:\.\d+)*)-\d+$/, "$1");
}
function compareVersions(current: string, latest: string): boolean {
// Returns true if latest is newer than current
if (!current || !latest) return false;
if (current === "development") return false;
// Strip "v" prefix if present
const normalizedCurrent = current.replace(/^v/, "");
const normalizedLatest = latest.replace(/^v/, "");
const currentParts = normalizedCurrent
.split(".")
.map((p) => parseInt(p, 10) || 0);
const latestParts = normalizedLatest
.split(".")
.map((p) => parseInt(p, 10) || 0);
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
const c = currentParts[i] || 0;
const l = latestParts[i] || 0;
if (l > c) return true;
if (l < c) return false;
}
return false;
return formatVersion(version)
.replace(/\+.*$/, "")
.replace(/^(v?\d+(?:\.\d+)*)-\d+$/, "$1");
}
export const NavigationVersionInfo = () => {
@@ -84,12 +63,18 @@ const NavigationVersionInfoContent = () => {
if (!versionInfo) return null;
// Compare versions to detect updates (returns false for "development" versions)
const managementUpdateAvailable = compareVersions(
versionInfo.management_current_version,
versionInfo.management_available_version,
);
const dashboardUpdateAvailable = compareVersions(
// Prefer the server's verdict: it knows the release channel the installation
// runs on and compares with a full semver implementation. Fall back to a local
// comparison for management servers that don't report the flag yet.
const managementUpdateAvailable =
versionInfo.management_update_available ??
isNewerVersion(
versionInfo.management_current_version,
versionInfo.management_available_version,
);
// The dashboard's installed version is baked in at build time and the server
// never sees it, so this one is always compared here.
const dashboardUpdateAvailable = isNewerVersion(
dashboardVersion,
versionInfo.dashboard_available_version,
);
+5
View File
@@ -22,4 +22,9 @@ export interface VersionInfo {
management_current_version: string;
management_available_version: string;
dashboard_available_version: string;
// Whether a newer management release exists, decided server-side with a full
// semver comparison — so an enterprise build ("0.77.0+enterprise.1") counts
// as up to date against "0.77.0". Optional: management servers that predate
// the field omit it.
management_update_available?: boolean;
}
+50 -2
View File
@@ -1,4 +1,4 @@
import { isNativeSSHSupported } from "./version.js";
import { compareVersions, isNativeSSHSupported, isNewerVersion } from "./version.js";
console.log("=== Testing isNativeSSHSupported ===");
const sshTestCases = [
@@ -17,6 +17,9 @@ const sshTestCases = [
{ version: "v0.60.0-beta", shouldSupport: true, desc: "with suffix" },
{ version: "v0.60.0-rc1", shouldSupport: true, desc: "with rc suffix" },
{ version: "v0.59.9-beta", shouldSupport: false, desc: "old version with suffix" },
{ version: "0.60.0+enterprise.1", shouldSupport: true, desc: "enterprise build" },
{ version: "0.59.9+enterprise.1", shouldSupport: false, desc: "old enterprise build" },
{ version: "0.76.3-31256681241", shouldSupport: true, desc: "CI build suffix" },
];
let failures = 0;
@@ -24,11 +27,56 @@ sshTestCases.forEach(({ version, shouldSupport, desc }) => {
const result = isNativeSSHSupported(version);
const status = result === shouldSupport ? "âś“" : "âś—";
if (result !== shouldSupport) failures++;
const label = desc ? `${version.padEnd(15)} (${desc})` : version.padEnd(15);
const label = desc ? `${version.padEnd(22)} (${desc})` : version.padEnd(22);
console.log(
`${status} ${label} → ${result.toString().padStart(5)} (expected: ${shouldSupport})`,
);
});
console.log("\n=== Testing compareVersions (version >= minVersion) ===");
const compareTestCases = [
{ version: "0.60.0", min: "0.60.0", expected: true },
{ version: "0.60.1", min: "0.60.0", expected: true },
{ version: "0.59.9", min: "0.60.0", expected: false },
// Suffixed builds must compare on the release only — the trailing build
// number is not a fourth release component.
{ version: "0.60.0-beta.1", min: "0.60.1", expected: false, desc: "pre-release vs newer patch" },
{ version: "0.60.0+enterprise.1", min: "0.60.1", expected: false, desc: "enterprise vs newer patch" },
{ version: "0.77.0+enterprise.1", min: "0.77.0", expected: true, desc: "enterprise build metadata ignored" },
];
compareTestCases.forEach(({ version, min, expected, desc }) => {
const result = compareVersions(version, min);
const status = result === expected ? "âś“" : "âś—";
if (result !== expected) failures++;
const label = `${version} >= ${min}`.padEnd(38);
console.log(
`${status} ${label}${desc ? ` (${desc})` : ""} → ${result.toString().padStart(5)} (expected: ${expected})`,
);
});
console.log("\n=== Testing isNewerVersion (update available) ===");
const updateTestCases = [
// The shape enterprise/cloud installations report: same release, plus a build
// tag. Not an update.
{ current: "0.77.0+enterprise.1", latest: "0.77.0", expected: false, desc: "enterprise build, same release" },
{ current: "0.77.0+enterprise.1", latest: "0.77.1", expected: true, desc: "enterprise build, newer patch" },
{ current: "0.77.0+enterprise.2", latest: "0.76.9", expected: false, desc: "enterprise build, older latest" },
{ current: "0.76.3-31256681241", latest: "0.76.3", expected: false, desc: "CI build suffix" },
{ current: "v2.91.0", latest: "2.91.0", expected: false, desc: "v prefix on one side" },
{ current: "development", latest: "0.77.0", expected: false, desc: "development build" },
{ current: "0.77.0", latest: "", expected: false, desc: "unknown latest" },
];
updateTestCases.forEach(({ current, latest, expected, desc }) => {
const result = isNewerVersion(current, latest);
const status = result === expected ? "âś“" : "âś—";
if (result !== expected) failures++;
const label = `${current || "(empty)"} → ${latest || "(empty)"}`.padEnd(38);
console.log(
`${status} ${label}${desc ? ` (${desc})` : ""} → ${result.toString().padStart(5)} (expected: ${expected})`,
);
});
console.log(`\n${failures} test(s) failed`);
process.exit(failures > 0 ? 1 : 0);
+54 -6
View File
@@ -36,6 +36,32 @@ export const getLatestNetbirdRelease = async (
}
};
/**
* Split a version string into its numeric release components.
*
* Handles every shape NetBird components report: an optional "v" prefix, semver
* build metadata ("0.77.0+enterprise.1" on enterprise management builds), a CI
* build suffix ("0.76.3-31256681241"), and pre-release labels ("0.60.0-rc.1").
* Everything after the release itself is dropped — build metadata carries no
* precedence in semver, and a pre-release of X.Y.Z is treated as X.Y.Z so a
* feature gate keyed on a release also holds for its release candidates.
*
* Splitting on "." alone (the previous behaviour) left the suffix inside a
* component, so "0.77.0+enterprise.1" parsed as [0, 77, 0, 1] — smuggling the
* build number in as a fourth release component. That ranked today's tags
* correctly only by coincidence; dropping the suffix removes the guesswork.
*/
const releaseParts = (version: string): number[] =>
version
.trim()
.replace(/^v/i, "")
.split(/[-+]/, 1)[0]
.split(".")
.map((part) => {
const parsed = parseInt(part, 10);
return Number.isNaN(parsed) ? 0 : parsed;
});
/**
* Compare semantic versions.
* Returns true if version >= minVersion.
@@ -44,12 +70,8 @@ export const compareVersions = (
version: string,
minVersion: string,
): boolean => {
const parseVersion = (v: string): number[] => {
return v.replace(/^v/, "").split(".").map(Number);
};
const vParts = parseVersion(version);
const minParts = parseVersion(minVersion);
const vParts = releaseParts(version);
const minParts = releaseParts(minVersion);
for (let i = 0; i < Math.max(vParts.length, minParts.length); i++) {
const vPart = vParts[i] || 0;
@@ -62,6 +84,32 @@ export const compareVersions = (
return true;
};
/**
* Returns true when `latest` is a strictly newer release than `current` — i.e.
* an update is available. Only release components decide: an enterprise build
* ("0.77.0+enterprise.1") is up to date against the "0.77.0" it was built from,
* matching how the management server evaluates it server-side.
*
* "development" builds never report an update, in either position.
*/
export const isNewerVersion = (current: string, latest: string): boolean => {
if (!current || !latest) return false;
if (current === "development" || latest === "development") return false;
const currentParts = releaseParts(current);
const latestParts = releaseParts(latest);
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
const currentPart = currentParts[i] || 0;
const latestPart = latestParts[i] || 0;
if (latestPart > currentPart) return true;
if (latestPart < currentPart) return false;
}
return false;
};
/**
* Check if peer as routing peer is supported by the provided version and operating system.
* Routing peers are supported on Windows, macOS, iOS & Android starting from NetBird v0.36.6+.