diff --git a/packages/backend/server/package.json b/packages/backend/server/package.json index 41f4a92c33..0cf40d31fe 100644 --- a/packages/backend/server/package.json +++ b/packages/backend/server/package.json @@ -101,7 +101,7 @@ "react-dom": "19.2.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.2", - "semver": "^7.7.3", + "semver": "^7.7.4", "socket.io": "^4.8.1", "stripe": "^17.7.0", "tldts": "^7.0.19", @@ -128,7 +128,7 @@ "@types/nodemailer": "^7.0.0", "@types/on-headers": "^1.0.3", "@types/react": "^19.0.1", - "@types/semver": "^7.5.8", + "@types/semver": "^7.7.1", "@types/sinon": "^21.0.0", "@types/supertest": "^7.0.0", "ava": "^7.0.0", diff --git a/packages/backend/server/src/__tests__/version.spec.ts b/packages/backend/server/src/__tests__/version.spec.ts index 418bf579bc..15e61907c6 100644 --- a/packages/backend/server/src/__tests__/version.spec.ts +++ b/packages/backend/server/src/__tests__/version.spec.ts @@ -6,6 +6,7 @@ import { AppModule } from '../app.module'; import { CANARY_CLIENT_VERSION_MAX_AGE_DAYS, ConfigFactory, + hasNewerVersion, UseNamedGuard, } from '../base'; import { Public } from '../core/auth/guard'; @@ -249,3 +250,11 @@ test('should reject old canary date version in canary namespace', async t => { env.NAMESPACE = prevNamespace; } }); + +test('should compare release versions for available upgrades', t => { + t.false(hasNewerVersion('0.26.5', '0.26.4')); + t.false(hasNewerVersion('0.26.5', '0.26.5')); + t.true(hasNewerVersion('0.26.5', '0.26.6')); + t.true(hasNewerVersion('0.26.5', '0.26.6-beta.1')); + t.false(hasNewerVersion('0.26.6-beta.2', '0.26.6-beta.1')); +}); diff --git a/packages/backend/server/src/base/utils/client-version.ts b/packages/backend/server/src/base/utils/client-version.ts index 66be07cba9..28b4860ea3 100644 --- a/packages/backend/server/src/base/utils/client-version.ts +++ b/packages/backend/server/src/base/utils/client-version.ts @@ -1,3 +1,5 @@ +import semver from 'semver'; + const DAY_MS = 24 * 60 * 60 * 1000; // Example: 2026.2.6-canary.015 @@ -89,3 +91,26 @@ export function checkCanaryDateClientVersion( normalized: parsed.normalized, }; } + +function normalizeComparableVersion(version: string): string | null { + const canary = parseCanaryDateClientVersion(version); + return semver.valid(canary?.normalized ?? version.trim(), { + loose: true, + }); +} + +export function hasNewerVersion( + currentVersion: string, + nextVersion: string +): boolean { + const current = normalizeComparableVersion(currentVersion); + const next = normalizeComparableVersion(nextVersion); + + if (!current || !next) { + return currentVersion.trim() !== nextVersion.trim(); + } + + return semver.gt(next, current, { + loose: true, + }); +} diff --git a/packages/backend/server/src/core/config/resolver.ts b/packages/backend/server/src/core/config/resolver.ts index 10831d7ce9..2c989615a2 100644 --- a/packages/backend/server/src/core/config/resolver.ts +++ b/packages/backend/server/src/core/config/resolver.ts @@ -12,7 +12,7 @@ import { } from '@nestjs/graphql'; import { GraphQLJSON, GraphQLJSONObject } from 'graphql-scalars'; -import { Config, URLHelper } from '../../base'; +import { Config, hasNewerVersion, URLHelper } from '../../base'; import { Namespace } from '../../env'; import { Feature, type WorkspaceFeatureName } from '../../models'; import { CurrentUser, Public } from '../auth'; @@ -143,7 +143,7 @@ export class ServerConfigResolver { }>; const latest = releases.at(0); - if (!latest || latest.name === env.version) { + if (!latest || !hasNewerVersion(env.version, latest.name)) { return null; } diff --git a/yarn.lock b/yarn.lock index 2c35bccf88..600fd5ac05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -977,7 +977,7 @@ __metadata: "@types/nodemailer": "npm:^7.0.0" "@types/on-headers": "npm:^1.0.3" "@types/react": "npm:^19.0.1" - "@types/semver": "npm:^7.5.8" + "@types/semver": "npm:^7.7.1" "@types/sinon": "npm:^21.0.0" "@types/supertest": "npm:^7.0.0" ava: "npm:^7.0.0" @@ -1019,7 +1019,7 @@ __metadata: react-email: "npm:^4.3.2" reflect-metadata: "npm:^0.2.2" rxjs: "npm:^7.8.2" - semver: "npm:^7.7.3" + semver: "npm:^7.7.4" sinon: "npm:^21.0.1" socket.io: "npm:^4.8.1" socket.io-client: "npm:^4.8.3" @@ -16451,10 +16451,10 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:^7, @types/semver@npm:^7.5.8": - version: 7.7.0 - resolution: "@types/semver@npm:7.7.0" - checksum: 10/ee4514c6c852b1c38f951239db02f9edeea39f5310fad9396a00b51efa2a2d96b3dfca1ae84c88181ea5b7157c57d32d7ef94edacee36fbf975546396b85ba5b +"@types/semver@npm:^7, @types/semver@npm:^7.7.1": + version: 7.7.1 + resolution: "@types/semver@npm:7.7.1" + checksum: 10/8f09e7e6ca3ded67d78ba7a8f7535c8d9cf8ced83c52e7f3ac3c281fe8c689c3fe475d199d94390dc04fc681d51f2358b430bb7b2e21c62de24f2bee2c719068 languageName: node linkType: hard