Skip to content

Commit

Permalink
Add skuba migrate command to upgrade projects to Node.js 20 (#1382)
Browse files Browse the repository at this point in the history
* Automatically upgrade projects to Node.js 20

* Also patch CDK

* Update .changeset/real-oranges-rescue.md

* Workshop changelog

* Update .changeset/real-oranges-rescue.md

* Run `skuba format`

* Move code around

* Move to a skuba migrate command

* Add doc

* Revert template changes

* Wrangle types

* Patch inline docker images too

* Changeset

* Go for node20 but keep the code reusable for later

---------

Co-authored-by: skuba <[email protected]>
  • Loading branch information
AaronMoat and seek-oss-ci authored Feb 15, 2024
1 parent 97a84fd commit 8fb2d8f
Show file tree
Hide file tree
Showing 26 changed files with 373 additions and 47 deletions.
18 changes: 18 additions & 0 deletions .changeset/real-oranges-rescue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'skuba': minor
---

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

`skuba migrate node20` 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 `.buildkite/` files, and try to upgrade them to a Node.js 20 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.
- 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
37 changes: 37 additions & 0 deletions docs/cli/migrate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
parent: CLI
nav_order: 7
---

# Migrate

---

## skuba migrate help

Echoes the available **skuba** migrations

```shell
skuba migrate help
```

---

## skuba migrate node20

`skuba migrate node20` 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 `.buildkite/` files, and try to upgrade them to a Node.js 20 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.
- You may need to upgrade your versions of CDK and Serverless as appropriate to support nodejs20.x.

```shell
skuba migrate node20
```
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,10 +3,10 @@ import { inspect } from 'util';
import chalk from 'chalk';

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

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
39 changes: 39 additions & 0 deletions src/cli/migrate/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { log } from '../../utils/logging';

import { nodeVersionMigration } from './nodeVersion';

const migrations: Record<string, () => Promise<void>> = {
node20: () => nodeVersionMigration(20),
};

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;
}

const migration = migrations[args[0]];

if (!migration) {
log.err(`Migration "${args[0]}" is not a valid option.`);
logAvailableMigrations();
process.exitCode = 1;
return;
}

await migration();
};
Loading

0 comments on commit 8fb2d8f

Please sign in to comment.