Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 15 additions & 0 deletions .changeset/node24-support-drop-node20.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@twilio-labs/serverless-twilio-runtime': major
'create-twilio-function': major
'@twilio-labs/plugin-serverless': major
'@twilio-labs/plugin-assets': major
'twilio-run': major
---

**WHAT**: Add Node.js 24 to supported versions and remove Node.js 20.

**ALSO**: `twilio-run deploy` now warns when a project is configured to deploy to a deprecated runtime such as `node20`, pointing at the `runtime` field in `.twilioserverlessrc`. It is a warning, not a failure — the deploy still proceeds and Twilio Serverless has the final say. Both this check and the local Node.js version check use a deny list of deprecated versions rather than an allow list of supported ones, so an older release of the toolkit never claims a newer Node.js version is invalid. `printVersionWarning` drops its unused `expectedVersion` parameter as part of this.

**WHY**: `node24` was added as a deploy runtime and made the default in #555, but `engines.node` was never widened, so the toolkit defaulted to deploying to node24 while refusing to declare support for running on Node 24 (#563). Node.js 20 is deprecated on Twilio Serverless and can no longer be used for new builds, so it is removed at the same time.

**BREAKING CHANGE**: Projects using `create-twilio-function`, `@twilio-labs/plugin-assets`, `@twilio-labs/plugin-serverless`, `@twilio-labs/serverless-twilio-runtime` or `twilio-run` will have to migrate to Node.js 22 or 24.
4 changes: 2 additions & 2 deletions .github/workflows/on-merge-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ jobs:
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js 20
- name: Setup Node.js 22
uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3
with:
node-version: 20
node-version: 22
- name: Update npm to 10.x
run: |
npm i -g npm@10
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20
22
4 changes: 2 additions & 2 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ If you are planning to contribute something that does not have an open issue yet

## Requirements

Make sure you have Node.js 20 or newer installed. Due to compatibility with Twilio
Functions this project has to support at least Node.js 20.0.0.
Make sure you have Node.js 22 or newer installed. Due to compatibility with Twilio
Functions this project has to support at least Node.js 22.0.0.

We are using the npm CLI to manage our project. You'll need at least `npm` version 8 or newer.

