Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

skuba migrate node 22 #1735

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion src/cli/migrate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { log } from '../../utils/logging';
import { nodeVersionMigration } from './nodeVersion';

const migrations: Record<string, () => Promise<void>> = {
node20: () => nodeVersionMigration(20),
node22: () => nodeVersionMigration(22),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like it could be fine to keep 20 around just for purposes of people doing a 2-step upgrade? 🤷 maybe not

};

const logAvailableMigrations = () => {
Expand Down
4 changes: 4 additions & 0 deletions src/cli/migrate/nodeVersion/getNode22TypesVersion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { execSync } from 'child_process';

export const getNode22TypesVersion = () =>
execSync("npm show @types/node@^22 version --json | jq '.[-1]'").toString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could generalise a bit:

Suggested change
export const getNode22TypesVersion = () =>
execSync("npm show @types/node@^22 version --json | jq '.[-1]'").toString();
export const getNodeTypesVersionForMajor = (major: number) =>
execSync(`npm show @types/node@^${major} version --json | jq '.[-1]'`).toString();

154 changes: 126 additions & 28 deletions src/cli/migrate/nodeVersion/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import memfs, { vol } from 'memfs';

import { 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', () => ({
Expand All @@ -11,9 +17,10 @@ jest.mock('../../../utils/logging');

const volToJson = () => vol.toJSON(process.cwd(), undefined, true);

beforeEach(jest.clearAllMocks);
beforeEach(() => vol.reset());

afterEach(() => jest.clearAllMocks());

describe('nodeVersionMigration', () => {
const scenarios: Array<{
filesBefore: Record<string, string>;
Expand Down Expand Up @@ -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',
},
},
{
Expand All @@ -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',
},
},
{
Expand All @@ -117,16 +144,87 @@ 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)(
'handles $scenario',
async ({ filesBefore, filesAfter }) => {
vol.fromJSON(filesBefore, process.cwd());

await nodeVersionMigration(20);
await nodeVersionMigration(22);

expect(volToJson()).toEqual(filesAfter ?? filesBefore);
},
);
});

describe('getNodeTypesVersion', () => {
it('finds the latest node22 types version', () => {
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', () => {
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');
});
});
Loading