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

Add skuba migrate command to upgrade projects to Node.js 20 #1382

Merged
merged 16 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
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
20 changes: 20 additions & 0 deletions .changeset/real-oranges-rescue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'skuba': minor
AaronMoat marked this conversation as resolved.
Show resolved Hide resolved
AaronMoat marked this conversation as resolved.
Show resolved Hide resolved
---

migrate: Introduce `skuba migrate node-version` to automatically upgrade a project's Node.js version

`skuba migrate node-version` will attempt to automatically upgrade projects to Node.js 20.
AaronMoat marked this conversation as resolved.
Show resolved Hide resolved
It will look in the project root for Dockerfiles, `.nvmrc`, and Serverless files,
as well as CDK files in `infra/`, and try to upgrade them to a Node.js 20 version.

Other Node.js versions can be specified with `skuba migrate node-version <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 20 comes with its own breaking changes, so please read the [Node.js 20 release notes](https://nodejs.org/en/blog/announcements/v20-release-announce) alongside the skuba release notes. In addition,

- For AWS Lambda runtime updates to `nodejs20.x`, consider reading the [release announcement](https://aws.amazon.com/blogs/compute/node-js-20-x-runtime-now-available-in-aws-lambda/) as there are some breaking changes with this upgrade.
AaronMoat marked this conversation as resolved.
Show resolved Hide resolved
- You may need to upgrade your versions of CDK and Serverless as appropriate to support nodejs20.x.
2 changes: 1 addition & 1 deletion docs/cli/help.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
parent: CLI
nav_order: 7
nav_order: 8
---

# Help
Expand Down
36 changes: 36 additions & 0 deletions docs/cli/migrate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
parent: CLI
nav_order: 7
---

# Migrate

---

## skuba migrate help

Echoes the available **skuba** migrations

```shell
skuba migrate help
```

---

## skuba migrate node-version

`skuba migrate node-version` will attempt to automatically upgrade projects to Node.js 20.
It will look in the project root for Dockerfiles, `.nvmrc`, and Serverless files,
as well as CDK files in `infra/`, and try to upgrade them to a Node.js 20 version.

Other Node.js versions can be specified with `skuba migrate node-version <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).

```shell
skuba version
skuba -v
skuba --version
```
2 changes: 1 addition & 1 deletion docs/cli/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,6 @@ Execution should pause on the breakpoint until we hit `F5` or the `▶️` butto
[`tsconfig-paths`]: https://github.com/dividab/tsconfig-paths
[express]: https://expressjs.com/
[fastify]: https://www.fastify.io/
[http server]: https://nodejs.org/docs/latest-v18.x/api/http.html#class-httpserver
[http server]: https://nodejs.org/docs/latest-v20.x/api/http.html#class-httpserver
[koa]: https://koajs.com/
[node.js options]: https://nodejs.org/en/docs/guides/debugging-getting-started/#command-line-options
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"eslint": "^8.11.0",
"eslint-config-skuba": "3.1.0",
"execa": "^5.0.0",
"fast-glob": "^3.3.2",
"fdir": "^6.0.0",
"fs-extra": "^11.0.0",
"function-arguments": "^1.0.9",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion scripts/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ const main = async () => {
'CONTRIBUTING.md',
path.join('dist-docs', 'CONTRIBUTING.md'),
),
// `fs.promises.cp` is still experimental in Node.js 18.
// `fs.promises.cp` is still experimental in Node.js 20.
copy('site', 'dist-docs'),
copy('docs', path.join('dist-docs', 'docs')),
]);
Expand Down
2 changes: 1 addition & 1 deletion src/cli/init/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
ensureTemplateConfigDeletion,
} from '../../utils/template';
import { runPrettier } from '../adapter/prettier';
import { tryPatchRenovateConfig } from '../configure/patchRenovateConfig';
import { tryPatchRenovateConfig } from '../lint/internalLints/patchRenovateConfig';

import { getConfig } from './getConfig';
import { initialiseRepo } from './git';
Expand Down
2 changes: 1 addition & 1 deletion src/cli/lint/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { inspect } from 'util';
import chalk from 'chalk';

import { type Logger, createLogger } from '../../utils/logging';
import { upgradeSkuba } from '../configure/upgrade';

import { deleteFilesLint } from './internalLints/deleteFiles';
import { noSkubaTemplateJs } from './internalLints/noSkubaTemplateJs';
import { tryRefreshConfigFiles } from './internalLints/refreshConfigFiles';
import { upgradeSkuba } from './internalLints/upgrade';
import type { Input } from './types';

export type InternalLintResult = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { inspect } from 'util';

import memfs, { vol } from 'memfs';

import * as Git from '../../api/git';
import * as Git from '../../../api/git';

import { tryPatchRenovateConfig } from './patchRenovateConfig';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import fs from 'fs-extra';
import * as fleece from 'golden-fleece';
import { z } from 'zod';

