
Turn a security release into a reviewable deployment action
On 21 October 2026, official Node.js notes identified 22.23.4, 24.22.1 and 26.12.1 as security releases. Reading a version number is only the beginning of an operational response. Identify which executable each service actually runs and how upgrade evidence will be recorded. This tutorial turns a dated baseline into a small testable acceptance gate rather than leaving the release announcement as an untracked reminder.
The policy stores a reviewed minimum separately for each major branch. Comparison uses numeric minor and patch values within the same major, not lexical string ordering. Unknown branches and prereleases are refused because this policy has not reviewed them. That refusal does not prove that every other version is vulnerable. It establishes only whether an observed runtime meets the limited baseline whose review date and primary references are recorded here.
A version gate with an explicit review date
The policy also has a review date and maximum age. After that interval, the gate requires another review instead of treating today's patch list as permanent assurance. This example uses 25 October 2026 and thirty days. Before real use, review later advisories and branch support status, then update the policy through the project's change process. A locally stored JSON file is useful only when an owner is responsible for keeping it current.
check-node assesses the running process. For a containerized service, execute the check inside the image or actual service environment. Updating the host terminal's Node.js does not update an older container image. upgrade-evidence starts a child using the same executable and records its result, architecture and check time. Exclusive file creation preserves an existing evidence record rather than silently replacing the previous observation with a newer one.
Tests cover each baseline, the preceding patch, unknown majors, prereleases, expired policy and a policy dated in the future. These verify gate decisions; they are not a complete vulnerability scan or an application compatibility suite. After changing the runtime, the real deployment workflow should still test application behavior, TLS, HTTP requests and native packages. Passing a version comparison does not establish that those separate acceptance conditions have been satisfied.
Complete files, execution and tests
Node.js 22+ to run the tools; assess the runtime against the dated policy. Tests require no network. Save the four complete files below together using the stated filenames; no code is replaced by ellipses.
1. A dated version policy
File: node-policy.json. Save this complete file without removing lines.
{
"reviewedOn": "2026-10-25",
"maximumAgeDays": 30,
"minimumByMajor": {
"22": "22.23.4",
"24": "24.22.1",
"26": "26.12.1"
}
}These values are the baseline reviewed on 25 October 2026. Recheck official notes and policy age before later use; a passing version today is not permanent assurance.
2. A machine-readable version gate
File: check-node.mjs. Save this complete file without removing lines.
import { readFileSync } from 'node:fs';
import { pathToFileURL } from 'node:url';
export function assessVersion(version, policy, today = new Date().toISOString().slice(0, 10)) {
const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(version);
if (!match) return { allowed: false, reason: 'Not a reviewed stable version', version };
const current = match.slice(1).map(Number);
const minimum = policy.minimumByMajor[String(current[0])];
if (!minimum) return { allowed: false, reason: 'Major branch is not in this policy', version };
const age = (Date.parse(today) - Date.parse(policy.reviewedOn)) / 86400000;
if (!Number.isFinite(age) || age < 0 || age > policy.maximumAgeDays) {
return { allowed: false, reason: 'Policy needs a fresh review', version, minimum };
}
const required = minimum.split('.').map(Number);
const allowed = current[1] > required[1] || (current[1] === required[1] && current[2] >= required[2]);
return { allowed, version, minimum, reviewedOn: policy.reviewedOn,
reason: allowed ? 'Meets this dated baseline' : 'Below the reviewed baseline' };
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const policy = JSON.parse(readFileSync(new URL('./node-policy.json', import.meta.url), 'utf8'));
const result = assessVersion(process.version, policy);
console.log(JSON.stringify(result, null, 2));
process.exitCode = result.allowed ? 0 : 1;
}Run node check-node.mjs. Exit zero passes this policy and exit one stops the workflow. The script checks the current interpreter and installs or upgrades nothing.
3. Boundary-version and policy-expiry tests
File: test-node-policy.mjs. Save this complete file without removing lines.
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { assessVersion } from './check-node.mjs';
const policy = JSON.parse(readFileSync(new URL('./node-policy.json', import.meta.url), 'utf8'));
const check = version => assessVersion(version, policy, '2026-10-25');
test('all reviewed baseline versions pass', () => {
for (const version of ['22.23.4', '24.22.1', 'v26.12.1', '24.23.0']) {
assert.equal(check(version).allowed, true);
}
});
test('previous patches, unknown branches and prereleases are refused', () => {
for (const version of ['22.23.3', '24.22.0', '26.12.0', '25.9.0', '26.12.1-rc.1', 'not-a-version']) {
assert.equal(check(version).allowed, false);
}
});
test('an old or future policy is not treated as current assurance', () => {
assert.equal(assessVersion('24.22.1', policy, '2026-12-01').allowed, false);
assert.equal(assessVersion('24.22.1', policy, '2026-10-01').allowed, false);
});Run node --test test-node-policy.mjs. The three test groups use an explicit date so results do not drift with the workstation or CI clock.
4. Record target-runtime evidence
File: upgrade-evidence.mjs. Save this complete file without removing lines.
import { spawnSync } from 'node:child_process';
import { readFileSync, writeFileSync } from 'node:fs';
import { assessVersion } from './check-node.mjs';
const probe = spawnSync(process.execPath, ['--version'], { encoding: 'utf8', shell: false });
if (probe.status !== 0) throw new Error('The selected runtime could not report its version');
const policy = JSON.parse(readFileSync(new URL('./node-policy.json', import.meta.url), 'utf8'));
const assessment = assessVersion(probe.stdout.trim(), policy);
const evidence = { checkedAt: new Date().toISOString(), platform: process.platform,
architecture: process.arch, assessment };
writeFileSync('upgrade-evidence.json', JSON.stringify(evidence, null, 2) + '\n', { flag: 'wx' });
console.log(JSON.stringify(evidence, null, 2));
process.exitCode = assessment.allowed ? 0 : 1;Run this file in the test working directory. It exclusively creates upgrade-evidence.json and returns the gate decision; exit one is expected on an older runtime.
Actual-runtime evidence and the next policy review
Save the four files together and run node --test test-node-policy.mjs. Then execute node check-node.mjs using the target runtime. Exit zero means the runtime meets this dated policy; exit one stops the workflow for review. Rejection is expected on an older interpreter. The tutorial does not upgrade a system runtime or retrieve policy from the network. Make the actual version change through the project's controlled deployment mechanism and retain its evidence.
Implementation checklist
- Use the official notes for the same major branch.
- Check the actual service or container process.
- Require a fresh review when policy expires.
- Run application and native-dependency acceptance tests after upgrading.
References
Editorial guidance: Liyan Knowledge.



