diff --git a/CHANGELOG.md b/CHANGELOG.md index 8365923de..00c24fb65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +- Added Node.js 25.0.0 (linux-amd64) +- Added Node.js 22.21.0 (linux-amd64) ## [v314] - 2025-10-09 diff --git a/inventory/node.toml b/inventory/node.toml index bed40f13c..c04e96d85 100644 --- a/inventory/node.toml +++ b/inventory/node.toml @@ -1,3 +1,10 @@ +[[artifacts]] +version = "25.0.0" +os = "linux" +arch = "amd64" +url = "https://nodejs.org/download/release/v25.0.0/node-v25.0.0-linux-x64.tar.gz" +checksum = "sha256:28dd46a6733192647d7c8267343f5a3f1c616f773c448e2c0d2539ae70724b40" + [[artifacts]] version = "24.10.0" os = "linux" @@ -194,6 +201,13 @@ arch = "amd64" url = "https://nodejs.org/download/release/v23.0.0/node-v23.0.0-linux-x64.tar.gz" checksum = "sha256:702cbc710fcf1102cef1aced74443fee34eff8df4827de30ec970d377ce31d9e" +[[artifacts]] +version = "22.21.0" +os = "linux" +arch = "amd64" +url = "https://nodejs.org/download/release/v22.21.0/node-v22.21.0-linux-x64.tar.gz" +checksum = "sha256:262b84b02f7e2bc017d4bdb81fec85ca0d6190a5cd0781d2d6e84317c08871f8" + [[artifacts]] version = "22.20.0" os = "linux" diff --git a/spec/ci/node_25_metrics_spec.rb b/spec/ci/node_25_metrics_spec.rb new file mode 100644 index 000000000..82d343cdd --- /dev/null +++ b/spec/ci/node_25_metrics_spec.rb @@ -0,0 +1,25 @@ +require_relative '../spec_helper' + +describe "Node Metrics for v25.x" do + context "test metrics for Node v25.x app" do + let(:app) { + Hatchet::Runner.new( + "spec/fixtures/repos/node-25-metrics", + config: { + "HEROKU_METRICS_URL" => "http://localhost:3000", + "METRICS_INTERVAL_OVERRIDE" => "10000" + } + ) + } + + it "should deploy" do + app.deploy do |app| + data = successful_json_body(app) + expect(data["gauges"]["node.eventloop.delay.ms.max"]).to be >= 2000 + expect(data["counters"]["node.gc.collections"]).to be >= 0 + expect(data["counters"]["node.gc.young.collections"]).to be >= 0 + expect(data["counters"]["node.gc.old.collections"]).to be >= 0 + end + end + end +end diff --git a/spec/ci/node_25_spec.rb b/spec/ci/node_25_spec.rb new file mode 100644 index 000000000..14a784e52 --- /dev/null +++ b/spec/ci/node_25_spec.rb @@ -0,0 +1,15 @@ +require_relative '../spec_helper' + +describe "Hello World for Node v25.x" do + context "a single-process Node v25.x app" do + let(:app) { + Hatchet::Runner.new("spec/fixtures/repos/node-25") + } + + it "should deploy successfully" do + app.deploy do |app| + expect(successful_body(app).strip).to eq("Hello, world!") + end + end + end +end diff --git a/spec/fixtures/repos/node-25-metrics/Procfile b/spec/fixtures/repos/node-25-metrics/Procfile new file mode 100644 index 000000000..1da0cd6f6 --- /dev/null +++ b/spec/fixtures/repos/node-25-metrics/Procfile @@ -0,0 +1 @@ +web: node index.js diff --git a/spec/fixtures/repos/node-25-metrics/index.js b/spec/fixtures/repos/node-25-metrics/index.js new file mode 100755 index 000000000..eae912a05 --- /dev/null +++ b/spec/fixtures/repos/node-25-metrics/index.js @@ -0,0 +1,72 @@ +#!/usr/bin/env node + +const http = require('http'); +const EventEmitter = require('events'); + +const PORT = process.env.PORT || 5000; +const Events = new EventEmitter(); + +// This will block the event loop for ~lengths of time +function blockCpuFor(ms) { + return new Promise((resolve, reject) => { + setTimeout(() => { + console.log(`blocking the event loop for ${ms}ms`); + let now = new Date().getTime(); + let result = 0 + while(true) { + result += Math.random() * Math.random(); + if (new Date().getTime() > now + ms) + break; + } + resolve(); + }, 100); + }); +} + +function getNextMetricsEvent() { + return new Promise((resolve, reject) => Events.once('metrics', resolve)); +} + +const server = http.createServer((req, res) => { + // wait for the next metrics event + getNextMetricsEvent() + .then(blockCpuFor(2000)) + .then(blockCpuFor(100)) + .then(blockCpuFor(100)) + .then(blockCpuFor(100)) + .then(blockCpuFor(100)) + .then(blockCpuFor(100)) + .then(blockCpuFor(100)) + .then(blockCpuFor(100)) + .then(blockCpuFor(100)) + .then(blockCpuFor(100)) + .then(blockCpuFor(100)) + // gather the next metrics data which should include these pauses + .then(getNextMetricsEvent()) + .then(data => { + res.setHeader('Content-Type', 'application/json'); + res.end(data); + }) + .catch(() => { + res.statusCode = 500; + res.end("Something went wrong"); + }); +}); + +server.listen(PORT, () => console.log(`Listening on ${PORT}`)); + +// Create a second server that intercepts the HTTP requests +// sent by the metrics plugin +const metricsListener = http.createServer((req, res) => { + if (req.method == 'POST') { + let body = ''; + req.on('data', (data) => body += data); + req.on('end', () => { + res.statusCode = 200; + res.end(); + Events.emit('metrics', body) + }); + } +}); + +metricsListener.listen(3000, () => console.log('Listening for metrics on 3000')); diff --git a/spec/fixtures/repos/node-25-metrics/package.json b/spec/fixtures/repos/node-25-metrics/package.json new file mode 100644 index 000000000..28f216cf2 --- /dev/null +++ b/spec/fixtures/repos/node-25-metrics/package.json @@ -0,0 +1,11 @@ +{ + "name": "node-metrics-test-app", + "version": "1.0.0", + "engines": { + "node": "25.x" + }, + "main": "index.js", + "license": "MIT", + "devDependencies": {}, + "dependencies": {} +} diff --git a/spec/fixtures/repos/node-25/Procfile b/spec/fixtures/repos/node-25/Procfile new file mode 100644 index 000000000..1da0cd6f6 --- /dev/null +++ b/spec/fixtures/repos/node-25/Procfile @@ -0,0 +1 @@ +web: node index.js diff --git a/spec/fixtures/repos/node-25/app.json b/spec/fixtures/repos/node-25/app.json new file mode 100644 index 000000000..63b5c96f2 --- /dev/null +++ b/spec/fixtures/repos/node-25/app.json @@ -0,0 +1,3 @@ +{ + "name": "hello-world" +} diff --git a/spec/fixtures/repos/node-25/index.js b/spec/fixtures/repos/node-25/index.js new file mode 100755 index 000000000..6a0090ff5 --- /dev/null +++ b/spec/fixtures/repos/node-25/index.js @@ -0,0 +1,13 @@ +#!/usr/bin/env node + +const http = require('http'); + +const PORT = process.env.PORT || 5000; + +const server = http.createServer((_req, res) => { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.end("Hello, world!"); +}) + +server.listen(PORT, () => console.log(`Listening on ${PORT}`)); diff --git a/spec/fixtures/repos/node-25/package.json b/spec/fixtures/repos/node-25/package.json new file mode 100644 index 000000000..22c8bf850 --- /dev/null +++ b/spec/fixtures/repos/node-25/package.json @@ -0,0 +1,21 @@ +{ + "name": "hello-world", + "version": "1.0.0", + "engines": { + "node": "25.x" + }, + "scripts": { + "prettify": "prettier --single-quote --trailing-comma all --write 'bin/*' 'src/**/*.js'", + "test": "jest --silent", + "dev": "nodemon --watch . --watch src/* src/index.js", + "build": "echo NODE_OPTIONS: $NODE_OPTIONS" + }, + "main": "index.js", + "license": "MIT", + "devDependencies": { + "jest": "^19.0.2", + "nodemon": "^1.19.4", + "prettier": "^0.22.0" + }, + "dependencies": {} +} diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b7db54831..73ba28a45 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -78,7 +78,7 @@ def resolve_all_supported_node_versions(options = {}) end def version_supports_metrics(version) - SemVersion.new(version).satisfies?('>= 10.0.0') && SemVersion.new(version).satisfies?('< 25.0.0') + SemVersion.new(version).satisfies?('>= 10.0.0') && SemVersion.new(version).satisfies?('< 26.0.0') end def get_test_versions @@ -87,7 +87,7 @@ def get_test_versions elsif ENV['TEST_ALL_NODE_VERSIONS'] == 'true' versions = resolve_all_supported_node_versions() else - versions = resolve_node_version(['20.x', '22.x', '24.x']) + versions = resolve_node_version(['20.x', '22.x', '24.x', '25.x']) end puts("Running tests for Node versions: #{versions.join(', ')}") versions diff --git a/test/run b/test/run index 268ddca8d..2313ed97d 100755 --- a/test/run +++ b/test/run @@ -1495,7 +1495,7 @@ testYarnBuildMetaData() { assertMetrics "${cache_dir}" <<-'EOF' select(.build_step == "finished") select(.build_time | type == "number") - select(.bundled_npm_version == "10.9.3") + select(.bundled_npm_version | test("10.\\d+.\\d+")) select(.cache_status == "not-found") select(.has_cached_bower_components == false) select(.has_custom_cache_dirs == false)