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

Open
wants to merge 38 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
72c0a9f
Initial `skuba migrate` modifications
zbrydon Nov 13, 2024
c7be1a5
Use latest node 22 types
zbrydon Nov 13, 2024
ca66785
Replace fetch with npm show
zbrydon Nov 13, 2024
14dc05c
Mock execSync
zbrydon Nov 13, 2024
2176a05
Generalise and add back node20 migrate
zbrydon Nov 14, 2024
575eaea
Merge branch 'main' of github.com:seek-oss/skuba into node-22
zbrydon Nov 18, 2024
ec85772
Add checkServerlessVersion
zbrydon Nov 19, 2024
178c02f
WIP dependent file checks
zbrydon Nov 19, 2024
8d2a75b
This isn't suspicious at all
zbrydon Nov 19, 2024
99be3a9
Refactor
zbrydon Nov 20, 2024
b165e28
WIP
zbrydon Nov 26, 2024
c80bb24
Merge branch 'main' of github.com:seek-oss/skuba into node-22
zbrydon Jan 8, 2025
bf26aa1
Fix
zbrydon Jan 10, 2025
9c55f4d
Fix
zbrydon Jan 10, 2025
659fe97
Init changeset
zbrydon Jan 10, 2025
1eb0494
changeset scope
zbrydon Jan 10, 2025
3e8d957
Fixes
zbrydon Jan 14, 2025
4c5da8d
Merge branch 'main' of github.com:seek-oss/skuba into node-22
zbrydon Jan 15, 2025
c66aa14
Fixes
zbrydon Jan 15, 2025
88e54f7
Remove " from node types version
zbrydon Jan 15, 2025
6f543cf
Snapshots and mock
zbrydon Jan 15, 2025
805ec17
Merge branch 'main' of github.com:seek-oss/skuba into node-22
zbrydon Jan 15, 2025
0efdaf0
Sanitize queried node types version
zbrydon Jan 15, 2025
c771121
Well... I don't like this
zbrydon Jan 15, 2025
45f800a
.trim() & it.each
zbrydon Jan 15, 2025
a0e2a27
Support nested files
zbrydon Jan 15, 2025
4ebea5b
Missed one
zbrydon Jan 15, 2025
9928479
Fix lint
zbrydon Jan 15, 2025
c930683
Stop self upgrading...
zbrydon Jan 15, 2025
ae6e164
Update workflow node
zbrydon Jan 15, 2025
4b16e12
Conditionally update based on serverless version
zbrydon Jan 15, 2025
421b193
This would probably help
zbrydon Jan 15, 2025
a2305b8
Don't error on intended functionality
zbrydon Jan 15, 2025
ebc6bc3
Merge branch 'main' into node-22
zbrydon Jan 15, 2025
4aeabb1
Shamelessly yoink changeset
zbrydon Jan 15, 2025
3ae3e5e
Merge branch 'node-22' of github.com:seek-oss/skuba into node-22
zbrydon Jan 15, 2025
32a852e
Words
zbrydon Jan 15, 2025
ed14a1b
Not bumping the minimum node version
zbrydon Jan 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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),
zbrydon marked this conversation as resolved.
Show resolved Hide resolved
};

const logAvailableMigrations = () => {
Expand Down
141 changes: 113 additions & 28 deletions src/cli/migrate/nodeVersion/index.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => ({
Expand All @@ -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', () => {
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,74 @@ 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('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');
});
});
124 changes: 110 additions & 14 deletions src/cli/migrate/nodeVersion/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,45 @@ type SubPatch = (
replace: string;
};

type VersionResult = {
version: string;
err: string | undefined;
};

type RegistryResponse = {
'dist-tags': Record<string, string>;
};

export const getLatestNodeTypes = async (): Promise<VersionResult> => {
zbrydon marked this conversation as resolved.
Show resolved Hide resolved
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*',
Expand All @@ -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,
zbrydon marked this conversation as resolved.
Show resolved Hide resolved
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]
Expand All @@ -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),
),
);
};

Expand All @@ -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');
Expand Down