Expand Down
2 changes: 1 addition & 1 deletion packages/create-twilio-function/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"yargs": "^17.2.1"
},
"engines": {
"node": "^20.x || ^22.x"
"node": "^22.x || ^24.x"
},
"files": [
"bin/",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module.exports = {
'@twilio/runtime-handler'
].replace(/[\^~]/, ''),
twilioRun: pkgJson.dependencies['twilio-run'],
node: '^20.x || ^22.x || ^24.x',
node: '^22.x || ^24.x',
typescript: '^5.3.3',
serverlessRuntimeTypes: '^4.0.0',
copyfiles: '^2.4.1',
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"rimraf": "^5.0.5"
},
"engines": {
"node": "^20.x || ^22.x"
"node": "^22.x || ^24.x"
},
"files": [
"/oclif.manifest.json",
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-serverless/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"oclif": "^4.0.0"
},
"engines": {
"node": "^20.x || ^22.x"
"node": "^22.x || ^24.x"
},
"files": [
"/oclif.manifest.json",
Expand Down
2 changes: 1 addition & 1 deletion packages/serverless-twilio-runtime/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Serverless Framework plugin to deploy to the Twilio Runtime.

### Pre-requisites

- Node.js v20.x (this is the runtime version supported by Twilio Functions)
- Node.js v22.x or v24.x (these are the runtime versions supported by Twilio Functions)
- Serverless CLI v1.50.0+. You can run npm i -g serverless if you don't already have it.
- A Twilio account. If you don't have one you can [sign up quickly](https://www.twilio.com/try-twilio).

Expand Down
78 changes: 78 additions & 0 deletions packages/twilio-run/__tests__/checks/deprecated-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { mocked } from 'jest-mock';
import {
checkForDeprecatedRuntime,
DEPRECATED_RUNTIMES,
} from '../../src/checks/deprecated-runtime';
import { logger } from '../../src/utils/logger';

jest.mock('../../src/utils/logger', () => {
return {
logger: {
warn: jest.fn(),
},
};
});

describe('checkForDeprecatedRuntime', () => {
beforeEach(() => {
mocked(logger.warn).mockClear();
});

test('should warn for every deprecated runtime', () => {
DEPRECATED_RUNTIMES.forEach((runtime) => {
expect(checkForDeprecatedRuntime(runtime)).toEqual(false);
});
expect(logger.warn).toHaveBeenCalledTimes(DEPRECATED_RUNTIMES.length);
});

test('should list every even major up to and including 20', () => {
expect(DEPRECATED_RUNTIMES).toEqual([
'node4',
'node6',
'node8',
'node10',
'node12',
'node14',
'node16',
'node18',
'node20',
]);
});

test('should name the configured runtime in the warning', () => {
checkForDeprecatedRuntime('node20');
expect(mocked(logger.warn).mock.calls[0][0]).toContain('node20');
});

test('should not warn for currently supported runtimes', () => {
['node22', 'node24'].forEach((runtime) => {
expect(checkForDeprecatedRuntime(runtime)).toEqual(true);
});
expect(logger.warn).not.toHaveBeenCalled();
});

test('should not warn for runtimes newer than this release knows about', () => {
// An older toolkit must not claim a future runtime is invalid.
['node26', 'node28', 'node30'].forEach((runtime) => {
expect(checkForDeprecatedRuntime(runtime)).toEqual(true);
});
expect(logger.warn).not.toHaveBeenCalled();
});

test('should not warn when no runtime is configured', () => {
expect(checkForDeprecatedRuntime(undefined)).toEqual(true);
expect(logger.warn).not.toHaveBeenCalled();
});

test('should leave unrecognised values to the platform', () => {
expect(checkForDeprecatedRuntime('nope')).toEqual(true);
expect(logger.warn).not.toHaveBeenCalled();
});

test('should not exit the process', () => {
const exit = jest.spyOn(process, 'exit').mockImplementation();
checkForDeprecatedRuntime('node20');
expect(exit).not.toHaveBeenCalled();
exit.mockRestore();
});
});
57 changes: 57 additions & 0 deletions packages/twilio-run/__tests__/checks/nodejs-version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { mocked } from 'jest-mock';
import checkNodejsVersion from '../../src/checks/nodejs-version';
import { logger } from '../../src/utils/logger';

jest.mock('../../src/utils/logger', () => {
return {
logger: {
warn: jest.fn(),
},
};
});

const originalVersion = process.versions.node;

function setNodeVersion(version: string) {
Object.defineProperty(process.versions, 'node', {
value: version,
configurable: true,
});
}

describe('checkNodejsVersion', () => {
beforeEach(() => {
mocked(logger.warn).mockClear();
});

afterEach(() => {
setNodeVersion(originalVersion);
});

test('should warn on deprecated Node.js versions', () => {
['20.11.1', '18.19.0', '16.20.2'].forEach((version) => {
mocked(logger.warn).mockClear();
setNodeVersion(version);
checkNodejsVersion();
expect(logger.warn).toHaveBeenCalledTimes(1);
expect(mocked(logger.warn).mock.calls[0][0]).toContain(version);
});
});

test('should not warn on currently supported Node.js versions', () => {
['22.11.0', '24.0.0'].forEach((version) => {
setNodeVersion(version);
checkNodejsVersion();
});
expect(logger.warn).not.toHaveBeenCalled();
});

test('should not warn on Node.js versions newer than this release knows about', () => {
// An older toolkit must not tell users a newer Node.js version is invalid.
['26.0.0', '28.1.0'].forEach((version) => {
setNodeVersion(version);
checkNodejsVersion();
});
expect(logger.warn).not.toHaveBeenCalled();
});
});
2 changes: 1 addition & 1 deletion packages/twilio-run/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@
"README.md"
],
"engines": {
"node": "^20.x || ^22.x"
"node": "^22.x || ^24.x"
},
"gitHead": "6db273648ed19474f4125042556b10c051529912"
}
41 changes: 41 additions & 0 deletions packages/twilio-run/src/checks/deprecated-runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { stripIndent } from 'common-tags';
import { logger } from '../utils/logger';

