From 72c0a9f31ace04ad7712dea33d9f75f73fc7bc3d Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Wed, 13 Nov 2024 11:53:53 +1100 Subject: [PATCH 01/32] Initial `skuba migrate` modifications --- src/cli/migrate/index.ts | 2 +- src/cli/migrate/nodeVersion/index.test.ts | 141 +++++++++++++++++----- src/cli/migrate/nodeVersion/index.ts | 124 ++++++++++++++++--- 3 files changed, 224 insertions(+), 43 deletions(-) diff --git a/src/cli/migrate/index.ts b/src/cli/migrate/index.ts index 650eedb95..62ac9f33e 100644 --- a/src/cli/migrate/index.ts +++ b/src/cli/migrate/index.ts @@ -3,7 +3,7 @@ import { log } from '../../utils/logging'; import { nodeVersionMigration } from './nodeVersion'; const migrations: Record Promise> = { - node20: () => nodeVersionMigration(20), + node22: () => nodeVersionMigration(22), }; const logAvailableMigrations = () => { diff --git a/src/cli/migrate/nodeVersion/index.test.ts b/src/cli/migrate/nodeVersion/index.test.ts index 74665a999..4801b7a41 100644 --- a/src/cli/migrate/nodeVersion/index.test.ts +++ b/src/cli/migrate/nodeVersion/index.test.ts @@ -1,6 +1,6 @@ import memfs, { vol } from 'memfs'; -import { nodeVersionMigration } from '.'; +import { getLatestNodeTypes, nodeVersionMigration } from '.'; jest.mock('fs-extra', () => memfs); jest.mock('fast-glob', () => ({ @@ -9,9 +9,16 @@ jest.mock('fast-glob', () => ({ })); jest.mock('../../../utils/logging'); +jest + .spyOn(global, 'fetch') + .mockImplementation(() => + Promise.resolve( + new Response(JSON.stringify({ 'dist-tags': { latest: '22.9.0' } })), + ), + ); + const volToJson = () => vol.toJSON(process.cwd(), undefined, true); -beforeEach(jest.clearAllMocks); beforeEach(() => vol.reset()); describe('nodeVersionMigration', () => { @@ -41,22 +48,30 @@ describe('nodeVersionMigration', () => { 'plugins:\n - docker#v3.0.0:\n image: node:18.1.2-slim\n', '.buildkite/pipeline2.yml': 'plugins:\n - docker#v3.0.0:\n image: node:18\n', + '.buildkite/pipline3.yml': + 'plugins:\n - docker#v3.0.0:\n image: public.ecr.aws/docker/library/node:20-alpine\n', + '.node-version': '18.1.2\n', + '.node-version2': 'v20.15.0\n', }, filesAfter: { - '.nvmrc': '20\n', - Dockerfile: 'FROM node:20\nRUN echo "hello"', + '.nvmrc': '22\n', + Dockerfile: 'FROM node:22\nRUN echo "hello"', 'Dockerfile.dev-deps': - 'FROM --platform=linux/amd64 node:20-slim AS dev-deps\nRUN echo "hello"', + 'FROM --platform=linux/amd64 node:22-slim AS dev-deps\nRUN echo "hello"', 'serverless.yml': - 'provider:\n logRetentionInDays: 30\n runtime: nodejs20.x\n region: ap-southeast-2', + 'provider:\n logRetentionInDays: 30\n runtime: nodejs22.x\n region: ap-southeast-2', 'serverless.melb.yaml': - 'provider:\n logRetentionInDays: 7\n runtime: nodejs20.x\n region: ap-southeast-4', - 'infra/myCoolStack.ts': `const worker = new aws_lambda.Function(this, 'worker', {\n architecture: aws_lambda.Architecture[architecture],\n code: new aws_lambda.AssetCode('./lib'),\n runtime: aws_lambda.Runtime.NODEJS_20_X,\n}`, - 'infra/myCoolFolder/evenCoolerStack.ts': `const worker = new aws_lambda.Function(this, 'worker', {\n architecture: aws_lambda.Architecture[architecture],\n code: new aws_lambda.AssetCode('./lib'),\n runtime: aws_lambda.Runtime.NODEJS_20_X,\n}`, + 'provider:\n logRetentionInDays: 7\n runtime: nodejs22.x\n region: ap-southeast-4', + 'infra/myCoolStack.ts': `const worker = new aws_lambda.Function(this, 'worker', {\n architecture: aws_lambda.Architecture[architecture],\n code: new aws_lambda.AssetCode('./lib'),\n runtime: aws_lambda.Runtime.NODEJS_22_X,\n}`, + 'infra/myCoolFolder/evenCoolerStack.ts': `const worker = new aws_lambda.Function(this, 'worker', {\n architecture: aws_lambda.Architecture[architecture],\n code: new aws_lambda.AssetCode('./lib'),\n runtime: aws_lambda.Runtime.NODEJS_22_X,\n}`, '.buildkite/pipeline.yml': - 'plugins:\n - docker#v3.0.0:\n image: node:20-slim\n', + 'plugins:\n - docker#v3.0.0:\n image: node:22-slim\n', '.buildkite/pipeline2.yml': - 'plugins:\n - docker#v3.0.0:\n image: node:20\n', + 'plugins:\n - docker#v3.0.0:\n image: node:22\n', + '.buildkite/pipline3.yml': + 'plugins:\n - docker#v3.0.0:\n image: public.ecr.aws/docker/library/node:22-alpine\n', + '.node-version': '22\n', + '.node-version2': 'v22\n', }, }, { @@ -79,36 +94,48 @@ describe('nodeVersionMigration', () => { 'FROM gcr.io/distroless/nodejs18-debian12\nRUN echo "hello"', 'Dockerfile.10': 'FROM --platform=linux/amd64 gcr.io/distroless/nodejs18-debian12 AS dev-deps\nRUN echo "hello"', + 'Dockerfile.11': + 'FROM --platform=${BUILDPLATFORM:-arm64} gcr.io/distroless/nodejs20-debian12@sha256:9f43117c3e33c3ed49d689e51287a246edef3af0afed51a54dc0a9095b2b3ef9 AS runtime', + 'Dockerfile.12': + '# syntax=docker/dockerfile:1.10@sha256:865e5dd094beca432e8c0a1d5e1c465db5f998dca4e439981029b3b81fb39ed5\nFROM --platform=arm64 node:20@sha256:a5e0ed56f2c20b9689e0f7dd498cac7e08d2a3a283e92d9304e7b9b83e3c6ff3 AS dev-deps', + 'Dockerfile.13': + 'FROM public.ecr.aws/docker/library/node:20-alpine@sha256:c13b26e7e602ef2f1074aef304ce6e9b7dd284c419b35d89fcf3cc8e44a8def9 AS runtime', }, filesAfter: { - '.nvmrc': '20\n', - 'Dockerfile.1': 'FROM node:20\nRUN echo "hello"', - 'Dockerfile.2': 'FROM node:20\nRUN echo "hello"', - 'Dockerfile.3': 'FROM node:20-slim\nRUN echo "hello"', - 'Dockerfile.4': 'FROM node:20-slim\nRUN echo "hello"', + '.nvmrc': '22\n', + 'Dockerfile.1': 'FROM node:22\nRUN echo "hello"', + 'Dockerfile.2': 'FROM node:22\nRUN echo "hello"', + 'Dockerfile.3': 'FROM node:22-slim\nRUN echo "hello"', + 'Dockerfile.4': 'FROM node:22-slim\nRUN echo "hello"', 'Dockerfile.5': - 'FROM --platform=linux/amd64 node:20 AS dev-deps\nRUN echo "hello"', + 'FROM --platform=linux/amd64 node:22 AS dev-deps\nRUN echo "hello"', 'Dockerfile.6': - 'FROM --platform=linux/amd64 node:20 AS dev-deps\nRUN echo "hello"', + 'FROM --platform=linux/amd64 node:22 AS dev-deps\nRUN echo "hello"', 'Dockerfile.7': - 'FROM --platform=linux/amd64 node:20-slim AS dev-deps\nRUN echo "hello"', + 'FROM --platform=linux/amd64 node:22-slim AS dev-deps\nRUN echo "hello"', 'Dockerfile.8': - 'FROM --platform=linux/amd64 node:20-slim AS dev-deps\nRUN echo "hello"', + 'FROM --platform=linux/amd64 node:22-slim AS dev-deps\nRUN echo "hello"', 'Dockerfile.9': - 'FROM gcr.io/distroless/nodejs20-debian12\nRUN echo "hello"', + 'FROM gcr.io/distroless/nodejs22-debian12\nRUN echo "hello"', 'Dockerfile.10': - 'FROM --platform=linux/amd64 gcr.io/distroless/nodejs20-debian12 AS dev-deps\nRUN echo "hello"', + 'FROM --platform=linux/amd64 gcr.io/distroless/nodejs22-debian12 AS dev-deps\nRUN echo "hello"', + 'Dockerfile.11': + 'FROM --platform=${BUILDPLATFORM:-arm64} gcr.io/distroless/nodejs22-debian12 AS runtime', + 'Dockerfile.12': + '# syntax=docker/dockerfile:1.10@sha256:865e5dd094beca432e8c0a1d5e1c465db5f998dca4e439981029b3b81fb39ed5\nFROM --platform=arm64 node:22 AS dev-deps', + 'Dockerfile.13': + 'FROM public.ecr.aws/docker/library/node:22-alpine AS runtime', }, }, { - scenario: 'already node 20', + scenario: 'already node 22', filesBefore: { - '.nvmrc': '20\n', - Dockerfile: 'FROM node:20\nRUN echo "hello"', + '.nvmrc': '22\n', + Dockerfile: 'FROM node:22\nRUN echo "hello"', 'Dockerfile.dev-deps': - 'FROM --platform=linux/amd64 node:20-slim AS dev-deps\nRUN echo "hello"', + 'FROM --platform=linux/amd64 node:22-slim AS dev-deps\nRUN echo "hello"', 'serverless.yml': - 'provider:\n logRetentionInDays: 30\n runtime: nodejs20.x\n region: ap-southeast-2', + 'provider:\n logRetentionInDays: 30\n runtime: nodejs22.x\n region: ap-southeast-2', }, }, { @@ -117,6 +144,49 @@ describe('nodeVersionMigration', () => { Dockerfile: 'FROM node:latest\nRUN echo "hello"', }, }, + { + scenario: 'node types', + filesBefore: { + 'package.json': '"@types/node": "^14.0.0"', + '1/package.json': '"@types/node": "18.0.0"', + '2/package.json': `"engines": {\n"node": ">=18"\n},\n`, + '3/package.json': `"engines": {\n"node": ">=18"\n},\n"skuba": {\n"type": "package"\n}`, + '4/package.json': `"engines": {\n"node": ">=18"\n},\n"skuba": {\n"type": "application"\n}`, + }, + filesAfter: { + 'package.json': '"@types/node": "^22.9.0"', + '1/package.json': '"@types/node": "22.9.0"', + '2/package.json': `"engines": {\n"node": ">=22"\n},\n`, + '3/package.json': `"engines": {\n"node": ">=18"\n},\n"skuba": {\n"type": "package"\n}`, + '4/package.json': `"engines": {\n"node": ">=22"\n},\n"skuba": {\n"type": "application"\n}`, + }, + }, + { + scenario: 'tsconfig target', + filesBefore: { + 'tsconfig.json': '"target": "ES2020"', + '1/tsconfig.json': '"target": "es2014"', + '2/tsconfig.json': '"target": "ESNext"', + }, + filesAfter: { + 'tsconfig.json': '"target": "ES2024"', + '1/tsconfig.json': '"target": "ES2024"', + '2/tsconfig.json': '"target": "ES2024"', + }, + }, + { + scenario: 'docker-compose.yml target', + filesBefore: { + 'docker-compose.yml': 'image: node:18.1.2\n', + 'docker-compose.dev.yml': 'image: node:18\n', + 'docker-compose.prod.yml': 'image: node:18-slim\n', + }, + filesAfter: { + 'docker-compose.yml': 'image: node:22\n', + 'docker-compose.dev.yml': 'image: node:22\n', + 'docker-compose.prod.yml': 'image: node:22-slim\n', + }, + }, ]; it.each(scenarios)( @@ -124,9 +194,24 @@ describe('nodeVersionMigration', () => { async ({ filesBefore, filesAfter }) => { vol.fromJSON(filesBefore, process.cwd()); - await nodeVersionMigration(20); + await nodeVersionMigration(22); expect(volToJson()).toEqual(filesAfter ?? filesBefore); }, ); }); + +describe('getLatestNodeTypes', () => { + it('fetches the latest node types version', async () => { + const { version, err } = await getLatestNodeTypes(); + expect(version).toBe('22.9.0'); + expect(err).toBeUndefined(); + }); + it('defaults to 22.9.0 if the fetch fails', async () => { + jest.spyOn(global, 'fetch').mockImplementation(() => Promise.reject()); + const { version, err } = await getLatestNodeTypes(); + + expect(version).toBe('22.9.0'); + expect(err).toBe('Failed to fetch latest version, using fallback version'); + }); +}); diff --git a/src/cli/migrate/nodeVersion/index.ts b/src/cli/migrate/nodeVersion/index.ts index 6d14357aa..3d5f7d565 100644 --- a/src/cli/migrate/nodeVersion/index.ts +++ b/src/cli/migrate/nodeVersion/index.ts @@ -14,12 +14,45 @@ type SubPatch = ( replace: string; }; +type VersionResult = { + version: string; + err: string | undefined; +}; + +type RegistryResponse = { + 'dist-tags': Record; +}; + +export const getLatestNodeTypes = async (): Promise => { + const FALLBACK_VERSION = '22.9.0'; + const url = 'https://registry.npmjs.org/@types/node'; + const headers = { + accept: + 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*', + }; + + try { + const fetchResponse = await fetch(url, { headers }); + const jsonResponse = (await fetchResponse.json()) as RegistryResponse; + const latest = jsonResponse['dist-tags'].latest; + + return { version: latest ?? FALLBACK_VERSION, err: undefined }; + } catch { + return { + version: FALLBACK_VERSION, + err: 'Failed to fetch latest version, using fallback version', + }; + } +}; + +const SHA_REGEX = /(?<=node.*)(@sha256:[a-f0-9]{64})/gm; + const subPatches: SubPatch[] = [ { file: '.nvmrc', replace: '<%- version %>\n' }, { files: 'Dockerfile*', - test: /^FROM(.*) node:[0-9.]+(\.[^- \n]+)?(-[^ \n]+)?( .+|)$/gm, - replace: 'FROM$1 node:<%- version %>$3$4', + test: /^FROM(.*) (public.ecr.aws\/docker\/library\/)?node:[0-9.]+(@sha256:[a-f0-9]{64})?(\.[^- \n]+)?(-[^ \n]+)?( .+|)$/gm, + replace: 'FROM$1 $2node:<%- version %>$3$5$6', }, { files: 'Dockerfile*', @@ -37,13 +70,50 @@ const subPatches: SubPatch[] = [ replace: 'NODEJS_<%- version %>_X', }, { - files: '.buildkite/*', - test: /image: node:[0-9.]+(\.[^- \n]+)?(-[^ \n]+)?$/gm, - replace: 'image: node:<%- version %>$2', + files: '**/.buildkite/*', + test: /image: (public.ecr.aws\/docker\/library\/)?node:[0-9.]+(\.[^- \n]+)?(-[^ \n]+)?$/gm, + replace: 'image: $1node:<%- version %>$3', + }, + { + files: '.node-version*', + test: /(v)?\d+\.\d+\.\d+(.+)?/gm, + replace: '$1<%- version %>$2', + }, + { + files: '**/package.json', + test: /"@types\/node": "(\^)?[0-9.]+"/gm, + replace: '"@types/node": "$1<%- version %>"', + }, + { + files: '**/package.json', + test: /("engines":\s*{[^}]*"node":\s*">=)(\d+)("[^}]*})(?![^}]*"skuba":\s*{[^}]*"type":\s*"package")/gm, + replace: '$1<%- version %>$3', + }, + { + files: '**/tsconfig.json', + test: /("target":\s*")(ES?:[0-9]+|Next|[A-Za-z]+[0-9]*)"/gim, + replace: '$1<%- version %>"', + }, + { + files: '**/docker-compose*.y*ml', + test: /image: (public.ecr.aws\/docker\/library\/)?node:[0-9.]+(\.[^- \n]+)?(-[^ \n]+)?$/gm, + replace: 'image: $1node:<%- version %>$3', }, ]; -const runSubPatch = async (version: number, dir: string, patch: SubPatch) => { +const removeNodeShas = (content: string): string => + content.replace(SHA_REGEX, ''); + +type Versions = { + nodeVersion: number; + nodeTypesVersion: string; +}; + +const runSubPatch = async ( + { nodeVersion, nodeTypesVersion }: Versions, + dir: string, + patch: SubPatch, +) => { const readFile = createDestinationFileReader(dir); const paths = patch.file ? [patch.file] @@ -60,22 +130,44 @@ const runSubPatch = async (version: number, dir: string, patch: SubPatch) => { return; } - const templated = patch.replace.replaceAll( - '<%- version %>', - version.toString(), - ); + const unPinnedContents = removeNodeShas(contents); + + let templated: string; + if ( + path.includes('package.json') && + patch.replace.includes('@types/node') + ) { + templated = patch.replace.replaceAll( + '<%- version %>', + nodeTypesVersion, + ); + } else if (path.includes('tsconfig.json')) { + templated = patch.replace.replaceAll('<%- version %>', 'ES2024'); + } else { + templated = patch.replace.replaceAll( + '<%- version %>', + nodeVersion.toString(), + ); + } await fs.promises.writeFile( path, - patch.test ? contents.replaceAll(patch.test, templated) : templated, + patch.test + ? unPinnedContents.replaceAll(patch.test, templated) + : templated, ); }), ); }; -const upgrade = async (version: number, dir: string) => { +const upgrade = async ( + { nodeVersion, nodeTypesVersion }: Versions, + dir: string, +) => { await Promise.all( - subPatches.map((subPatch) => runSubPatch(version, dir, subPatch)), + subPatches.map((subPatch) => + runSubPatch({ nodeVersion, nodeTypesVersion }, dir, subPatch), + ), ); }; @@ -85,7 +177,11 @@ export const nodeVersionMigration = async ( ) => { log.ok(`Upgrading to Node.js ${version}`); try { - await upgrade(version, dir); + const { version: nodeTypesVersion, err } = await getLatestNodeTypes(); + if (err) { + log.warn(err); + } + await upgrade({ nodeVersion: version, nodeTypesVersion }, dir); log.ok('Upgraded to Node.js', version); } catch (err) { log.err('Failed to upgrade'); From c7be1a50c9d64bffb0a214ffad397b4c5d90dc0e Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Wed, 13 Nov 2024 12:25:38 +1100 Subject: [PATCH 02/32] Use latest node 22 types --- src/cli/migrate/nodeVersion/index.test.ts | 8 ++++---- src/cli/migrate/nodeVersion/index.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/cli/migrate/nodeVersion/index.test.ts b/src/cli/migrate/nodeVersion/index.test.ts index 4801b7a41..4ad5f7e21 100644 --- a/src/cli/migrate/nodeVersion/index.test.ts +++ b/src/cli/migrate/nodeVersion/index.test.ts @@ -1,6 +1,6 @@ import memfs, { vol } from 'memfs'; -import { getLatestNodeTypes, nodeVersionMigration } from '.'; +import { getLatestNode22Types, nodeVersionMigration } from '.'; jest.mock('fs-extra', () => memfs); jest.mock('fast-glob', () => ({ @@ -201,15 +201,15 @@ describe('nodeVersionMigration', () => { ); }); -describe('getLatestNodeTypes', () => { +describe('getLatestNode22Types', () => { it('fetches the latest node types version', async () => { - const { version, err } = await getLatestNodeTypes(); + const { version, err } = await getLatestNode22Types(); expect(version).toBe('22.9.0'); expect(err).toBeUndefined(); }); it('defaults to 22.9.0 if the fetch fails', async () => { jest.spyOn(global, 'fetch').mockImplementation(() => Promise.reject()); - const { version, err } = await getLatestNodeTypes(); + const { version, err } = await getLatestNode22Types(); expect(version).toBe('22.9.0'); expect(err).toBe('Failed to fetch latest version, using fallback version'); diff --git a/src/cli/migrate/nodeVersion/index.ts b/src/cli/migrate/nodeVersion/index.ts index 3d5f7d565..320d7d72c 100644 --- a/src/cli/migrate/nodeVersion/index.ts +++ b/src/cli/migrate/nodeVersion/index.ts @@ -23,7 +23,7 @@ type RegistryResponse = { 'dist-tags': Record; }; -export const getLatestNodeTypes = async (): Promise => { +export const getLatestNode22Types = async (): Promise => { const FALLBACK_VERSION = '22.9.0'; const url = 'https://registry.npmjs.org/@types/node'; const headers = { @@ -34,9 +34,9 @@ export const getLatestNodeTypes = async (): Promise => { try { const fetchResponse = await fetch(url, { headers }); const jsonResponse = (await fetchResponse.json()) as RegistryResponse; - const latest = jsonResponse['dist-tags'].latest; + const latestNode22 = jsonResponse['dist-tags'].node22; - return { version: latest ?? FALLBACK_VERSION, err: undefined }; + return { version: latestNode22 ?? FALLBACK_VERSION, err: undefined }; } catch { return { version: FALLBACK_VERSION, @@ -177,7 +177,7 @@ export const nodeVersionMigration = async ( ) => { log.ok(`Upgrading to Node.js ${version}`); try { - const { version: nodeTypesVersion, err } = await getLatestNodeTypes(); + const { version: nodeTypesVersion, err } = await getLatestNode22Types(); if (err) { log.warn(err); } From ca6678546093da55a7710244b73e5ac872181a40 Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:23:07 +1100 Subject: [PATCH 03/32] Replace fetch with npm show --- src/cli/migrate/nodeVersion/index.test.ts | 21 ++++++-------- src/cli/migrate/nodeVersion/index.ts | 34 +++++++++++------------ 2 files changed, 25 insertions(+), 30 deletions(-) diff --git a/src/cli/migrate/nodeVersion/index.test.ts b/src/cli/migrate/nodeVersion/index.test.ts index 4ad5f7e21..405245d08 100644 --- a/src/cli/migrate/nodeVersion/index.test.ts +++ b/src/cli/migrate/nodeVersion/index.test.ts @@ -9,14 +9,6 @@ jest.mock('fast-glob', () => ({ })); jest.mock('../../../utils/logging'); -jest - .spyOn(global, 'fetch') - .mockImplementation(() => - Promise.resolve( - new Response(JSON.stringify({ 'dist-tags': { latest: '22.9.0' } })), - ), - ); - const volToJson = () => vol.toJSON(process.cwd(), undefined, true); beforeEach(() => vol.reset()); @@ -202,15 +194,18 @@ describe('nodeVersionMigration', () => { }); describe('getLatestNode22Types', () => { - it('fetches the latest node types version', async () => { - const { version, err } = await getLatestNode22Types(); + it('finds the latest node22 types version', () => { + const { version, err } = getLatestNode22Types(); expect(version).toBe('22.9.0'); expect(err).toBeUndefined(); }); - it('defaults to 22.9.0 if the fetch fails', async () => { - jest.spyOn(global, 'fetch').mockImplementation(() => Promise.reject()); - const { version, err } = await getLatestNode22Types(); + it('defaults to 22.9.0 if the exec fails', () => { + // Mock JSON.parse to throw an error + jest.spyOn(JSON, 'parse').mockImplementationOnce(() => { + throw new Error('Failed to fetch latest version'); + }); + const { version, err } = getLatestNode22Types(); expect(version).toBe('22.9.0'); expect(err).toBe('Failed to fetch latest version, using fallback version'); }); diff --git a/src/cli/migrate/nodeVersion/index.ts b/src/cli/migrate/nodeVersion/index.ts index 320d7d72c..df6c024e4 100644 --- a/src/cli/migrate/nodeVersion/index.ts +++ b/src/cli/migrate/nodeVersion/index.ts @@ -1,3 +1,4 @@ +import { execSync } from 'child_process'; import { inspect } from 'util'; import { glob } from 'fast-glob'; @@ -19,24 +20,23 @@ type VersionResult = { err: string | undefined; }; -type RegistryResponse = { - 'dist-tags': Record; -}; - -export const getLatestNode22Types = async (): Promise => { +export const getLatestNode22Types = (): VersionResult => { const FALLBACK_VERSION = '22.9.0'; - const url = 'https://registry.npmjs.org/@types/node'; - const headers = { - accept: - 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*', - }; - try { - const fetchResponse = await fetch(url, { headers }); - const jsonResponse = (await fetchResponse.json()) as RegistryResponse; - const latestNode22 = jsonResponse['dist-tags'].node22; - - return { version: latestNode22 ?? FALLBACK_VERSION, err: undefined }; + const version = ( + JSON.parse( + execSync('npm show @types/node@^22 version --json', { + encoding: 'utf8', + }), + ) as string[] + ).pop(); + if (!version) { + throw new Error('No version found'); + } + return { + version, + err: undefined, + }; } catch { return { version: FALLBACK_VERSION, @@ -177,7 +177,7 @@ export const nodeVersionMigration = async ( ) => { log.ok(`Upgrading to Node.js ${version}`); try { - const { version: nodeTypesVersion, err } = await getLatestNode22Types(); + const { version: nodeTypesVersion, err } = getLatestNode22Types(); if (err) { log.warn(err); } From 14dc05c307661e2e07035a9dc6ba817b2fa7cab9 Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Wed, 13 Nov 2024 18:06:31 +1100 Subject: [PATCH 04/32] Mock execSync --- .../nodeVersion/getNode22TypesVersion.ts | 4 +++ src/cli/migrate/nodeVersion/index.test.ts | 34 ++++++++++++++----- src/cli/migrate/nodeVersion/index.ts | 17 ++++------ 3 files changed, 36 insertions(+), 19 deletions(-) create mode 100644 src/cli/migrate/nodeVersion/getNode22TypesVersion.ts diff --git a/src/cli/migrate/nodeVersion/getNode22TypesVersion.ts b/src/cli/migrate/nodeVersion/getNode22TypesVersion.ts new file mode 100644 index 000000000..49ce7875d --- /dev/null +++ b/src/cli/migrate/nodeVersion/getNode22TypesVersion.ts @@ -0,0 +1,4 @@ +import { execSync } from 'child_process'; + +export const getNode22TypesVersion = () => + execSync("npm show @types/node@^22 version --json | jq '.[-1]'").toString(); diff --git a/src/cli/migrate/nodeVersion/index.test.ts b/src/cli/migrate/nodeVersion/index.test.ts index 405245d08..82c6535b9 100644 --- a/src/cli/migrate/nodeVersion/index.test.ts +++ b/src/cli/migrate/nodeVersion/index.test.ts @@ -1,6 +1,12 @@ import memfs, { vol } from 'memfs'; -import { getLatestNode22Types, nodeVersionMigration } from '.'; +import * as getNode22TypesVersionModule from './getNode22TypesVersion'; + +import { getNode22TypeVersion, nodeVersionMigration } from '.'; + +jest + .spyOn(getNode22TypesVersionModule, 'getNode22TypesVersion') + .mockReturnValue('22.9.0'); jest.mock('fs-extra', () => memfs); jest.mock('fast-glob', () => ({ @@ -13,6 +19,8 @@ const volToJson = () => vol.toJSON(process.cwd(), undefined, true); beforeEach(() => vol.reset()); +afterEach(() => jest.clearAllMocks()); + describe('nodeVersionMigration', () => { const scenarios: Array<{ filesBefore: Record; @@ -193,19 +201,29 @@ describe('nodeVersionMigration', () => { ); }); -describe('getLatestNode22Types', () => { +describe('getNodeTypesVersion', () => { it('finds the latest node22 types version', () => { - const { version, err } = getLatestNode22Types(); + const { version, err } = getNode22TypeVersion(); expect(version).toBe('22.9.0'); expect(err).toBeUndefined(); }); + it('defaults to 22.9.0 if the exec returns an invalid version', () => { + jest + .spyOn(getNode22TypesVersionModule, 'getNode22TypesVersion') + .mockReturnValue('This is not a version'); + const { version, err } = getNode22TypeVersion(); + expect(version).toBe('22.9.0'); + expect(err).toBe('Failed to fetch latest version, using fallback version'); + }); + it('defaults to 22.9.0 if the exec fails', () => { - // Mock JSON.parse to throw an error - jest.spyOn(JSON, 'parse').mockImplementationOnce(() => { - throw new Error('Failed to fetch latest version'); - }); - const { version, err } = getLatestNode22Types(); + jest + .spyOn(getNode22TypesVersionModule, 'getNode22TypesVersion') + .mockReturnValue( + new Error('Failed to fetch latest version') as unknown as string, + ); + const { version, err } = getNode22TypeVersion(); expect(version).toBe('22.9.0'); expect(err).toBe('Failed to fetch latest version, using fallback version'); }); diff --git a/src/cli/migrate/nodeVersion/index.ts b/src/cli/migrate/nodeVersion/index.ts index df6c024e4..b7215b848 100644 --- a/src/cli/migrate/nodeVersion/index.ts +++ b/src/cli/migrate/nodeVersion/index.ts @@ -1,4 +1,3 @@ -import { execSync } from 'child_process'; import { inspect } from 'util'; import { glob } from 'fast-glob'; @@ -7,6 +6,8 @@ import fs from 'fs-extra'; import { log } from '../../../utils/logging'; import { createDestinationFileReader } from '../../configure/analysis/project'; +import { getNode22TypesVersion } from './getNode22TypesVersion'; + type SubPatch = ( | { files: string; file?: never } | { file: string; files?: never } @@ -20,17 +21,11 @@ type VersionResult = { err: string | undefined; }; -export const getLatestNode22Types = (): VersionResult => { +export const getNode22TypeVersion = (): VersionResult => { const FALLBACK_VERSION = '22.9.0'; try { - const version = ( - JSON.parse( - execSync('npm show @types/node@^22 version --json', { - encoding: 'utf8', - }), - ) as string[] - ).pop(); - if (!version) { + const version = getNode22TypesVersion(); + if (!version || !/^22.\d+\.\d+$/.test(version)) { throw new Error('No version found'); } return { @@ -177,7 +172,7 @@ export const nodeVersionMigration = async ( ) => { log.ok(`Upgrading to Node.js ${version}`); try { - const { version: nodeTypesVersion, err } = getLatestNode22Types(); + const { version: nodeTypesVersion, err } = getNode22TypeVersion(); if (err) { log.warn(err); } From 2176a05260b5266a9dcfdb8151b27efc7ba9b434 Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Thu, 14 Nov 2024 11:06:13 +1100 Subject: [PATCH 05/32] Generalise and add back node20 migrate --- src/cli/migrate/index.ts | 1 + .../nodeVersion/getNode22TypesVersion.ts | 6 ++++-- src/cli/migrate/nodeVersion/index.test.ts | 6 +++--- src/cli/migrate/nodeVersion/index.ts | 17 ++++++++++++----- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/cli/migrate/index.ts b/src/cli/migrate/index.ts index 62ac9f33e..938a2cedc 100644 --- a/src/cli/migrate/index.ts +++ b/src/cli/migrate/index.ts @@ -3,6 +3,7 @@ import { log } from '../../utils/logging'; import { nodeVersionMigration } from './nodeVersion'; const migrations: Record Promise> = { + node20: () => nodeVersionMigration(20), node22: () => nodeVersionMigration(22), }; diff --git a/src/cli/migrate/nodeVersion/getNode22TypesVersion.ts b/src/cli/migrate/nodeVersion/getNode22TypesVersion.ts index 49ce7875d..672f67a8d 100644 --- a/src/cli/migrate/nodeVersion/getNode22TypesVersion.ts +++ b/src/cli/migrate/nodeVersion/getNode22TypesVersion.ts @@ -1,4 +1,6 @@ import { execSync } from 'child_process'; -export const getNode22TypesVersion = () => - execSync("npm show @types/node@^22 version --json | jq '.[-1]'").toString(); +export const getNode22TypesVersion = (major: number) => + execSync( + `npm show @types/node@^${major} version --json | jq '.[-1]'`, + ).toString(); diff --git a/src/cli/migrate/nodeVersion/index.test.ts b/src/cli/migrate/nodeVersion/index.test.ts index 82c6535b9..b232a4a4a 100644 --- a/src/cli/migrate/nodeVersion/index.test.ts +++ b/src/cli/migrate/nodeVersion/index.test.ts @@ -203,7 +203,7 @@ describe('nodeVersionMigration', () => { describe('getNodeTypesVersion', () => { it('finds the latest node22 types version', () => { - const { version, err } = getNode22TypeVersion(); + const { version, err } = getNode22TypeVersion(22, '22.9.0'); expect(version).toBe('22.9.0'); expect(err).toBeUndefined(); }); @@ -212,7 +212,7 @@ describe('getNodeTypesVersion', () => { jest .spyOn(getNode22TypesVersionModule, 'getNode22TypesVersion') .mockReturnValue('This is not a version'); - const { version, err } = getNode22TypeVersion(); + const { version, err } = getNode22TypeVersion(22, '22.9.0'); expect(version).toBe('22.9.0'); expect(err).toBe('Failed to fetch latest version, using fallback version'); }); @@ -223,7 +223,7 @@ describe('getNodeTypesVersion', () => { .mockReturnValue( new Error('Failed to fetch latest version') as unknown as string, ); - const { version, err } = getNode22TypeVersion(); + const { version, err } = getNode22TypeVersion(22, '22.9.0'); expect(version).toBe('22.9.0'); expect(err).toBe('Failed to fetch latest version, using fallback version'); }); diff --git a/src/cli/migrate/nodeVersion/index.ts b/src/cli/migrate/nodeVersion/index.ts index b7215b848..941ed6b79 100644 --- a/src/cli/migrate/nodeVersion/index.ts +++ b/src/cli/migrate/nodeVersion/index.ts @@ -8,6 +8,8 @@ import { createDestinationFileReader } from '../../configure/analysis/project'; import { getNode22TypesVersion } from './getNode22TypesVersion'; +const DEFAULT_NODE_TYPES = '22.9.0'; + type SubPatch = ( | { files: string; file?: never } | { file: string; files?: never } @@ -21,10 +23,12 @@ type VersionResult = { err: string | undefined; }; -export const getNode22TypeVersion = (): VersionResult => { - const FALLBACK_VERSION = '22.9.0'; +export const getNode22TypeVersion = ( + major: number, + defaultVersion: string, +): VersionResult => { try { - const version = getNode22TypesVersion(); + const version = getNode22TypesVersion(major); if (!version || !/^22.\d+\.\d+$/.test(version)) { throw new Error('No version found'); } @@ -34,7 +38,7 @@ export const getNode22TypeVersion = (): VersionResult => { }; } catch { return { - version: FALLBACK_VERSION, + version: defaultVersion, err: 'Failed to fetch latest version, using fallback version', }; } @@ -172,7 +176,10 @@ export const nodeVersionMigration = async ( ) => { log.ok(`Upgrading to Node.js ${version}`); try { - const { version: nodeTypesVersion, err } = getNode22TypeVersion(); + const { version: nodeTypesVersion, err } = getNode22TypeVersion( + version, + DEFAULT_NODE_TYPES, + ); if (err) { log.warn(err); } From ec857727d8984e7ee3ae10a1292280d285750c22 Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Tue, 19 Nov 2024 17:38:31 +1100 Subject: [PATCH 06/32] Add checkServerlessVersion --- .../checkServerlessVersion.test.ts | 60 +++++++++++++++++++ .../nodeVersion/checkServerlessVersion.ts | 35 +++++++++++ src/cli/migrate/nodeVersion/index.ts | 2 + 3 files changed, 97 insertions(+) create mode 100644 src/cli/migrate/nodeVersion/checkServerlessVersion.test.ts create mode 100644 src/cli/migrate/nodeVersion/checkServerlessVersion.ts diff --git a/src/cli/migrate/nodeVersion/checkServerlessVersion.test.ts b/src/cli/migrate/nodeVersion/checkServerlessVersion.test.ts new file mode 100644 index 000000000..81f4adbbd --- /dev/null +++ b/src/cli/migrate/nodeVersion/checkServerlessVersion.test.ts @@ -0,0 +1,60 @@ +import findUp from 'find-up'; +import fs from 'fs-extra'; + +jest.mock('find-up'); +jest.mock('fs-extra'); + +import { log } from '../../../utils/logging'; + +import { checkServerlessVersion } from './checkServerlessVersion'; + +jest.spyOn(log, 'warn'); + +describe('checkServerlessVersion', () => { + it('resolves as a noop when serverless version is supported', async () => { + jest.mocked(findUp).mockResolvedValueOnce('package.json'); + jest + .spyOn(fs, 'readFile') + .mockImplementation() + .mockReturnValue( + JSON.stringify({ + devDependencies: { + serverless: '4.0.0', + }, + }) as never, + ); + await checkServerlessVersion(); + expect(log.warn).not.toHaveBeenCalled(); + }); + it('throws when the serverless version is below 4', async () => { + jest.mocked(findUp).mockResolvedValueOnce('package.json'); + jest + .spyOn(fs, 'readFile') + .mockImplementation() + .mockReturnValue( + JSON.stringify({ + devDependencies: { + serverless: '3.0.0', + }, + }) as never, + ); + await expect(checkServerlessVersion()).rejects.toThrow( + 'Serverless version not supported, please upgrade to 4.x', + ); + }); + it('resolves as a noop when serverless version is not found', async () => { + jest.mocked(findUp).mockResolvedValueOnce('package.json'); + jest + .spyOn(fs, 'readFile') + .mockImplementation() + .mockReturnValue(JSON.stringify({}) as never); + await checkServerlessVersion(); + expect(log.warn).not.toHaveBeenCalled(); + }); + it('throws when no package.json is found', async () => { + jest.mocked(findUp).mockResolvedValueOnce(undefined); + await expect(checkServerlessVersion()).rejects.toThrow( + 'package.json not found', + ); + }); +}); diff --git a/src/cli/migrate/nodeVersion/checkServerlessVersion.ts b/src/cli/migrate/nodeVersion/checkServerlessVersion.ts new file mode 100644 index 000000000..d665dfef3 --- /dev/null +++ b/src/cli/migrate/nodeVersion/checkServerlessVersion.ts @@ -0,0 +1,35 @@ +import findUp from 'find-up'; +import fs from 'fs-extra'; + +export const checkServerlessVersion = async () => { + const packageJsonPath = await findUp('package.json', { cwd: process.cwd() }); + if (!packageJsonPath) { + throw new Error('package.json not found'); + } + const packageJson = await fs.readFile(packageJsonPath); + + try { + const serverlessVersion = ( + JSON.parse(packageJson.toString()) as { + devDependencies: Record; + } + ).devDependencies.serverless; + if (!serverlessVersion) { + return; + } + + if (!serverlessVersion.startsWith('4')) { + throw new Error( + 'Serverless version not supported, please upgrade to 4.x', + ); + } + } catch (error) { + if ( + error instanceof TypeError && + error.message.includes('Cannot read properties of undefined') + ) { + return; + } + throw error; + } +}; diff --git a/src/cli/migrate/nodeVersion/index.ts b/src/cli/migrate/nodeVersion/index.ts index 941ed6b79..aa73e0cca 100644 --- a/src/cli/migrate/nodeVersion/index.ts +++ b/src/cli/migrate/nodeVersion/index.ts @@ -6,6 +6,7 @@ import fs from 'fs-extra'; import { log } from '../../../utils/logging'; import { createDestinationFileReader } from '../../configure/analysis/project'; +import { checkServerlessVersion } from './checkServerlessVersion'; import { getNode22TypesVersion } from './getNode22TypesVersion'; const DEFAULT_NODE_TYPES = '22.9.0'; @@ -176,6 +177,7 @@ export const nodeVersionMigration = async ( ) => { log.ok(`Upgrading to Node.js ${version}`); try { + await checkServerlessVersion(); const { version: nodeTypesVersion, err } = getNode22TypeVersion( version, DEFAULT_NODE_TYPES, From 178c02f18616c814e49099423eca2f219540760d Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Wed, 20 Nov 2024 09:41:33 +1100 Subject: [PATCH 07/32] WIP dependent file checks --- src/cli/__snapshots__/format.int.test.ts.snap | 2 +- src/cli/__snapshots__/lint.int.test.ts.snap | 2 +- .../nodeVersion/checkServerlessVersion.ts | 35 ---------- src/cli/migrate/nodeVersion/index.test.ts | 5 ++ src/cli/migrate/nodeVersion/index.ts | 2 +- ...sion.test.ts => packageJsonChecks.test.ts} | 6 +- .../migrate/nodeVersion/packageJsonChecks.ts | 69 +++++++++++++++++++ 7 files changed, 82 insertions(+), 39 deletions(-) delete mode 100644 src/cli/migrate/nodeVersion/checkServerlessVersion.ts rename src/cli/migrate/nodeVersion/{checkServerlessVersion.test.ts => packageJsonChecks.test.ts} (94%) create mode 100644 src/cli/migrate/nodeVersion/packageJsonChecks.ts diff --git a/src/cli/__snapshots__/format.int.test.ts.snap b/src/cli/__snapshots__/format.int.test.ts.snap index d8531dd06..e3d5b949c 100644 --- a/src/cli/__snapshots__/format.int.test.ts.snap +++ b/src/cli/__snapshots__/format.int.test.ts.snap @@ -129,8 +129,8 @@ ESLint Initialising ESLint... Processing files... Processed 2 files in s. -○ a/a/a.ts ○ d.js +○ a/a/a.ts Prettier Initialising Prettier... diff --git a/src/cli/__snapshots__/lint.int.test.ts.snap b/src/cli/__snapshots__/lint.int.test.ts.snap index 33b4d7731..71bebcf60 100644 --- a/src/cli/__snapshots__/lint.int.test.ts.snap +++ b/src/cli/__snapshots__/lint.int.test.ts.snap @@ -101,8 +101,8 @@ exports[`ok --debug 1`] = ` ESLint │ Initialising ESLint... ESLint │ Processing files... ESLint │ Processed 2 files in s. -ESLint │ ○ a/a/a.ts ESLint │ ○ d.js +ESLint │ ○ a/a/a.ts Prettier │ Initialising Prettier... Prettier │ Detected project root: Prettier │ Discovering files... diff --git a/src/cli/migrate/nodeVersion/checkServerlessVersion.ts b/src/cli/migrate/nodeVersion/checkServerlessVersion.ts deleted file mode 100644 index d665dfef3..000000000 --- a/src/cli/migrate/nodeVersion/checkServerlessVersion.ts +++ /dev/null @@ -1,35 +0,0 @@ -import findUp from 'find-up'; -import fs from 'fs-extra'; - -export const checkServerlessVersion = async () => { - const packageJsonPath = await findUp('package.json', { cwd: process.cwd() }); - if (!packageJsonPath) { - throw new Error('package.json not found'); - } - const packageJson = await fs.readFile(packageJsonPath); - - try { - const serverlessVersion = ( - JSON.parse(packageJson.toString()) as { - devDependencies: Record; - } - ).devDependencies.serverless; - if (!serverlessVersion) { - return; - } - - if (!serverlessVersion.startsWith('4')) { - throw new Error( - 'Serverless version not supported, please upgrade to 4.x', - ); - } - } catch (error) { - if ( - error instanceof TypeError && - error.message.includes('Cannot read properties of undefined') - ) { - return; - } - throw error; - } -}; diff --git a/src/cli/migrate/nodeVersion/index.test.ts b/src/cli/migrate/nodeVersion/index.test.ts index b232a4a4a..3f640b88f 100644 --- a/src/cli/migrate/nodeVersion/index.test.ts +++ b/src/cli/migrate/nodeVersion/index.test.ts @@ -1,6 +1,7 @@ import memfs, { vol } from 'memfs'; import * as getNode22TypesVersionModule from './getNode22TypesVersion'; +import * as checkServerlessVersion from './packageJsonChecks'; import { getNode22TypeVersion, nodeVersionMigration } from '.'; @@ -8,6 +9,10 @@ jest .spyOn(getNode22TypesVersionModule, 'getNode22TypesVersion') .mockReturnValue('22.9.0'); +jest + .spyOn(checkServerlessVersion, 'checkServerlessVersion') + .mockResolvedValue(undefined); + jest.mock('fs-extra', () => memfs); jest.mock('fast-glob', () => ({ glob: (pat: any, opts: any) => diff --git a/src/cli/migrate/nodeVersion/index.ts b/src/cli/migrate/nodeVersion/index.ts index aa73e0cca..48dbd4f1d 100644 --- a/src/cli/migrate/nodeVersion/index.ts +++ b/src/cli/migrate/nodeVersion/index.ts @@ -6,8 +6,8 @@ import fs from 'fs-extra'; import { log } from '../../../utils/logging'; import { createDestinationFileReader } from '../../configure/analysis/project'; -import { checkServerlessVersion } from './checkServerlessVersion'; import { getNode22TypesVersion } from './getNode22TypesVersion'; +import { checkServerlessVersion } from './packageJsonChecks'; const DEFAULT_NODE_TYPES = '22.9.0'; diff --git a/src/cli/migrate/nodeVersion/checkServerlessVersion.test.ts b/src/cli/migrate/nodeVersion/packageJsonChecks.test.ts similarity index 94% rename from src/cli/migrate/nodeVersion/checkServerlessVersion.test.ts rename to src/cli/migrate/nodeVersion/packageJsonChecks.test.ts index 81f4adbbd..99a47f2bd 100644 --- a/src/cli/migrate/nodeVersion/checkServerlessVersion.test.ts +++ b/src/cli/migrate/nodeVersion/packageJsonChecks.test.ts @@ -6,10 +6,14 @@ jest.mock('fs-extra'); import { log } from '../../../utils/logging'; -import { checkServerlessVersion } from './checkServerlessVersion'; +import { checkServerlessVersion } from './packageJsonChecks'; jest.spyOn(log, 'warn'); +afterEach(() => { + jest.resetAllMocks(); +}); + describe('checkServerlessVersion', () => { it('resolves as a noop when serverless version is supported', async () => { jest.mocked(findUp).mockResolvedValueOnce('package.json'); diff --git a/src/cli/migrate/nodeVersion/packageJsonChecks.ts b/src/cli/migrate/nodeVersion/packageJsonChecks.ts new file mode 100644 index 000000000..cd4f31eb8 --- /dev/null +++ b/src/cli/migrate/nodeVersion/packageJsonChecks.ts @@ -0,0 +1,69 @@ +import findUp from 'find-up'; +import fs from 'fs-extra'; + +const getParentPackageJson = async () => { + const packageJsonPath = await findUp('package.json', { cwd: process.cwd() }); + if (!packageJsonPath) { + throw new Error('package.json not found'); + } + return fs.readFile(packageJsonPath); +}; + +const isTypeError = (error: unknown): error is TypeError => + error instanceof TypeError && + error.message.includes('Cannot read properties of undefined'); + +const isSyntaxError = (error: unknown): error is SyntaxError => + error instanceof SyntaxError && error.message.includes('Unexpected token'); + +export const checkServerlessVersion = async () => { + const packageJson = await getParentPackageJson(); + + try { + const serverlessVersion = ( + JSON.parse(packageJson.toString()) as { + devDependencies: Record; + } + ).devDependencies.serverless; + if (!serverlessVersion) { + return; + } + + if (!serverlessVersion.startsWith('4')) { + throw new Error( + 'Serverless version not supported, please upgrade to 4.x', + ); + } + } catch (error) { + if (isTypeError(error) || isSyntaxError(error)) { + return; + } + throw error; + } +}; + +export const checkSkubaType = async () => { + const packageJson = await getParentPackageJson(); + + try { + const type = ( + JSON.parse(packageJson.toString()) as { + skuba: Record; + } + ).skuba.type; + if (!type) { + return; + } + + if (type === 'package') { + throw new Error( + 'Skuba type package is not supported, packages should be updated manually to ensure major runtime depreciations are intended', + ); + } + } catch (error) { + if (isTypeError(error) || isSyntaxError(error)) { + return; + } + throw error; + } +}; From 8d2a75b710e00b6f57ab5c46eb160185fa169ef1 Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Wed, 20 Nov 2024 09:46:25 +1100 Subject: [PATCH 08/32] This isn't suspicious at all --- src/cli/__snapshots__/format.int.test.ts.snap | 2 +- src/cli/__snapshots__/lint.int.test.ts.snap | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/__snapshots__/format.int.test.ts.snap b/src/cli/__snapshots__/format.int.test.ts.snap index e3d5b949c..d8531dd06 100644 --- a/src/cli/__snapshots__/format.int.test.ts.snap +++ b/src/cli/__snapshots__/format.int.test.ts.snap @@ -129,8 +129,8 @@ ESLint Initialising ESLint... Processing files... Processed 2 files in s. -○ d.js ○ a/a/a.ts +○ d.js Prettier Initialising Prettier... diff --git a/src/cli/__snapshots__/lint.int.test.ts.snap b/src/cli/__snapshots__/lint.int.test.ts.snap index 71bebcf60..33b4d7731 100644 --- a/src/cli/__snapshots__/lint.int.test.ts.snap +++ b/src/cli/__snapshots__/lint.int.test.ts.snap @@ -101,8 +101,8 @@ exports[`ok --debug 1`] = ` ESLint │ Initialising ESLint... ESLint │ Processing files... ESLint │ Processed 2 files in s. -ESLint │ ○ d.js ESLint │ ○ a/a/a.ts +ESLint │ ○ d.js Prettier │ Initialising Prettier... Prettier │ Detected project root: Prettier │ Discovering files... From 99be3a9ee800e14f656a044a35425a119579975e Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:10:53 +1100 Subject: [PATCH 09/32] Refactor --- src/cli/migrate/index.ts | 6 +- src/cli/migrate/nodeVersion/index.test.ts | 11 +- src/cli/migrate/nodeVersion/index.ts | 121 ++++++++++++------ .../nodeVersion/packageJsonChecks.test.ts | 49 ++++++- 4 files changed, 140 insertions(+), 47 deletions(-) diff --git a/src/cli/migrate/index.ts b/src/cli/migrate/index.ts index 938a2cedc..92d6f0751 100644 --- a/src/cli/migrate/index.ts +++ b/src/cli/migrate/index.ts @@ -3,8 +3,10 @@ import { log } from '../../utils/logging'; import { nodeVersionMigration } from './nodeVersion'; const migrations: Record Promise> = { - node20: () => nodeVersionMigration(20), - node22: () => nodeVersionMigration(22), + node20: () => + nodeVersionMigration({ nodeVersion: 20, ECMAScriptVersion: 'ES2023' }), + node22: () => + nodeVersionMigration({ nodeVersion: 22, ECMAScriptVersion: 'ES2024' }), }; const logAvailableMigrations = () => { diff --git a/src/cli/migrate/nodeVersion/index.test.ts b/src/cli/migrate/nodeVersion/index.test.ts index 3f640b88f..5f1d2d859 100644 --- a/src/cli/migrate/nodeVersion/index.test.ts +++ b/src/cli/migrate/nodeVersion/index.test.ts @@ -1,7 +1,7 @@ import memfs, { vol } from 'memfs'; import * as getNode22TypesVersionModule from './getNode22TypesVersion'; -import * as checkServerlessVersion from './packageJsonChecks'; +import * as packageJsonChecks from './packageJsonChecks'; import { getNode22TypeVersion, nodeVersionMigration } from '.'; @@ -10,9 +10,11 @@ jest .mockReturnValue('22.9.0'); jest - .spyOn(checkServerlessVersion, 'checkServerlessVersion') + .spyOn(packageJsonChecks, 'checkServerlessVersion') .mockResolvedValue(undefined); +jest.spyOn(packageJsonChecks, 'checkSkubaType').mockResolvedValue(undefined); + jest.mock('fs-extra', () => memfs); jest.mock('fast-glob', () => ({ glob: (pat: any, opts: any) => @@ -199,7 +201,10 @@ describe('nodeVersionMigration', () => { async ({ filesBefore, filesAfter }) => { vol.fromJSON(filesBefore, process.cwd()); - await nodeVersionMigration(22); + await nodeVersionMigration({ + nodeVersion: 22, + ECMAScriptVersion: 'ES2024', + }); expect(volToJson()).toEqual(filesAfter ?? filesBefore); }, diff --git a/src/cli/migrate/nodeVersion/index.ts b/src/cli/migrate/nodeVersion/index.ts index 48dbd4f1d..8f86b266c 100644 --- a/src/cli/migrate/nodeVersion/index.ts +++ b/src/cli/migrate/nodeVersion/index.ts @@ -7,7 +7,7 @@ import { log } from '../../../utils/logging'; import { createDestinationFileReader } from '../../configure/analysis/project'; import { getNode22TypesVersion } from './getNode22TypesVersion'; -import { checkServerlessVersion } from './packageJsonChecks'; +import { checkServerlessVersion, checkSkubaType } from './packageJsonChecks'; const DEFAULT_NODE_TYPES = '22.9.0'; @@ -17,6 +17,7 @@ type SubPatch = ( ) & { test?: RegExp; replace: string; + id: string; }; type VersionResult = { @@ -48,56 +49,66 @@ export const getNode22TypeVersion = ( const SHA_REGEX = /(?<=node.*)(@sha256:[a-f0-9]{64})/gm; const subPatches: SubPatch[] = [ - { file: '.nvmrc', replace: '<%- version %>\n' }, + { id: 'nvmrc', file: '.nvmrc', replace: '<%- version %>\n' }, { + id: 'Dockerfile-1', files: 'Dockerfile*', test: /^FROM(.*) (public.ecr.aws\/docker\/library\/)?node:[0-9.]+(@sha256:[a-f0-9]{64})?(\.[^- \n]+)?(-[^ \n]+)?( .+|)$/gm, replace: 'FROM$1 $2node:<%- version %>$3$5$6', }, { + id: 'Dockerfile-2', files: 'Dockerfile*', test: /^FROM(.*) gcr.io\/distroless\/nodejs\d+-debian(.+)$/gm, replace: 'FROM$1 gcr.io/distroless/nodejs<%- version %>-debian$2', }, { + id: 'serverless', files: 'serverless*.y*ml', test: /nodejs\d+.x/gm, replace: 'nodejs<%- version %>.x', }, { + id: 'cdk', files: 'infra/**/*.ts', test: /NODEJS_\d+_X/g, replace: 'NODEJS_<%- version %>_X', }, { + id: 'buildkite', files: '**/.buildkite/*', - test: /image: (public.ecr.aws\/docker\/library\/)?node:[0-9.]+(\.[^- \n]+)?(-[^ \n]+)?$/gm, - replace: 'image: $1node:<%- version %>$3', + test: /(image: )(public.ecr.aws\/docker\/library\/)?(node:)[0-9.]+(\.[^- \n]+)?(-[^ \n]+)?$/gm, + replace: '$1$2$3<%- version %>$5', }, { + id: 'node-version', files: '.node-version*', test: /(v)?\d+\.\d+\.\d+(.+)?/gm, replace: '$1<%- version %>$2', }, { + id: 'package-json-1', files: '**/package.json', - test: /"@types\/node": "(\^)?[0-9.]+"/gm, - replace: '"@types/node": "$1<%- version %>"', + test: /("@types\/node": ")(\^)?[0-9.]+"/gm, + replace: '$1$2<%- version %>"', }, { + id: 'package-json-2', files: '**/package.json', test: /("engines":\s*{[^}]*"node":\s*">=)(\d+)("[^}]*})(?![^}]*"skuba":\s*{[^}]*"type":\s*"package")/gm, replace: '$1<%- version %>$3', }, { + id: 'tsconfig', files: '**/tsconfig.json', test: /("target":\s*")(ES?:[0-9]+|Next|[A-Za-z]+[0-9]*)"/gim, replace: '$1<%- version %>"', }, { + id: 'docker-compose', files: '**/docker-compose*.y*ml', - test: /image: (public.ecr.aws\/docker\/library\/)?node:[0-9.]+(\.[^- \n]+)?(-[^ \n]+)?$/gm, - replace: 'image: $1node:<%- version %>$3', + test: /(image: )(public.ecr.aws\/docker\/library\/)?(node:)[0-9.]+(\.[^- \n]+)?(-[^ \n]+)?$/gm, + replace: '$1$2$3<%- version %>$5', }, ]; @@ -107,10 +118,11 @@ const removeNodeShas = (content: string): string => type Versions = { nodeVersion: number; nodeTypesVersion: string; + ECMAScriptVersion: string; }; const runSubPatch = async ( - { nodeVersion, nodeTypesVersion }: Versions, + { nodeVersion, nodeTypesVersion, ECMAScriptVersion }: Versions, dir: string, patch: SubPatch, ) => { @@ -132,61 +144,96 @@ const runSubPatch = async ( const unPinnedContents = removeNodeShas(contents); - let templated: string; - if ( - path.includes('package.json') && - patch.replace.includes('@types/node') - ) { - templated = patch.replace.replaceAll( - '<%- version %>', - nodeTypesVersion, - ); - } else if (path.includes('tsconfig.json')) { - templated = patch.replace.replaceAll('<%- version %>', 'ES2024'); - } else { - templated = patch.replace.replaceAll( - '<%- version %>', - nodeVersion.toString(), - ); + if (patch.id === 'package-json-1') { + return await writePatchedContents({ + path, + contents: unPinnedContents, + templated: patch.replace.replaceAll( + '<%- version %>', + nodeTypesVersion, + ), + test: patch.test, + }); + } + if (patch.id === 'tsconfig') { + return await writePatchedContents({ + path, + contents: unPinnedContents, + templated: patch.replace.replaceAll( + '<%- version %>', + ECMAScriptVersion, + ), + test: patch.test, + }); } - await fs.promises.writeFile( + if (patch.id === 'package-json-2') { + await checkSkubaType(); + } + + await writePatchedContents({ path, - patch.test - ? unPinnedContents.replaceAll(patch.test, templated) - : templated, - ); + contents: unPinnedContents, + templated: patch.replace.replaceAll( + '<%- version %>', + nodeVersion.toString(), + ), + test: patch.test, + }); }), ); }; +const writePatchedContents = async ({ + path, + contents, + templated, + test, +}: { + path: string; + contents: string; + templated: string; + test?: RegExp; +}) => + await fs.promises.writeFile( + path, + test ? contents.replaceAll(test, templated) : templated, + ); + const upgrade = async ( - { nodeVersion, nodeTypesVersion }: Versions, + { nodeVersion, nodeTypesVersion, ECMAScriptVersion }: Versions, dir: string, ) => { await Promise.all( subPatches.map((subPatch) => - runSubPatch({ nodeVersion, nodeTypesVersion }, dir, subPatch), + runSubPatch( + { nodeVersion, nodeTypesVersion, ECMAScriptVersion }, + dir, + subPatch, + ), ), ); }; export const nodeVersionMigration = async ( - version: number, + { + nodeVersion, + ECMAScriptVersion, + }: { nodeVersion: number; ECMAScriptVersion: string }, dir = process.cwd(), ) => { - log.ok(`Upgrading to Node.js ${version}`); + log.ok(`Upgrading to Node.js ${nodeVersion}`); try { await checkServerlessVersion(); const { version: nodeTypesVersion, err } = getNode22TypeVersion( - version, + nodeVersion, DEFAULT_NODE_TYPES, ); if (err) { log.warn(err); } - await upgrade({ nodeVersion: version, nodeTypesVersion }, dir); - log.ok('Upgraded to Node.js', version); + await upgrade({ nodeVersion, nodeTypesVersion, ECMAScriptVersion }, dir); + log.ok('Upgraded to Node.js', nodeVersion); } catch (err) { log.err('Failed to upgrade'); log.subtle(inspect(err)); diff --git a/src/cli/migrate/nodeVersion/packageJsonChecks.test.ts b/src/cli/migrate/nodeVersion/packageJsonChecks.test.ts index 99a47f2bd..3064ed845 100644 --- a/src/cli/migrate/nodeVersion/packageJsonChecks.test.ts +++ b/src/cli/migrate/nodeVersion/packageJsonChecks.test.ts @@ -6,7 +6,7 @@ jest.mock('fs-extra'); import { log } from '../../../utils/logging'; -import { checkServerlessVersion } from './packageJsonChecks'; +import { checkServerlessVersion, checkSkubaType } from './packageJsonChecks'; jest.spyOn(log, 'warn'); @@ -27,8 +27,7 @@ describe('checkServerlessVersion', () => { }, }) as never, ); - await checkServerlessVersion(); - expect(log.warn).not.toHaveBeenCalled(); + await expect(checkServerlessVersion()).resolves.toBeUndefined(); }); it('throws when the serverless version is below 4', async () => { jest.mocked(findUp).mockResolvedValueOnce('package.json'); @@ -52,8 +51,7 @@ describe('checkServerlessVersion', () => { .spyOn(fs, 'readFile') .mockImplementation() .mockReturnValue(JSON.stringify({}) as never); - await checkServerlessVersion(); - expect(log.warn).not.toHaveBeenCalled(); + await expect(checkServerlessVersion()).resolves.toBeUndefined(); }); it('throws when no package.json is found', async () => { jest.mocked(findUp).mockResolvedValueOnce(undefined); @@ -62,3 +60,44 @@ describe('checkServerlessVersion', () => { ); }); }); + +describe('checkSkubaType', () => { + it('should return undefined when skuba type is not "package"', async () => { + jest.mocked(findUp).mockResolvedValueOnce('package.json'); + jest + .spyOn(fs, 'readFile') + .mockImplementation() + .mockReturnValue( + JSON.stringify({ + skuba: { + type: 'application', + }, + }) as never, + ); + await expect(checkSkubaType()).resolves.toBeUndefined(); + }); + it('should throw when skuba type is "package"', async () => { + jest.mocked(findUp).mockResolvedValueOnce('package.json'); + jest + .spyOn(fs, 'readFile') + .mockImplementation() + .mockReturnValue( + JSON.stringify({ + skuba: { + type: 'package', + }, + }) as never, + ); + await expect(checkSkubaType()).rejects.toThrow( + 'Skuba type package is not supported, packages should be updated manually to ensure major runtime depreciations are intended', + ); + }); + it('should return undefined when skuba type is not found', async () => { + jest.mocked(findUp).mockResolvedValueOnce('package.json'); + jest + .spyOn(fs, 'readFile') + .mockImplementation() + .mockReturnValue(JSON.stringify({}) as never); + await expect(checkSkubaType()).resolves.toBeUndefined(); + }); +}); From b165e280a28deb770907db9d709678105bdda0dc Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Tue, 26 Nov 2024 11:26:53 +1100 Subject: [PATCH 10/32] WIP --- .../upgrade/patches/10.0.0/index.ts | 10 ++++ .../upgrade/patches/10.0.0/upgradeNode.ts | 31 +++++++++++ src/cli/migrate/nodeVersion/index.ts | 54 +++++++++++++------ 3 files changed, 80 insertions(+), 15 deletions(-) create mode 100644 src/cli/lint/internalLints/upgrade/patches/10.0.0/index.ts create mode 100644 src/cli/lint/internalLints/upgrade/patches/10.0.0/upgradeNode.ts diff --git a/src/cli/lint/internalLints/upgrade/patches/10.0.0/index.ts b/src/cli/lint/internalLints/upgrade/patches/10.0.0/index.ts new file mode 100644 index 000000000..96745e832 --- /dev/null +++ b/src/cli/lint/internalLints/upgrade/patches/10.0.0/index.ts @@ -0,0 +1,10 @@ +import type { Patches } from '../..'; + +import { tryUpgradeNode } from './upgradeNode'; + +export const patches: Patches = [ + { + apply: tryUpgradeNode, + description: 'TODO', + }, +]; diff --git a/src/cli/lint/internalLints/upgrade/patches/10.0.0/upgradeNode.ts b/src/cli/lint/internalLints/upgrade/patches/10.0.0/upgradeNode.ts new file mode 100644 index 000000000..c42ae6da5 --- /dev/null +++ b/src/cli/lint/internalLints/upgrade/patches/10.0.0/upgradeNode.ts @@ -0,0 +1,31 @@ +import { inspect } from 'util'; + +import { Env } from 'skuba-dive'; + +import type { PatchFunction, PatchReturnType } from '../..'; +import { log } from '../../../../../../utils/logging'; +import { nodeVersionMigration } from '../../../../../migrate/nodeVersion'; + +const upgradeNode: PatchFunction = async ({ + mode, +}): Promise => { + if (mode === 'lint' || Env.string('SKIP_NODE_UPGRADE') === 'true') { + return { + result: 'apply', + }; + } + + await nodeVersionMigration({ nodeVersion: 22, ECMAScriptVersion: 'ES2024' }); + + return { result: 'apply' }; +}; + +export const tryUpgradeNode: PatchFunction = async (config) => { + try { + return await upgradeNode(config); + } catch (err) { + log.warn('Failed to patch Docker images'); + log.subtle(inspect(err)); + return { result: 'skip', reason: 'due to an error' }; + } +}; diff --git a/src/cli/migrate/nodeVersion/index.ts b/src/cli/migrate/nodeVersion/index.ts index 8f86b266c..a9d13a84c 100644 --- a/src/cli/migrate/nodeVersion/index.ts +++ b/src/cli/migrate/nodeVersion/index.ts @@ -11,14 +11,19 @@ import { checkServerlessVersion, checkSkubaType } from './packageJsonChecks'; const DEFAULT_NODE_TYPES = '22.9.0'; -type SubPatch = ( - | { files: string; file?: never } - | { file: string; files?: never } -) & { - test?: RegExp; - replace: string; - id: string; -}; +type SubPatch = + | (({ files: string; file?: never } | { file: string; files?: never }) & { + test?: RegExp; + replace: string; + id: string; + }) + | Array< + ({ files: string; file?: never } | { file: string; files?: never }) & { + test?: RegExp; + replace: string; + id: string; + } + >; type VersionResult = { version: string; @@ -68,12 +73,20 @@ const subPatches: SubPatch[] = [ test: /nodejs\d+.x/gm, replace: 'nodejs<%- version %>.x', }, - { - id: 'cdk', - files: 'infra/**/*.ts', - test: /NODEJS_\d+_X/g, - replace: 'NODEJS_<%- version %>_X', - }, + [ + { + id: 'cdk-1', + files: 'infra/**/*.ts', + test: /NODEJS_\d+_X/g, + replace: 'NODEJS_<%- version %>_X', + }, + { + id: 'cdk-2', + files: 'infra/**/*.ts', + test: /(target:\s*'node)(\d+)(.+)$/gm, + replace: '$1<%- version %>$3', + }, + ], { id: 'buildkite', files: '**/.buildkite/*', @@ -95,7 +108,7 @@ const subPatches: SubPatch[] = [ { id: 'package-json-2', files: '**/package.json', - test: /("engines":\s*{[^}]*"node":\s*">=)(\d+)("[^}]*})(?![^}]*"skuba":\s*{[^}]*"type":\s*"package")/gm, + test: /("engines":\s*{[^}]*"node":\s*">=)(\d+)("[^}]*})/gm, replace: '$1<%- version %>$3', }, { @@ -126,6 +139,16 @@ const runSubPatch = async ( dir: string, patch: SubPatch, ) => { + if (Array.isArray(patch)) { + for (const subPatch of patch) { + await runSubPatch( + { nodeVersion, nodeTypesVersion, ECMAScriptVersion }, + dir, + subPatch, + ); + } + return; + } const readFile = createDestinationFileReader(dir); const paths = patch.file ? [patch.file] @@ -156,6 +179,7 @@ const runSubPatch = async ( }); } if (patch.id === 'tsconfig') { + await checkSkubaType(); return await writePatchedContents({ path, contents: unPinnedContents, From bf26aa1a39a11ed34f70516edfc41e905d24744f Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:09:18 +1100 Subject: [PATCH 11/32] Fix --- src/cli/migrate/nodeVersion/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/migrate/nodeVersion/index.ts b/src/cli/migrate/nodeVersion/index.ts index a9d13a84c..d165a67c8 100644 --- a/src/cli/migrate/nodeVersion/index.ts +++ b/src/cli/migrate/nodeVersion/index.ts @@ -108,7 +108,7 @@ const subPatches: SubPatch[] = [ { id: 'package-json-2', files: '**/package.json', - test: /("engines":\s*{[^}]*"node":\s*">=)(\d+)("[^}]*})/gm, + test: /("engines":\s*{[^}]*"node":\s*">=)(\d+)("[^}]*})(?![^}]*"skuba":\s*{[^}]*"type":\s*"package")/gm, replace: '$1<%- version %>$3', }, { From 9c55f4d73b8b191dcdaa361f2cd1d686357b7fa3 Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:40:47 +1100 Subject: [PATCH 12/32] Fix --- src/cli/__snapshots__/format.int.test.ts.snap | 20 +++++++++++++++++++ .../upgrade/patches/10.0.0/index.ts | 2 +- .../upgrade/patches/10.0.0/upgradeNode.ts | 6 ++---- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/cli/__snapshots__/format.int.test.ts.snap b/src/cli/__snapshots__/format.int.test.ts.snap index d8531dd06..bc9d06ccf 100644 --- a/src/cli/__snapshots__/format.int.test.ts.snap +++ b/src/cli/__snapshots__/format.int.test.ts.snap @@ -30,6 +30,11 @@ Patch skipped: Update docker image references to use public.ecr.aws and remove - Patch skipped: Move .npmrc mounts from tmp/.npmrc to /tmp/.npmrc - no Buildkite files found Patch skipped: Use pinned pnpm version in Dockerfiles - no Dockerfiles found +Upgrading to Node.js 22 +Failed to fetch latest version, using fallback version +Upgraded to Node.js 22 + +Patch applied: Upgrades Node.js to version 22 skuba update complete. @@ -116,6 +121,11 @@ Patch skipped: Update docker image references to use public.ecr.aws and remove - Patch skipped: Move .npmrc mounts from tmp/.npmrc to /tmp/.npmrc - no Buildkite files found Patch skipped: Use pinned pnpm version in Dockerfiles - no Dockerfiles found +Upgrading to Node.js 22 +Failed to fetch latest version, using fallback version +Upgraded to Node.js 22 + +Patch applied: Upgrades Node.js to version 22 skuba update complete. @@ -197,6 +207,11 @@ Patch skipped: Update docker image references to use public.ecr.aws and remove - Patch skipped: Move .npmrc mounts from tmp/.npmrc to /tmp/.npmrc - no Buildkite files found Patch skipped: Use pinned pnpm version in Dockerfiles - no Dockerfiles found +Upgrading to Node.js 22 +Failed to fetch latest version, using fallback version +Upgraded to Node.js 22 + +Patch applied: Upgrades Node.js to version 22 skuba update complete. @@ -249,6 +264,11 @@ Patch skipped: Update docker image references to use public.ecr.aws and remove - Patch skipped: Move .npmrc mounts from tmp/.npmrc to /tmp/.npmrc - no Buildkite files found Patch skipped: Use pinned pnpm version in Dockerfiles - no Dockerfiles found +Upgrading to Node.js 22 +Failed to fetch latest version, using fallback version +Upgraded to Node.js 22 + +Patch applied: Upgrades Node.js to version 22 skuba update complete. diff --git a/src/cli/lint/internalLints/upgrade/patches/10.0.0/index.ts b/src/cli/lint/internalLints/upgrade/patches/10.0.0/index.ts index 96745e832..406487ba3 100644 --- a/src/cli/lint/internalLints/upgrade/patches/10.0.0/index.ts +++ b/src/cli/lint/internalLints/upgrade/patches/10.0.0/index.ts @@ -5,6 +5,6 @@ import { tryUpgradeNode } from './upgradeNode'; export const patches: Patches = [ { apply: tryUpgradeNode, - description: 'TODO', + description: 'Upgrades Node.js to version 22', }, ]; diff --git a/src/cli/lint/internalLints/upgrade/patches/10.0.0/upgradeNode.ts b/src/cli/lint/internalLints/upgrade/patches/10.0.0/upgradeNode.ts index c42ae6da5..7be9bdaae 100644 --- a/src/cli/lint/internalLints/upgrade/patches/10.0.0/upgradeNode.ts +++ b/src/cli/lint/internalLints/upgrade/patches/10.0.0/upgradeNode.ts @@ -1,7 +1,5 @@ import { inspect } from 'util'; -import { Env } from 'skuba-dive'; - import type { PatchFunction, PatchReturnType } from '../..'; import { log } from '../../../../../../utils/logging'; import { nodeVersionMigration } from '../../../../../migrate/nodeVersion'; @@ -9,9 +7,9 @@ import { nodeVersionMigration } from '../../../../../migrate/nodeVersion'; const upgradeNode: PatchFunction = async ({ mode, }): Promise => { - if (mode === 'lint' || Env.string('SKIP_NODE_UPGRADE') === 'true') { + if (mode === 'lint' || process.env.SKIP_NODE_UPGRADE) { return { - result: 'apply', + result: 'skip', }; } From 659fe97de6d66afcf833ec86d4062247ede71150 Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:48:34 +1100 Subject: [PATCH 13/32] Init changeset --- .changeset/nasty-masks-eat.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/nasty-masks-eat.md diff --git a/.changeset/nasty-masks-eat.md b/.changeset/nasty-masks-eat.md new file mode 100644 index 000000000..85d8d0447 --- /dev/null +++ b/.changeset/nasty-masks-eat.md @@ -0,0 +1,5 @@ +--- +'skuba': major +--- + +TODO From 1eb04945020e6b907474a35cff954cba866d70b9 Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:56:03 +1100 Subject: [PATCH 14/32] changeset scope --- .changeset/nasty-masks-eat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/nasty-masks-eat.md b/.changeset/nasty-masks-eat.md index 85d8d0447..1a7b1f75d 100644 --- a/.changeset/nasty-masks-eat.md +++ b/.changeset/nasty-masks-eat.md @@ -2,4 +2,4 @@ 'skuba': major --- -TODO +lint: TODO From 3e8d9573bf218569b76a2e0be0d4f383b3986acd Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Tue, 14 Jan 2025 16:54:53 +1100 Subject: [PATCH 15/32] Fixes --- src/cli/migrate/nodeVersion/index.test.ts | 4 ++-- src/cli/migrate/nodeVersion/index.ts | 26 ++++++++++++----------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/cli/migrate/nodeVersion/index.test.ts b/src/cli/migrate/nodeVersion/index.test.ts index 5f1d2d859..502a179cf 100644 --- a/src/cli/migrate/nodeVersion/index.test.ts +++ b/src/cli/migrate/nodeVersion/index.test.ts @@ -154,14 +154,14 @@ describe('nodeVersionMigration', () => { { scenario: 'node types', filesBefore: { - 'package.json': '"@types/node": "^14.0.0"', + 'package.json': '"@types/node": "^14.0.0",', '1/package.json': '"@types/node": "18.0.0"', '2/package.json': `"engines": {\n"node": ">=18"\n},\n`, '3/package.json': `"engines": {\n"node": ">=18"\n},\n"skuba": {\n"type": "package"\n}`, '4/package.json': `"engines": {\n"node": ">=18"\n},\n"skuba": {\n"type": "application"\n}`, }, filesAfter: { - 'package.json': '"@types/node": "^22.9.0"', + 'package.json': '"@types/node": "^22.9.0",', '1/package.json': '"@types/node": "22.9.0"', '2/package.json': `"engines": {\n"node": ">=22"\n},\n`, '3/package.json': `"engines": {\n"node": ">=18"\n},\n"skuba": {\n"type": "package"\n}`, diff --git a/src/cli/migrate/nodeVersion/index.ts b/src/cli/migrate/nodeVersion/index.ts index d165a67c8..014ff4d32 100644 --- a/src/cli/migrate/nodeVersion/index.ts +++ b/src/cli/migrate/nodeVersion/index.ts @@ -99,18 +99,20 @@ const subPatches: SubPatch[] = [ test: /(v)?\d+\.\d+\.\d+(.+)?/gm, replace: '$1<%- version %>$2', }, - { - id: 'package-json-1', - files: '**/package.json', - test: /("@types\/node": ")(\^)?[0-9.]+"/gm, - replace: '$1$2<%- version %>"', - }, - { - id: 'package-json-2', - files: '**/package.json', - test: /("engines":\s*{[^}]*"node":\s*">=)(\d+)("[^}]*})(?![^}]*"skuba":\s*{[^}]*"type":\s*"package")/gm, - replace: '$1<%- version %>$3', - }, + [ + { + id: 'package-json-1', + files: '**/package.json', + test: /(\\?"@types\/node\\?": \\?")(\^)?[0-9.]+(\\?(",?)\\?n?)/gm, + replace: '$1$2<%- version %>$4', + }, + { + id: 'package-json-2', + files: '**/package.json', + test: /(\\?"engines\\?":\s*{\\?n?[^}]*\\?"node\\?":\s*\\?">=)(\d+)\\?("[^}]*})(?![^}]*\\?"skuba\\?":\s*{\\?n?[^}]*\\?"type\\?":\s*\\?"package\\?")/gm, + replace: '$1<%- version %>$3', + }, + ], { id: 'tsconfig', files: '**/tsconfig.json', From c66aa140b72225f5f8a084fb4a1f7e9fec7c8db6 Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:34:44 +1100 Subject: [PATCH 16/32] Fixes --- src/cli/migrate/nodeVersion/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/cli/migrate/nodeVersion/index.ts b/src/cli/migrate/nodeVersion/index.ts index 014ff4d32..57e494def 100644 --- a/src/cli/migrate/nodeVersion/index.ts +++ b/src/cli/migrate/nodeVersion/index.ts @@ -36,7 +36,7 @@ export const getNode22TypeVersion = ( ): VersionResult => { try { const version = getNode22TypesVersion(major); - if (!version || !/^22.\d+\.\d+$/.test(version)) { + if (!version || !/22\.\d+\.\d+/.test(version)) { throw new Error('No version found'); } return { @@ -158,6 +158,9 @@ const runSubPatch = async ( await Promise.all( paths.map(async (path) => { + if (path.includes('node_modules')) { + return; + } const contents = await readFile(path); if (!contents) { return; From 88e54f7b9aab3ab0b8ab470af4b89e7b6433c51e Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:58:59 +1100 Subject: [PATCH 17/32] Remove " from node types version --- src/cli/migrate/nodeVersion/getNode22TypesVersion.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli/migrate/nodeVersion/getNode22TypesVersion.ts b/src/cli/migrate/nodeVersion/getNode22TypesVersion.ts index 672f67a8d..422a13154 100644 --- a/src/cli/migrate/nodeVersion/getNode22TypesVersion.ts +++ b/src/cli/migrate/nodeVersion/getNode22TypesVersion.ts @@ -1,6 +1,6 @@ import { execSync } from 'child_process'; export const getNode22TypesVersion = (major: number) => - execSync( - `npm show @types/node@^${major} version --json | jq '.[-1]'`, - ).toString(); + execSync(`npm show @types/node@^${major} version --json | jq '.[-1]'`) + .toString() + .replace(/"/g, ''); From 6f543cf48dbc7025f86134a44460b0789813d8f7 Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:02:27 +1100 Subject: [PATCH 18/32] Snapshots and mock --- src/cli/__snapshots__/format.int.test.ts.snap | 4 ---- src/cli/format.int.test.ts | 5 +++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/cli/__snapshots__/format.int.test.ts.snap b/src/cli/__snapshots__/format.int.test.ts.snap index bc9d06ccf..0f467c90f 100644 --- a/src/cli/__snapshots__/format.int.test.ts.snap +++ b/src/cli/__snapshots__/format.int.test.ts.snap @@ -31,7 +31,6 @@ Patch skipped: Move .npmrc mounts from tmp/.npmrc to /tmp/.npmrc - no Buildkite Patch skipped: Use pinned pnpm version in Dockerfiles - no Dockerfiles found Upgrading to Node.js 22 -Failed to fetch latest version, using fallback version Upgraded to Node.js 22 Patch applied: Upgrades Node.js to version 22 @@ -122,7 +121,6 @@ Patch skipped: Move .npmrc mounts from tmp/.npmrc to /tmp/.npmrc - no Buildkite Patch skipped: Use pinned pnpm version in Dockerfiles - no Dockerfiles found Upgrading to Node.js 22 -Failed to fetch latest version, using fallback version Upgraded to Node.js 22 Patch applied: Upgrades Node.js to version 22 @@ -208,7 +206,6 @@ Patch skipped: Move .npmrc mounts from tmp/.npmrc to /tmp/.npmrc - no Buildkite Patch skipped: Use pinned pnpm version in Dockerfiles - no Dockerfiles found Upgrading to Node.js 22 -Failed to fetch latest version, using fallback version Upgraded to Node.js 22 Patch applied: Upgrades Node.js to version 22 @@ -265,7 +262,6 @@ Patch skipped: Move .npmrc mounts from tmp/.npmrc to /tmp/.npmrc - no Buildkite Patch skipped: Use pinned pnpm version in Dockerfiles - no Dockerfiles found Upgrading to Node.js 22 -Failed to fetch latest version, using fallback version Upgraded to Node.js 22 Patch applied: Upgrades Node.js to version 22 diff --git a/src/cli/format.int.test.ts b/src/cli/format.int.test.ts index 9b17bfbba..2ec58c822 100644 --- a/src/cli/format.int.test.ts +++ b/src/cli/format.int.test.ts @@ -6,6 +6,7 @@ import git from 'isomorphic-git'; import { diff } from 'jest-diff'; import { format } from './format'; +import * as getNode22TypesVersionModule from './migrate/nodeVersion/getNode22TypesVersion'; jest.setTimeout(15_000); @@ -15,6 +16,10 @@ jest .spyOn(console, 'log') .mockImplementation((...args) => stdoutMock(`${args.join(' ')}\n`)); +jest + .spyOn(getNode22TypesVersionModule, 'getNode22TypesVersion') + .mockReturnValue('22.9.0'); + jest .spyOn(git, 'listRemotes') .mockResolvedValue([ From 0efdaf0dcba9ba5c1422adcb2172242c83e1655f Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:34:17 +1100 Subject: [PATCH 19/32] Sanitize queried node types version --- src/cli/migrate/nodeVersion/getNode22TypesVersion.ts | 6 +++--- src/cli/migrate/nodeVersion/index.ts | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/cli/migrate/nodeVersion/getNode22TypesVersion.ts b/src/cli/migrate/nodeVersion/getNode22TypesVersion.ts index 422a13154..672f67a8d 100644 --- a/src/cli/migrate/nodeVersion/getNode22TypesVersion.ts +++ b/src/cli/migrate/nodeVersion/getNode22TypesVersion.ts @@ -1,6 +1,6 @@ import { execSync } from 'child_process'; export const getNode22TypesVersion = (major: number) => - execSync(`npm show @types/node@^${major} version --json | jq '.[-1]'`) - .toString() - .replace(/"/g, ''); + execSync( + `npm show @types/node@^${major} version --json | jq '.[-1]'`, + ).toString(); diff --git a/src/cli/migrate/nodeVersion/index.ts b/src/cli/migrate/nodeVersion/index.ts index 57e494def..c82fe9232 100644 --- a/src/cli/migrate/nodeVersion/index.ts +++ b/src/cli/migrate/nodeVersion/index.ts @@ -36,11 +36,12 @@ export const getNode22TypeVersion = ( ): VersionResult => { try { const version = getNode22TypesVersion(major); - if (!version || !/22\.\d+\.\d+/.test(version)) { + const versionRegex = /(22\.\d+\.\d+)/; + if (!version || !versionRegex.test(version)) { throw new Error('No version found'); } return { - version, + version: version.replace(versionRegex, '$1'), err: undefined, }; } catch { From c7711215ba9196d7e87e0c180a86e5aa02bfab47 Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:43:36 +1100 Subject: [PATCH 20/32] Well... I don't like this --- src/cli/migrate/nodeVersion/index.test.ts | 5 ++++- src/cli/migrate/nodeVersion/index.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/cli/migrate/nodeVersion/index.test.ts b/src/cli/migrate/nodeVersion/index.test.ts index 502a179cf..33ce1647c 100644 --- a/src/cli/migrate/nodeVersion/index.test.ts +++ b/src/cli/migrate/nodeVersion/index.test.ts @@ -213,8 +213,11 @@ describe('nodeVersionMigration', () => { describe('getNodeTypesVersion', () => { it('finds the latest node22 types version', () => { + jest + .spyOn(getNode22TypesVersionModule, 'getNode22TypesVersion') + .mockReturnValue('"22.10.6"'); const { version, err } = getNode22TypeVersion(22, '22.9.0'); - expect(version).toBe('22.9.0'); + expect(version).toBe('22.10.6'); expect(err).toBeUndefined(); }); diff --git a/src/cli/migrate/nodeVersion/index.ts b/src/cli/migrate/nodeVersion/index.ts index c82fe9232..ff219e9bb 100644 --- a/src/cli/migrate/nodeVersion/index.ts +++ b/src/cli/migrate/nodeVersion/index.ts @@ -40,8 +40,11 @@ export const getNode22TypeVersion = ( if (!version || !versionRegex.test(version)) { throw new Error('No version found'); } + const sanitizedVersion = version + .replace(versionRegex, '$1') + .replace(/"/g, ''); return { - version: version.replace(versionRegex, '$1'), + version: sanitizedVersion, err: undefined, }; } catch { From 45f800a481340bb9618cc7abe3a17ad28b736daf Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:56:53 +1100 Subject: [PATCH 21/32] .trim() & it.each --- src/cli/migrate/nodeVersion/index.test.ts | 63 +++++++++++++---------- src/cli/migrate/nodeVersion/index.ts | 3 +- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src/cli/migrate/nodeVersion/index.test.ts b/src/cli/migrate/nodeVersion/index.test.ts index 33ce1647c..9fdfe9be0 100644 --- a/src/cli/migrate/nodeVersion/index.test.ts +++ b/src/cli/migrate/nodeVersion/index.test.ts @@ -212,32 +212,39 @@ describe('nodeVersionMigration', () => { }); describe('getNodeTypesVersion', () => { - it('finds the latest node22 types version', () => { - jest - .spyOn(getNode22TypesVersionModule, 'getNode22TypesVersion') - .mockReturnValue('"22.10.6"'); - const { version, err } = getNode22TypeVersion(22, '22.9.0'); - expect(version).toBe('22.10.6'); - expect(err).toBeUndefined(); - }); - - it('defaults to 22.9.0 if the exec returns an invalid version', () => { - jest - .spyOn(getNode22TypesVersionModule, 'getNode22TypesVersion') - .mockReturnValue('This is not a version'); - const { version, err } = getNode22TypeVersion(22, '22.9.0'); - expect(version).toBe('22.9.0'); - expect(err).toBe('Failed to fetch latest version, using fallback version'); - }); - - it('defaults to 22.9.0 if the exec fails', () => { - jest - .spyOn(getNode22TypesVersionModule, 'getNode22TypesVersion') - .mockReturnValue( - new Error('Failed to fetch latest version') as unknown as string, - ); - const { version, err } = getNode22TypeVersion(22, '22.9.0'); - expect(version).toBe('22.9.0'); - expect(err).toBe('Failed to fetch latest version, using fallback version'); - }); + it.each([ + { + mockReturnValue: `"22.10.6"`, + expectedVersion: '22.10.6', + expectedError: undefined, + }, + { + mockReturnValue: `"22.10.6" +`, + expectedVersion: '22.10.6', + expectedError: undefined, + }, + { + mockReturnValue: 'This is not a version', + expectedVersion: '22.9.0', + expectedError: 'Failed to fetch latest version, using fallback version', + }, + { + mockReturnValue: new Error( + 'Failed to fetch latest version', + ) as unknown as string, + expectedVersion: '22.9.0', + expectedError: 'Failed to fetch latest version, using fallback version', + }, + ])( + 'finds the latest node22 types version with mock return value $mockReturnValue', + ({ mockReturnValue, expectedVersion, expectedError }) => { + jest + .spyOn(getNode22TypesVersionModule, 'getNode22TypesVersion') + .mockReturnValue(mockReturnValue); + const { version, err } = getNode22TypeVersion(22, '22.9.0'); + expect(version).toBe(expectedVersion); + expect(err).toBe(expectedError); + }, + ); }); diff --git a/src/cli/migrate/nodeVersion/index.ts b/src/cli/migrate/nodeVersion/index.ts index ff219e9bb..92687ffdd 100644 --- a/src/cli/migrate/nodeVersion/index.ts +++ b/src/cli/migrate/nodeVersion/index.ts @@ -42,7 +42,8 @@ export const getNode22TypeVersion = ( } const sanitizedVersion = version .replace(versionRegex, '$1') - .replace(/"/g, ''); + .replace(/"/g, '') + .trim(); return { version: sanitizedVersion, err: undefined, From a0e2a277bdbcee7b5ffc684a5defe6b15942655a Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Wed, 15 Jan 2025 17:30:12 +1100 Subject: [PATCH 22/32] Support nested files --- src/cli/migrate/nodeVersion/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cli/migrate/nodeVersion/index.ts b/src/cli/migrate/nodeVersion/index.ts index 92687ffdd..feb1e6b09 100644 --- a/src/cli/migrate/nodeVersion/index.ts +++ b/src/cli/migrate/nodeVersion/index.ts @@ -62,26 +62,26 @@ const subPatches: SubPatch[] = [ { id: 'nvmrc', file: '.nvmrc', replace: '<%- version %>\n' }, { id: 'Dockerfile-1', - files: 'Dockerfile*', + files: '**/Dockerfile*', test: /^FROM(.*) (public.ecr.aws\/docker\/library\/)?node:[0-9.]+(@sha256:[a-f0-9]{64})?(\.[^- \n]+)?(-[^ \n]+)?( .+|)$/gm, replace: 'FROM$1 $2node:<%- version %>$3$5$6', }, { id: 'Dockerfile-2', - files: 'Dockerfile*', + files: '**/Dockerfile*', test: /^FROM(.*) gcr.io\/distroless\/nodejs\d+-debian(.+)$/gm, replace: 'FROM$1 gcr.io/distroless/nodejs<%- version %>-debian$2', }, { id: 'serverless', - files: 'serverless*.y*ml', + files: '**/serverless*.y*ml', test: /nodejs\d+.x/gm, replace: 'nodejs<%- version %>.x', }, [ { id: 'cdk-1', - files: 'infra/**/*.ts', + files: '**/infra/**/*.ts', test: /NODEJS_\d+_X/g, replace: 'NODEJS_<%- version %>_X', }, From 4ebea5bc59f848f8add8450fc08b6c5260c665d4 Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Wed, 15 Jan 2025 17:35:44 +1100 Subject: [PATCH 23/32] Missed one --- src/cli/migrate/nodeVersion/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/migrate/nodeVersion/index.ts b/src/cli/migrate/nodeVersion/index.ts index feb1e6b09..aefb25a2f 100644 --- a/src/cli/migrate/nodeVersion/index.ts +++ b/src/cli/migrate/nodeVersion/index.ts @@ -87,7 +87,7 @@ const subPatches: SubPatch[] = [ }, { id: 'cdk-2', - files: 'infra/**/*.ts', + files: '**/infra/**/*.ts', test: /(target:\s*'node)(\d+)(.+)$/gm, replace: '$1<%- version %>$3', }, From 9928479e9672dd44d49e7b726ed87c522d8351d2 Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Thu, 16 Jan 2025 08:53:46 +1100 Subject: [PATCH 24/32] Fix lint --- .../internalLints/upgrade/patches/10.0.0/upgradeNode.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/cli/lint/internalLints/upgrade/patches/10.0.0/upgradeNode.ts b/src/cli/lint/internalLints/upgrade/patches/10.0.0/upgradeNode.ts index 7be9bdaae..1dcb1dff1 100644 --- a/src/cli/lint/internalLints/upgrade/patches/10.0.0/upgradeNode.ts +++ b/src/cli/lint/internalLints/upgrade/patches/10.0.0/upgradeNode.ts @@ -7,11 +7,15 @@ import { nodeVersionMigration } from '../../../../../migrate/nodeVersion'; const upgradeNode: PatchFunction = async ({ mode, }): Promise => { - if (mode === 'lint' || process.env.SKIP_NODE_UPGRADE) { + if (process.env.SKIP_NODE_UPGRADE) { return { result: 'skip', + reason: 'SKIP_NODE_UPGRADE environment variable set', }; } + if (mode === 'lint') { + return { result: 'apply' }; + } await nodeVersionMigration({ nodeVersion: 22, ECMAScriptVersion: 'ES2024' }); @@ -22,7 +26,7 @@ export const tryUpgradeNode: PatchFunction = async (config) => { try { return await upgradeNode(config); } catch (err) { - log.warn('Failed to patch Docker images'); + log.warn('Failed to upgrade node version'); log.subtle(inspect(err)); return { result: 'skip', reason: 'due to an error' }; } From c930683f83882487614919b4ca00f71cb4ca2a10 Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Thu, 16 Jan 2025 08:59:57 +1100 Subject: [PATCH 25/32] Stop self upgrading... --- .github/workflows/validate.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 56d3395e0..75cfafae9 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -105,6 +105,8 @@ jobs: - if: github.head_ref != 'changeset-release/main' && github.ref_name != 'changeset-release/main' name: Lint package + env: + SKIP_NODE_UPGRADE: true run: pnpm --filter ${{ matrix.template }} lint - if: github.head_ref != 'changeset-release/main' && github.ref_name != 'changeset-release/main' From ae6e164eb7da000739f5c74dcc1bc6727015e000 Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Thu, 16 Jan 2025 09:01:19 +1100 Subject: [PATCH 26/32] Update workflow node --- .github/workflows/release.yml | 2 +- .github/workflows/snapshot.yml | 2 +- .github/workflows/validate.yml | 6 +++--- template/oss-npm-package/.github/workflows/release.yml | 2 +- template/oss-npm-package/.github/workflows/validate.yml | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 70d7cd644..7d9ca812b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 22.x - name: Set up pnpm run: corepack enable pnpm && corepack install diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index aae258a20..23b6c5eed 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -21,7 +21,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 22.x - name: Set up pnpm run: corepack enable pnpm && corepack install diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 75cfafae9..33f70bc41 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -53,7 +53,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 22.x - name: Set up pnpm run: corepack enable pnpm && corepack install @@ -92,7 +92,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 22.x - name: Set up pnpm run: corepack enable pnpm @@ -135,7 +135,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 22.x - name: Set up pnpm run: corepack enable pnpm && corepack install diff --git a/template/oss-npm-package/.github/workflows/release.yml b/template/oss-npm-package/.github/workflows/release.yml index dd03ffaa5..7627dfe1f 100644 --- a/template/oss-npm-package/.github/workflows/release.yml +++ b/template/oss-npm-package/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 22.x - name: Set up pnpm run: corepack enable pnpm && corepack install diff --git a/template/oss-npm-package/.github/workflows/validate.yml b/template/oss-npm-package/.github/workflows/validate.yml index 1d35497c5..370753b5a 100644 --- a/template/oss-npm-package/.github/workflows/validate.yml +++ b/template/oss-npm-package/.github/workflows/validate.yml @@ -21,7 +21,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 22.x - name: Set up pnpm run: corepack enable pnpm && corepack install From 4b16e12e86071bb285ed753a1f1cd9e6a59ac17d Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Thu, 16 Jan 2025 09:17:50 +1100 Subject: [PATCH 27/32] Conditionally update based on serverless version --- src/cli/migrate/nodeVersion/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cli/migrate/nodeVersion/index.ts b/src/cli/migrate/nodeVersion/index.ts index aefb25a2f..05e6583f7 100644 --- a/src/cli/migrate/nodeVersion/index.ts +++ b/src/cli/migrate/nodeVersion/index.ts @@ -178,6 +178,7 @@ const runSubPatch = async ( const unPinnedContents = removeNodeShas(contents); if (patch.id === 'package-json-1') { + await checkServerlessVersion(); return await writePatchedContents({ path, contents: unPinnedContents, @@ -190,6 +191,7 @@ const runSubPatch = async ( } if (patch.id === 'tsconfig') { await checkSkubaType(); + await checkServerlessVersion(); return await writePatchedContents({ path, contents: unPinnedContents, @@ -203,6 +205,7 @@ const runSubPatch = async ( if (patch.id === 'package-json-2') { await checkSkubaType(); + await checkServerlessVersion(); } await writePatchedContents({ @@ -258,7 +261,6 @@ export const nodeVersionMigration = async ( ) => { log.ok(`Upgrading to Node.js ${nodeVersion}`); try { - await checkServerlessVersion(); const { version: nodeTypesVersion, err } = getNode22TypeVersion( nodeVersion, DEFAULT_NODE_TYPES, From 421b193ae822b6fe72cd3ca42875ed2560532608 Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Thu, 16 Jan 2025 09:28:42 +1100 Subject: [PATCH 28/32] This would probably help --- src/cli/migrate/nodeVersion/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cli/migrate/nodeVersion/index.ts b/src/cli/migrate/nodeVersion/index.ts index 05e6583f7..9272f552f 100644 --- a/src/cli/migrate/nodeVersion/index.ts +++ b/src/cli/migrate/nodeVersion/index.ts @@ -177,6 +177,10 @@ const runSubPatch = async ( const unPinnedContents = removeNodeShas(contents); + if (patch.id === 'serverless') { + await checkServerlessVersion(); + } + if (patch.id === 'package-json-1') { await checkServerlessVersion(); return await writePatchedContents({ From a2305b89f756aaa1da41a38b077550db447a3f0f Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Thu, 16 Jan 2025 10:02:27 +1100 Subject: [PATCH 29/32] Don't error on intended functionality --- src/cli/migrate/nodeVersion/index.test.ts | 6 ++-- src/cli/migrate/nodeVersion/index.ts | 20 +++++++---- .../nodeVersion/packageJsonChecks.test.ts | 34 ++++++++----------- .../migrate/nodeVersion/packageJsonChecks.ts | 24 ++++++++----- 4 files changed, 45 insertions(+), 39 deletions(-) diff --git a/src/cli/migrate/nodeVersion/index.test.ts b/src/cli/migrate/nodeVersion/index.test.ts index 9fdfe9be0..85e618dc9 100644 --- a/src/cli/migrate/nodeVersion/index.test.ts +++ b/src/cli/migrate/nodeVersion/index.test.ts @@ -9,11 +9,9 @@ jest .spyOn(getNode22TypesVersionModule, 'getNode22TypesVersion') .mockReturnValue('22.9.0'); -jest - .spyOn(packageJsonChecks, 'checkServerlessVersion') - .mockResolvedValue(undefined); +jest.spyOn(packageJsonChecks, 'validServerlessVersion').mockResolvedValue(true); -jest.spyOn(packageJsonChecks, 'checkSkubaType').mockResolvedValue(undefined); +jest.spyOn(packageJsonChecks, 'validSkubaType').mockResolvedValue(true); jest.mock('fs-extra', () => memfs); jest.mock('fast-glob', () => ({ diff --git a/src/cli/migrate/nodeVersion/index.ts b/src/cli/migrate/nodeVersion/index.ts index 9272f552f..e2ed83ca0 100644 --- a/src/cli/migrate/nodeVersion/index.ts +++ b/src/cli/migrate/nodeVersion/index.ts @@ -7,7 +7,7 @@ import { log } from '../../../utils/logging'; import { createDestinationFileReader } from '../../configure/analysis/project'; import { getNode22TypesVersion } from './getNode22TypesVersion'; -import { checkServerlessVersion, checkSkubaType } from './packageJsonChecks'; +import { validServerlessVersion, validSkubaType } from './packageJsonChecks'; const DEFAULT_NODE_TYPES = '22.9.0'; @@ -178,11 +178,15 @@ const runSubPatch = async ( const unPinnedContents = removeNodeShas(contents); if (patch.id === 'serverless') { - await checkServerlessVersion(); + if (!(await validServerlessVersion())) { + return; + } } if (patch.id === 'package-json-1') { - await checkServerlessVersion(); + if (!(await validServerlessVersion())) { + return; + } return await writePatchedContents({ path, contents: unPinnedContents, @@ -194,8 +198,9 @@ const runSubPatch = async ( }); } if (patch.id === 'tsconfig') { - await checkSkubaType(); - await checkServerlessVersion(); + if (!(await validServerlessVersion()) || !(await validSkubaType())) { + return; + } return await writePatchedContents({ path, contents: unPinnedContents, @@ -208,8 +213,9 @@ const runSubPatch = async ( } if (patch.id === 'package-json-2') { - await checkSkubaType(); - await checkServerlessVersion(); + if (!(await validServerlessVersion()) || !(await validSkubaType())) { + return; + } } await writePatchedContents({ diff --git a/src/cli/migrate/nodeVersion/packageJsonChecks.test.ts b/src/cli/migrate/nodeVersion/packageJsonChecks.test.ts index 3064ed845..a105e47bf 100644 --- a/src/cli/migrate/nodeVersion/packageJsonChecks.test.ts +++ b/src/cli/migrate/nodeVersion/packageJsonChecks.test.ts @@ -6,7 +6,7 @@ jest.mock('fs-extra'); import { log } from '../../../utils/logging'; -import { checkServerlessVersion, checkSkubaType } from './packageJsonChecks'; +import { validServerlessVersion, validSkubaType } from './packageJsonChecks'; jest.spyOn(log, 'warn'); @@ -14,7 +14,7 @@ afterEach(() => { jest.resetAllMocks(); }); -describe('checkServerlessVersion', () => { +describe('validServerlessVersion', () => { it('resolves as a noop when serverless version is supported', async () => { jest.mocked(findUp).mockResolvedValueOnce('package.json'); jest @@ -27,9 +27,9 @@ describe('checkServerlessVersion', () => { }, }) as never, ); - await expect(checkServerlessVersion()).resolves.toBeUndefined(); + await expect(validServerlessVersion()).resolves.toBe(true); }); - it('throws when the serverless version is below 4', async () => { + it('should return false when the serverless version is below 4', async () => { jest.mocked(findUp).mockResolvedValueOnce('package.json'); jest .spyOn(fs, 'readFile') @@ -41,28 +41,26 @@ describe('checkServerlessVersion', () => { }, }) as never, ); - await expect(checkServerlessVersion()).rejects.toThrow( - 'Serverless version not supported, please upgrade to 4.x', - ); + await expect(validServerlessVersion()).resolves.toBe(false); }); - it('resolves as a noop when serverless version is not found', async () => { + it('should return true when serverless version is not found', async () => { jest.mocked(findUp).mockResolvedValueOnce('package.json'); jest .spyOn(fs, 'readFile') .mockImplementation() .mockReturnValue(JSON.stringify({}) as never); - await expect(checkServerlessVersion()).resolves.toBeUndefined(); + await expect(validServerlessVersion()).resolves.toBe(true); }); it('throws when no package.json is found', async () => { jest.mocked(findUp).mockResolvedValueOnce(undefined); - await expect(checkServerlessVersion()).rejects.toThrow( + await expect(validServerlessVersion()).rejects.toThrow( 'package.json not found', ); }); }); -describe('checkSkubaType', () => { - it('should return undefined when skuba type is not "package"', async () => { +describe('validSkubaType', () => { + it('should return true when skuba type is not "package"', async () => { jest.mocked(findUp).mockResolvedValueOnce('package.json'); jest .spyOn(fs, 'readFile') @@ -74,9 +72,9 @@ describe('checkSkubaType', () => { }, }) as never, ); - await expect(checkSkubaType()).resolves.toBeUndefined(); + await expect(validSkubaType()).resolves.toBe(true); }); - it('should throw when skuba type is "package"', async () => { + it('should return false when skuba type is "package"', async () => { jest.mocked(findUp).mockResolvedValueOnce('package.json'); jest .spyOn(fs, 'readFile') @@ -88,16 +86,14 @@ describe('checkSkubaType', () => { }, }) as never, ); - await expect(checkSkubaType()).rejects.toThrow( - 'Skuba type package is not supported, packages should be updated manually to ensure major runtime depreciations are intended', - ); + await expect(validSkubaType()).resolves.toBe(false); }); - it('should return undefined when skuba type is not found', async () => { + it('should return true when skuba type is not found', async () => { jest.mocked(findUp).mockResolvedValueOnce('package.json'); jest .spyOn(fs, 'readFile') .mockImplementation() .mockReturnValue(JSON.stringify({}) as never); - await expect(checkSkubaType()).resolves.toBeUndefined(); + await expect(validSkubaType()).resolves.toBe(true); }); }); diff --git a/src/cli/migrate/nodeVersion/packageJsonChecks.ts b/src/cli/migrate/nodeVersion/packageJsonChecks.ts index cd4f31eb8..226980ff6 100644 --- a/src/cli/migrate/nodeVersion/packageJsonChecks.ts +++ b/src/cli/migrate/nodeVersion/packageJsonChecks.ts @@ -1,6 +1,8 @@ import findUp from 'find-up'; import fs from 'fs-extra'; +import { log } from '../../../utils/logging'; + const getParentPackageJson = async () => { const packageJsonPath = await findUp('package.json', { cwd: process.cwd() }); if (!packageJsonPath) { @@ -16,7 +18,7 @@ const isTypeError = (error: unknown): error is TypeError => const isSyntaxError = (error: unknown): error is SyntaxError => error instanceof SyntaxError && error.message.includes('Unexpected token'); -export const checkServerlessVersion = async () => { +export const validServerlessVersion = async (): Promise => { const packageJson = await getParentPackageJson(); try { @@ -26,23 +28,25 @@ export const checkServerlessVersion = async () => { } ).devDependencies.serverless; if (!serverlessVersion) { - return; + return true; } if (!serverlessVersion.startsWith('4')) { - throw new Error( - 'Serverless version not supported, please upgrade to 4.x', + log.warn( + 'Serverless version not supported, please upgrade to 4.x to automatically update serverless files', ); + return false; } } catch (error) { if (isTypeError(error) || isSyntaxError(error)) { - return; + return true; } throw error; } + return true; }; -export const checkSkubaType = async () => { +export const validSkubaType = async () => { const packageJson = await getParentPackageJson(); try { @@ -52,18 +56,20 @@ export const checkSkubaType = async () => { } ).skuba.type; if (!type) { - return; + return true; } if (type === 'package') { - throw new Error( + log.warn( 'Skuba type package is not supported, packages should be updated manually to ensure major runtime depreciations are intended', ); + return false; } } catch (error) { if (isTypeError(error) || isSyntaxError(error)) { - return; + return true; } throw error; } + return true; }; From 4aeabb151b89777bbbb32ce40fd8ca2228bc43fd Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Thu, 16 Jan 2025 10:30:26 +1100 Subject: [PATCH 30/32] Shamelessly yoink changeset --- .changeset/nasty-masks-eat.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.changeset/nasty-masks-eat.md b/.changeset/nasty-masks-eat.md index 1a7b1f75d..919709f6c 100644 --- a/.changeset/nasty-masks-eat.md +++ b/.changeset/nasty-masks-eat.md @@ -2,4 +2,17 @@ 'skuba': major --- -lint: TODO +migrate: Introduce `skuba migrate node22` to automatically upgrade a project's Node.js version + +`skuba migrate node22` will attempt to automatically upgrade projects to Node.js 22. +It will look in the project root for Dockerfiles, `.nvmrc`, `.node-version`, `tsconfig.json`, `package.json` and Serverless files, +as well as CDK files in `infra/` and `.buildkite/` files, and try to upgrade them to a Node.js 22 version. + +skuba might not be able to upgrade all projects, so please check your project for any files that skuba missed. It's +possible that skuba will modify a file incorrectly, in which case please +[open an issue](https://github.com/seek-oss/skuba/issues/new). + +Node.js 22 comes with its own breaking changes, so please read the [Node.js 22 release notes](https://nodejs.org/en/blog/announcements/v22-release-announce) alongside the skuba release notes. In addition, + +- For AWS Lambda runtime updates to `nodejs22.x`, consider reading the [release announcement](https://aws.amazon.com/blogs/compute/node-js-22-runtime-now-available-in-aws-lambda/) as there are some breaking changes with this upgrade. +- You may need to upgrade your versions of CDK and Serverless as appropriate to support nodejs22.x. From 32a852e3348e6b4d00e7518aac58aa0c5aed9ab9 Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Thu, 16 Jan 2025 10:32:54 +1100 Subject: [PATCH 31/32] Words --- .changeset/nasty-masks-eat.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/nasty-masks-eat.md b/.changeset/nasty-masks-eat.md index 919709f6c..a2fea6e71 100644 --- a/.changeset/nasty-masks-eat.md +++ b/.changeset/nasty-masks-eat.md @@ -12,6 +12,8 @@ skuba might not be able to upgrade all projects, so please check your project fo possible that skuba will modify a file incorrectly, in which case please [open an issue](https://github.com/seek-oss/skuba/issues/new). +If you wish not to upgrade to Node.js 22, you can run set the `SKIP_NODE_UPGRADE=true` environment variable before running `skuba lint` or `skuba format`. + Node.js 22 comes with its own breaking changes, so please read the [Node.js 22 release notes](https://nodejs.org/en/blog/announcements/v22-release-announce) alongside the skuba release notes. In addition, - For AWS Lambda runtime updates to `nodejs22.x`, consider reading the [release announcement](https://aws.amazon.com/blogs/compute/node-js-22-runtime-now-available-in-aws-lambda/) as there are some breaking changes with this upgrade. From ed14a1b01704f32a08bccf2c29157dd85bbe313d Mon Sep 17 00:00:00 2001 From: Zac Brydon <52645024+zbrydon@users.noreply.github.com> Date: Fri, 17 Jan 2025 09:05:12 +1100 Subject: [PATCH 32/32] Not bumping the minimum node version --- .changeset/nasty-masks-eat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/nasty-masks-eat.md b/.changeset/nasty-masks-eat.md index a2fea6e71..8a7303868 100644 --- a/.changeset/nasty-masks-eat.md +++ b/.changeset/nasty-masks-eat.md @@ -1,5 +1,5 @@ --- -'skuba': major +'skuba': minor --- migrate: Introduce `skuba migrate node22` to automatically upgrade a project's Node.js version