import * as Git from '../../api/git';
import { log } from '../../utils/logging';
import * as Git from '../../../api/git';
import { log } from '../../../utils/logging';
import { createDestinationFileReader } from '../../configure/analysis/project';
import { RENOVATE_CONFIG_FILENAMES } from '../../configure/modules/renovate';
import { formatPrettier } from '../../configure/processing/prettier';

import { createDestinationFileReader } from './analysis/project';
import { RENOVATE_CONFIG_FILENAMES } from './modules/renovate';
import { formatPrettier } from './processing/prettier';
import type { PatchFunction, PatchReturnType } from './upgrade';

const RENOVATE_PRESETS = [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { readdir, writeFile } from 'fs-extra';
import type { NormalizedPackageJson } from 'read-pkg-up';

import { log } from '../../../utils/logging';
import { getConsumerManifest } from '../../../utils/manifest';
import { getSkubaVersion } from '../../../utils/version';
import { log } from '../../../../utils/logging';
import { getConsumerManifest } from '../../../../utils/manifest';
import { getSkubaVersion } from '../../../../utils/version';

import { upgradeSkuba } from '.';

jest.mock('../../../utils/manifest');
jest.mock('../../../utils/version');
jest.mock('../../../../utils/manifest');
jest.mock('../../../../utils/version');
jest.mock('fs-extra');
jest.mock('../../../utils/logging');
jest.mock('../../../../utils/logging');

beforeEach(() => {
jest.clearAllMocks();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import path from 'path';
import { readdir, writeFile } from 'fs-extra';
import { gte, sort } from 'semver';

import type { Logger } from '../../../utils/logging';
import { getConsumerManifest } from '../../../utils/manifest';
import { detectPackageManager } from '../../../utils/packageManager';
import { getSkubaVersion } from '../../../utils/version';
import type { SkubaPackageJson } from '../../init/writePackageJson';
import type { InternalLintResult } from '../../lint/internal';
import { formatPackage } from '../processing/package';
import type { Logger } from '../../../../utils/logging';
import { getConsumerManifest } from '../../../../utils/manifest';
import { detectPackageManager } from '../../../../utils/packageManager';
import { getSkubaVersion } from '../../../../utils/version';
import { formatPackage } from '../../../configure/processing/package';
import type { SkubaPackageJson } from '../../../init/writePackageJson';
import type { InternalLintResult } from '../../internal';

export type Patches = Patch[];
export type Patch = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from 'fs-extra';

import * as packageAnalysis from '../../../analysis/package';
import * as projectAnalysis from '../../../analysis/project';
import * as packageAnalysis from '../../../../../configure/analysis/package';
import * as projectAnalysis from '../../../../../configure/analysis/project';

import { tryAddEmptyExports } from './addEmptyExports';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { inspect } from 'util';
import fs from 'fs-extra';

import type { PatchFunction } from '../..';
import { log } from '../../../../../utils/logging';
import { getDestinationManifest } from '../../../analysis/package';
import { createDestinationFileReader } from '../../../analysis/project';
import { formatPrettier } from '../../../processing/prettier';
import { log } from '../../../../../../utils/logging';
import { getDestinationManifest } from '../../../../../configure/analysis/package';
import { createDestinationFileReader } from '../../../../../configure/analysis/project';
import { formatPrettier } from '../../../../../configure/processing/prettier';

const JEST_SETUP_FILES = ['jest.setup.ts', 'jest.setup.int.ts'];

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from 'fs-extra';

import * as packageAnalysis from '../../../analysis/package';
import * as projectAnalysis from '../../../analysis/project';
import * as packageAnalysis from '../../../../../configure/analysis/package';
import * as projectAnalysis from '../../../../../configure/analysis/project';

import { tryMoveNpmrcOutOfIgnoreManagedSection } from './moveNpmrcOutOfIgnoreManagedSection';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { inspect } from 'util';
import fs from 'fs-extra';

import type { PatchFunction, PatchReturnType } from '../..';
import { log } from '../../../../../utils/logging';
import { NPMRC_LINES } from '../../../../../utils/npmrc';
import { createDestinationFileReader } from '../../../analysis/project';
import { log } from '../../../../../../utils/logging';
import { NPMRC_LINES } from '../../../../../../utils/npmrc';
import { createDestinationFileReader } from '../../../../../configure/analysis/project';

const NPMRC_IGNORE_SECTION = `

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { inspect } from 'util';
import fs from 'fs-extra';

import type { PatchFunction, PatchReturnType } from '../..';
import { log } from '../../../../../utils/logging';
import { createDestinationFileReader } from '../../../analysis/project';
import { log } from '../../../../../../utils/logging';
import { createDestinationFileReader } from '../../../../../configure/analysis/project';

const DOCKERFILE_FILENAME = 'Dockerfile';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe('patchServerListener', () => {
});

// Gantry ALB default idle timeout is 30 seconds
// https://nodejs.org/docs/latest-v18.x/api/http.html#serverkeepalivetimeout
// https://nodejs.org/docs/latest-v20.x/api/http.html#serverkeepalivetimeout
// Node default is 5 seconds
// https://docs.aws.amazon.com/elasticloadbalancing/latest/application/application-load-balancers.html#connection-idle-timeout
// AWS recommends setting an application timeout larger than the load balancer
Expand All @@ -73,7 +73,7 @@ describe('patchServerListener', () => {
"src/listen.ts": "const listener = app.listen(config.port);

// Gantry ALB default idle timeout is 30 seconds
// https://nodejs.org/docs/latest-v18.x/api/http.html#serverkeepalivetimeout
// https://nodejs.org/docs/latest-v20.x/api/http.html#serverkeepalivetimeout
// Node default is 5 seconds
// https://docs.aws.amazon.com/elasticloadbalancing/latest/application/application-load-balancers.html#connection-idle-timeout
// AWS recommends setting an application timeout larger than the load balancer
Expand Down Expand Up @@ -117,7 +117,7 @@ describe('patchServerListener', () => {
it('skips the templated Koa listener', async () => {
const listener = await fs.promises.readFile(
require.resolve(
'../../../../../../template/koa-rest-api/src/listen.ts',
'../../../../../../../template/koa-rest-api/src/listen.ts',
),
'utf-8',
);
Expand All @@ -139,7 +139,7 @@ describe('patchServerListener', () => {
it('skips the templated Express listener', async () => {
const listener = await fs.promises.readFile(
require.resolve(
'../../../../../../template/express-rest-api/src/listen.ts',
'../../../../../../../template/express-rest-api/src/listen.ts',
),
'utf-8',
);
Expand Down Expand Up @@ -243,7 +243,7 @@ describe('patchServerListener', () => {
it('skips the templated Koa listener', async () => {
const listener = await fs.promises.readFile(
require.resolve(
'../../../../../../template/koa-rest-api/src/listen.ts',
'../../../../../../../template/koa-rest-api/src/listen.ts',
),
'utf-8',
);
Expand All @@ -265,7 +265,7 @@ describe('patchServerListener', () => {
it('skips the templated Express listener', async () => {
const listener = await fs.promises.readFile(
require.resolve(
'../../../../../../template/express-rest-api/src/listen.ts',
'../../../../../../../template/express-rest-api/src/listen.ts',
),
'utf-8',
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import { inspect } from 'util';
import fs from 'fs-extra';

import type { PatchFunction, PatchReturnType } from '../..';
import { log } from '../../../../../utils/logging';
import { createDestinationFileReader } from '../../../analysis/project';
import { formatPrettier } from '../../../processing/prettier';
import { log } from '../../../../../../utils/logging';
import { createDestinationFileReader } from '../../../../../configure/analysis/project';
import { formatPrettier } from '../../../../../configure/processing/prettier';

const SERVER_LISTENER_FILENAME = 'src/listen.ts';

const KEEP_ALIVE_CODE = `
// Gantry ALB default idle timeout is 30 seconds
// https://nodejs.org/docs/latest-v18.x/api/http.html#serverkeepalivetimeout
// https://nodejs.org/docs/latest-v20.x/api/http.html#serverkeepalivetimeout
// Node default is 5 seconds
// https://docs.aws.amazon.com/elasticloadbalancing/latest/application/application-load-balancers.html#connection-idle-timeout
// AWS recommends setting an application timeout larger than the load balancer
Expand Down
54 changes: 54 additions & 0 deletions src/cli/migrate/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { log } from '../../utils/logging';

import { CURRENT_NODE_LTS, nodeVersionMigration } from './nodeVersion';

const migrations = {
AaronMoat marked this conversation as resolved.
Show resolved Hide resolved
'node-version': {
migrate: nodeVersionMigration,
getArgs: (args: string[]): [number] => {
if (args.length === 0) return [CURRENT_NODE_LTS];

const version = Number(args[0]);
if (Number.isNaN(version)) {
log.err('Provide a valid Node.js version to migrate to.');
process.exit(1);
}

return [version];
},
},
};

type Migration = keyof typeof migrations;

const logAvailableMigrations = () => {
log.ok('Available migrations:');
Object.keys(migrations).forEach((migration) => {
log.ok(`- ${migration}`);
});
};

export const migrate = async (args = process.argv.slice(2)) => {
if (!args[0]) {
log.err('Provide a migration to run.');
logAvailableMigrations();
process.exitCode = 1;
return;
}

if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
logAvailableMigrations();
return;
}

if (!migrations[args[0] as Migration]) {
AaronMoat marked this conversation as resolved.
Show resolved Hide resolved
log.err(`Migration "${args[0]}" is not a valid option.`);
logAvailableMigrations();
process.exitCode = 1;
return;
}

const migration = migrations[args[0] as Migration];

await migration.migrate(...migration.getArgs(args.slice(1)));
};
Loading
Loading