/**
* Node.js major versions that are no longer accepted by Twilio Serverless.
*
* This is deliberately a deny list rather than an allow list of supported
* versions: a project pinned to an older version of the toolkit must not warn
* about a runtime that became valid after it was published. A deprecation is a
* settled fact, so an outdated deny list stays correct — at worst it misses a
* newer deprecation, in which case the platform still rejects the build.
*/
export const DEPRECATED_NODE_MAJORS = [4, 6, 8, 10, 12, 14, 16, 18, 20];

export const DEPRECATED_RUNTIMES = DEPRECATED_NODE_MAJORS.map(
(major) => `node${major}`
);

/**
* Warns if a project is configured to deploy to a runtime that Twilio
* Serverless no longer accepts for new builds, pointing at the field to
* change. Twilio Serverless remains the authority on which runtimes it
* accepts, so this only surfaces the problem earlier and never blocks a
* deploy. Unknown or future runtime values are left alone.
*/
export function checkForDeprecatedRuntime(runtime?: string): boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Wouldn't there be a risk of divergence if someone is using toolkit programmatically and doesn't update to the latest version where supported runtimes are changed?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It sounds like it should be checked server-side?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

e.g. when we add node26, the toolkit will tell users their valid runtime "is not supported" Worth double-checking this scenario.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We can invert and use a denylist instead of allowlist, we don't need to keep it updated in order to allow new runtimes, if a runtime is deprecated it will be checked server-side anyways, so keeping it updated is less critical

if (typeof runtime !== 'string' || !DEPRECATED_RUNTIMES.includes(runtime)) {
return true;
}

const title = 'Deprecated Node.js Runtime Configured';
const msg = stripIndent`
Your project is configured to deploy to "${runtime}", which is deprecated and is no longer accepted for new builds.

Please update the "runtime" field in your .twilioserverlessrc (or pass the --runtime flag) to a version that is still supported.
`;

logger.warn(msg, title);

return false;
}
25 changes: 11 additions & 14 deletions packages/twilio-run/src/checks/nodejs-version.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import { stripIndent } from 'common-tags';
import { logger } from '../utils/logger';
import { DEPRECATED_NODE_MAJORS } from './deprecated-runtime';

const SERVERLESS_NODE_JS_VERSION = ['20.', '22.', '24.'];

export function printVersionWarning(
nodeVersion: string,
expectedVersion: string[]
): void {
const title = 'Different Node.js Version Found';
export function printVersionWarning(nodeVersion: string): void {
const title = 'Deprecated Node.js Version Found';
const msg = stripIndent`
You are currently running Node.js ${nodeVersion} on this local machine. The production environment for Twilio Serverless currently supports versions ${expectedVersion}x.
You are currently running Node.js ${nodeVersion} on this local machine. This version is deprecated and is no longer supported by the production environment for Twilio Serverless.

When you deploy to Twilio Serverless, you may encounter differences between local development and production.

Expand All @@ -22,11 +18,12 @@ export function printVersionWarning(

export default function checkNodejsVersion() {
const nodeVersion = process.versions.node;
if (
!SERVERLESS_NODE_JS_VERSION.some((nodeJsVersion) =>
nodeVersion.startsWith(nodeJsVersion)
)
) {
printVersionWarning(nodeVersion, SERVERLESS_NODE_JS_VERSION);
const major = Number.parseInt(nodeVersion.split('.')[0], 10);

// A deny list of deprecated majors rather than an allow list of supported
// ones, so that an older toolkit release never warns about a Node.js version
// that became supported after it was published.
if (DEPRECATED_NODE_MAJORS.includes(major)) {
printVersionWarning(nodeVersion);
}
}
3 changes: 3 additions & 0 deletions packages/twilio-run/src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import path from 'path';
import { Argv } from 'yargs';
import { checkConfigForCredentials } from '../checks/check-credentials';
import { checkForValidRuntimeHandlerVersion } from '../checks/check-runtime-handler';
import { checkForDeprecatedRuntime } from '../checks/deprecated-runtime';
import checkLegacyConfig from '../checks/legacy-config';
import checkProjectStructure from '../checks/project-structure';
import {
Expand Down Expand Up @@ -111,6 +112,8 @@ export async function handler(
return;
}

checkForDeprecatedRuntime(config.runtime);

debug('Deploy Config %P', config);

checkConfigForCredentials(config);
Expand Down
Loading