From 4c76b952719cd546865965d5494e2ec2faf31374 Mon Sep 17 00:00:00 2001 From: Katerina Skroumpelou Date: Tue, 2 Jul 2024 20:26:36 +0300 Subject: [PATCH] fix(nx-cloud): onboarding cloud version handling (#26790) ## Current Behavior ## Expected Behavior ## Related Issue(s) Fixes # --- .../nx-cloud/utilities/url-shorten.spec.ts | 103 ++++++++++++++++++ .../nx/src/nx-cloud/utilities/url-shorten.ts | 69 ++++++++++-- 2 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 packages/nx/src/nx-cloud/utilities/url-shorten.spec.ts diff --git a/packages/nx/src/nx-cloud/utilities/url-shorten.spec.ts b/packages/nx/src/nx-cloud/utilities/url-shorten.spec.ts new file mode 100644 index 0000000000000..372fa1e263a06 --- /dev/null +++ b/packages/nx/src/nx-cloud/utilities/url-shorten.spec.ts @@ -0,0 +1,103 @@ +import { + removeVersionModifier, + compareCleanCloudVersions, + getNxCloudVersion, + versionIsValid, +} from './url-shorten'; + +jest.mock('axios', () => ({ + get: jest.fn(), +})); + +describe('URL shorten various functions', () => { + describe('compareCleanCloudVersions', () => { + it('should return 1 if the first version is newer', () => { + expect(compareCleanCloudVersions('2407.01.100', '2312.25.50')).toBe(1); + expect(compareCleanCloudVersions('2402.01.20', '2401.31.300')).toBe(1); + expect(compareCleanCloudVersions('2401.01.20', '2312.31.300')).toBe(1); + expect(compareCleanCloudVersions('2312.26.05', '2312.25.100')).toBe(1); + expect(compareCleanCloudVersions('2312.25.100', '2311.25.90')).toBe(1); + expect(compareCleanCloudVersions('2312.25.120', '2312.24.110')).toBe(1); + }); + + it('should return -1 if the first version is older', () => { + expect(compareCleanCloudVersions('2312.25.50', '2407.01.100')).toBe(-1); + expect(compareCleanCloudVersions('2312.31.100', '2401.01.100')).toBe(-1); + expect(compareCleanCloudVersions('2312.25.100', '2312.26.50')).toBe(-1); + expect(compareCleanCloudVersions('2311.25.90', '2312.25.80')).toBe(-1); + expect(compareCleanCloudVersions('2312.24.110', '2312.25.100')).toBe(-1); + }); + + it('should return 0 if both versions are the same', () => { + expect(compareCleanCloudVersions('2312.25.50', '2312.25.50')).toBe(0); + expect(compareCleanCloudVersions('2407.01.100', '2407.01.100')).toBe(0); + }); + }); + + describe('removeVersionModifier', () => { + it('should return the version without the modifier', () => { + expect(removeVersionModifier('2406.13.5.hotfix2')).toBe('2406.13.5'); + expect(removeVersionModifier('2024.07.01.beta')).toBe('2024.07.01'); + expect(removeVersionModifier('2023.12.25.alpha1')).toBe('2023.12.25'); + }); + + it('should return the original version if there is no modifier', () => { + expect(removeVersionModifier('2406.13.5')).toBe('2406.13.5'); + expect(removeVersionModifier('2024.07.01')).toBe('2024.07.01'); + expect(removeVersionModifier('2023.12.25')).toBe('2023.12.25'); + }); + + it('should handle versions with multiple dots and hyphens correctly', () => { + expect(removeVersionModifier('2406-13-5-hotfix2')).toBe('2406.13.5'); + expect(removeVersionModifier('2024.07.01-patch')).toBe('2024.07.01'); + expect(removeVersionModifier('2023.12.25.alpha-1')).toBe('2023.12.25'); + }); + }); + + describe('getNxCloudVersion', () => { + const axios = require('axios'); + const apiUrl = 'https://cloud.nx.app'; + + it('should return the version if the response is successful', async () => { + const mockVersion = '2406.13.5.hotfix2'; + axios.get.mockResolvedValue({ + data: { version: mockVersion }, + }); + + const version = await getNxCloudVersion(apiUrl); + expect(version).toBe('2406.13.5'); + expect(axios.get).toHaveBeenCalledWith( + `${apiUrl}/nx-cloud/system/version` + ); + }); + + it('should return null if the request fails', async () => { + const mockError = new Error('Request failed'); + axios.get.mockRejectedValue(mockError); + const version = await getNxCloudVersion(apiUrl); + expect(version).toBeNull(); + }); + }); + + describe('versionIsValid', () => { + it('should return true for valid versions with build numbers', () => { + expect(versionIsValid('2407.01.100')).toBe(true); + expect(versionIsValid('2312.25.50')).toBe(true); + }); + + it('should return false for versions without build numbers', () => { + expect(versionIsValid('2407.01')).toBe(false); // Missing build number + expect(versionIsValid('2312.25')).toBe(false); // Missing build number + }); + + it('should return false for invalid versions', () => { + expect(versionIsValid('240701.100')).toBe(false); // No periods separating parts + expect(versionIsValid('2312.250.50')).toBe(false); // Day part has three digits + expect(versionIsValid('2401.1.100')).toBe(false); // Day part has one digit + expect(versionIsValid('23.12.26')).toBe(false); // YearMonth part has two digits + expect(versionIsValid('2312.26.')).toBe(false); // Extra period at the end + expect(versionIsValid('.2312.26.100')).toBe(false); // Extra period at the beginning + expect(versionIsValid('2312.26.extra')).toBe(false); // Non-numeric build number + }); + }); +}); diff --git a/packages/nx/src/nx-cloud/utilities/url-shorten.ts b/packages/nx/src/nx-cloud/utilities/url-shorten.ts index c228620e9cbf3..b28ad1ba35d96 100644 --- a/packages/nx/src/nx-cloud/utilities/url-shorten.ts +++ b/packages/nx/src/nx-cloud/utilities/url-shorten.ts @@ -1,6 +1,5 @@ import { logger } from '../../devkit-exports'; import { getGithubSlugOrNull } from '../../utils/git-utils'; -import { lt } from 'semver'; export async function shortenedCloudUrl( installationSource: string, @@ -13,9 +12,17 @@ export async function shortenedCloudUrl( process.env.NX_CLOUD_API || process.env.NRWL_API || `https://cloud.nx.app` ); - const version = await getNxCloudVersion(apiUrl); - - if (version && lt(removeVersionModifier(version), '2406.11.5')) { + try { + const version = await getNxCloudVersion(apiUrl); + if ( + (version && compareCleanCloudVersions(version, '2406.11.5') < 0) || + !version + ) { + return apiUrl; + } + } catch (e) { + logger.verbose(`Failed to get Nx Cloud version. + ${e}`); return apiUrl; } @@ -71,7 +78,7 @@ export async function repoUsesGithub(github?: boolean) { ); } -function removeTrailingSlash(apiUrl: string) { +export function removeTrailingSlash(apiUrl: string) { return apiUrl[apiUrl.length - 1] === '/' ? apiUrl.slice(0, -1) : apiUrl; } @@ -89,7 +96,7 @@ function getSource( } } -function getURLifShortenFailed( +export function getURLifShortenFailed( usesGithub: boolean, githubSlug: string, apiUrl: string, @@ -128,23 +135,67 @@ async function getInstallationSupportsGitHub(apiUrl: string): Promise { } } -async function getNxCloudVersion(apiUrl: string): Promise { +export async function getNxCloudVersion( + apiUrl: string +): Promise { try { const response = await require('axios').get( `${apiUrl}/nx-cloud/system/version` ); const version = removeVersionModifier(response.data.version); + const isValid = versionIsValid(version); if (!version) { throw new Error('Failed to extract version from response.'); } + if (!isValid) { + throw new Error(`Invalid version format: ${version}`); + } return version; } catch (e) { logger.verbose(`Failed to get version of Nx Cloud. ${e}`); + return null; } } -function removeVersionModifier(versionString: string): string { - // version may be something like 2406.13.5.hotfix2 +export function removeVersionModifier(versionString: string): string { + // Cloud version string is in the format of YYMM.DD.BuildNumber-Modifier return versionString.split(/[\.-]/).slice(0, 3).join('.'); } + +export function versionIsValid(version: string): boolean { + // Updated Regex pattern to require YYMM.DD.BuildNumber format + // All parts are required, including the BuildNumber. + const pattern = /^\d{4}\.\d{2}\.\d+$/; + return pattern.test(version); +} + +export function compareCleanCloudVersions( + version1: string, + version2: string +): number { + const parseVersion = (version: string) => { + // The format we're using is YYMM.DD.BuildNumber + const parts = version.split('.').map((part) => parseInt(part, 10)); + return { + yearMonth: parts[0], + day: parts[1], + buildNumber: parts[2], + }; + }; + + const v1 = parseVersion(version1); + const v2 = parseVersion(version2); + + if (v1.yearMonth !== v2.yearMonth) { + return v1.yearMonth > v2.yearMonth ? 1 : -1; + } + if (v1.day !== v2.day) { + return v1.day > v2.day ? 1 : -1; + } + if (v1.buildNumber !== v2.buildNumber) { + return v1.buildNumber > v2.buildNumber ? 1 : -1; + } + + return 0; +}