diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d07dd815..24590a791 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [Unreleased] +- Update Node.js version resolver binary to emit warnings about wide version ranges and enforce the LTS upper bound. ([#1498](https://github.com/heroku/heroku-buildpack-nodejs/pull/1498)) ## [v320] - 2025-12-03 diff --git a/lib/binaries.sh b/lib/binaries.sh index 3ebb0b761..014c03d93 100644 --- a/lib/binaries.sh +++ b/lib/binaries.sh @@ -3,12 +3,12 @@ # Compiled from: https://github.com/heroku/buildpacks-nodejs/blob/main/common/nodejs-utils/src/bin/resolve_version.rs RESOLVE="$BP_DIR/lib/vendor/resolve-version-$(get_os)" -resolve() { - local binary="$1" - local versionRequirement="$2" +resolve_nodejs() { + local node_version="$1" + local lts_major_version="$2" local output - if output=$($RESOLVE "$BP_DIR/inventory/$binary.toml" "$versionRequirement"); then + if output=$($RESOLVE "$BP_DIR/inventory/node.toml" "$node_version" "$lts_major_version"); then if [[ $output = "No result" ]]; then return 1 else @@ -47,52 +47,76 @@ install_yarn() { } install_nodejs() { - local version="${1:-}" + local requested_version="${1:-}" local dir="${2:?}" local code resolve_result local lts_major_version="24" - if [[ -z "$version" ]]; then - version="$lts_major_version.x" + if [[ -z "$requested_version" ]]; then + requested_version="$lts_major_version.x" fi if [[ -n "$NODE_BINARY_URL" ]]; then - url="$NODE_BINARY_URL" - echo "Downloading and installing node from $url" + download_url="$NODE_BINARY_URL" + echo "Downloading and installing node from $download_url" else - echo "Resolving node version $version..." - resolve_result=$(resolve node "$version" || echo "failed") - - read -r number url checksum_name digest < <(echo "$resolve_result") + echo "Resolving node version $requested_version..." + resolve_result=$(resolve_nodejs "$requested_version" "$lts_major_version" || echo "failed") if [[ "$resolve_result" == "failed" ]]; then - fail_bin_install node "$version" + fail_bin_install "$requested_version" "$lts_major_version" + fi + + version=$(echo "$resolve_result" | jq -r .version) + download_url=$(echo "$resolve_result" | jq -r .url) + checksum_type=$(echo "$resolve_result" | jq -r .checksum_type) + checksum_value=$(echo "$resolve_result" | jq -r .checksum_value) + uses_wide_range=$(echo "$resolve_result" | jq .uses_wide_range) + lts_upper_bound_enforced=$(echo "$resolve_result" | jq .lts_upper_bound_enforced) + + if [[ "$uses_wide_range" == "true" ]]; then + echo + echo "! The requested Node.js version is using a wide range ($requested_version) that can resolve to a Node.js major version" + echo " higher than you intended. Limiting the requested range to a major LTS range like \`$lts_major_version.x\` is recommended." + echo " https://devcenter.heroku.com/articles/nodejs-support#specifying-a-node-js-version" + fi + + if [[ "$lts_upper_bound_enforced" == "true" ]]; then + echo + echo "! The resolved Node.js version has been limited to the Active LTS ($version) for the requested range of \`$requested_version\`." + echo " To opt-out of this behavior, set the following config var: \`NODEJS_ALLOW_WIDE_RANGE=true\`" + echo " https://devcenter.heroku.com/articles/nodejs-support#supported-node-js-versions" + fi + + # if either warning message was displayed, ensure we add a newline before continuing with regular output + if [[ "$uses_wide_range" == "true" ]] || [[ "$lts_upper_bound_enforced" == "true" ]]; then + echo fi - echo "Downloading and installing node $number..." + echo "Downloading and installing node $version..." - if [[ "$number" == "22.5.0" ]]; then + if [[ "$version" == "22.5.0" ]]; then warn_about_node_version_22_5_0 fi fi output_file="/tmp/node.tar.gz" - code=$(curl "$url" -L --silent --fail --retry 5 --retry-max-time 15 --retry-connrefused --connect-timeout 5 -o "$output_file" --write-out "%{http_code}") + code=$(curl "$download_url" -L --silent --fail --retry 5 --retry-max-time 15 --retry-connrefused --connect-timeout 5 -o "$output_file" --write-out "%{http_code}") if [ "$code" != "200" ]; then echo "Unable to download node: $code" && false fi if [[ -z "$NODE_BINARY_URL" ]]; then - case "$checksum_name" in + case "$checksum_type" in "sha256") echo "Validating checksum" - if ! echo "$digest $output_file" | sha256sum --check --status; then - echo "Checksum validation failed for Node.js $number - $checksum_name:$digest" && false + if ! echo "$checksum_value $output_file" | sha256sum --check --status; then + echo "Checksum validation failed for Node.js $version - $checksum_type:$checksum_value" && false fi ;; *) - echo "Unsupported checksum for Node.js $number - $checksum_name:$digest" && false + echo "Unsupported checksum for Node.js $version - $checksum_type:$checksum_value" && false ;; esac fi diff --git a/lib/failure.sh b/lib/failure.sh index cbcdacf67..fb20cd4f6 100644 --- a/lib/failure.sh +++ b/lib/failure.sh @@ -218,32 +218,25 @@ fail_yarn_lockfile_outdated() { fail_bin_install() { local error - local bin="$1" - local version="$2" + local version="$1" + local lts_major_version="$2" # Allow the subcommand to fail without trapping the error so we can # get the failing message output set +e # re-request the result, saving off the reason for the failure this time - error=$($RESOLVE "$BP_DIR/inventory/$bin.toml" "$version" 2>&1) + error=$($RESOLVE "$BP_DIR/inventory/node.toml" "$version" "$lts_major_version" 2>&1) # re-enable trapping set -e if [[ $error = "No result" ]]; then - case $bin in - node) - echo "Could not find Node version corresponding to version requirement: $version";; - iojs) - echo "Could not find Iojs version corresponding to version requirement: $version";; - yarn) - echo "Could not find Yarn version corresponding to version requirement: $version";; - esac + echo "Could not find Node version corresponding to version requirement: $version" elif [[ $error == "Could not parse"* ]] || [[ $error == "Could not get"* ]]; then echo "Error: Invalid semantic version \"$version\"" else - echo "Error: Unknown error installing \"$version\" of $bin" + echo "Error: Unknown error installing \"$version\" of node" fi return 1 diff --git a/lib/vendor/resolve-version-linux b/lib/vendor/resolve-version-linux index fd8bacc98..1606208e8 100755 Binary files a/lib/vendor/resolve-version-linux and b/lib/vendor/resolve-version-linux differ diff --git a/makefile b/makefile index d55976ce4..a5802556b 100644 --- a/makefile +++ b/makefile @@ -4,6 +4,7 @@ build-resolvers: build-resolver-linux mkdir -p .build build-resolver-linux: .build + @cargo test --manifest-path ./resolve-version/Cargo.toml CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER="$(shell which x86_64-unknown-linux-musl-gcc)" \ CC_X86_64_UNKNOWN_LINUX_MUSL="$(shell which x86_64-unknown-linux-musl-gcc)" \ cargo build --manifest-path ./resolve-version/Cargo.toml --target x86_64-unknown-linux-musl --profile release diff --git a/resolve-version/Cargo.lock b/resolve-version/Cargo.lock index fdb64c1e3..3d36eb30d 100644 --- a/resolve-version/Cargo.lock +++ b/resolve-version/Cargo.lock @@ -198,6 +198,12 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "libc" version = "0.2.175" @@ -306,10 +312,17 @@ dependencies = [ "hex", "libherokubuildpack", "node-semver", + "serde_json", "sha2", "toml", ] +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "serde" version = "1.0.228" @@ -340,6 +353,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + [[package]] name = "serde_spanned" version = "1.0.3" diff --git a/resolve-version/Cargo.toml b/resolve-version/Cargo.toml index ee2220ded..957cba2b0 100644 --- a/resolve-version/Cargo.toml +++ b/resolve-version/Cargo.toml @@ -11,5 +11,6 @@ libherokubuildpack = { version = "=0.30.2", default-features = false, features = "inventory-sha2", ] } node-semver = "2" +serde_json = "1" sha2 = "0.10.9" toml = "0.9" diff --git a/resolve-version/src/main.rs b/resolve-version/src/main.rs index 443b8d3d1..eeae4da31 100644 --- a/resolve-version/src/main.rs +++ b/resolve-version/src/main.rs @@ -1,20 +1,30 @@ use clap::{Command, arg, value_parser}; use libherokubuildpack::inventory::artifact::{Arch, Os}; +use libherokubuildpack::inventory::version::VersionRequirement; use node_semver::{Range, SemverError, Version}; use sha2::Sha256; use std::env::consts; +use std::ops::Deref; use std::str::FromStr; -use libherokubuildpack::inventory::version::VersionRequirement; const VERSION_REQS_EXIT_CODE: i32 = 1; const INVENTORY_EXIT_CODE: i32 = 2; const UNSUPPORTED_OS_EXIT_CODE: i32 = 3; const UNSUPPORTED_ARCH_EXIT_CODE: i32 = 4; +type NodeInventory = libherokubuildpack::inventory::Inventory>; + +type NodeArtifact = libherokubuildpack::inventory::artifact::Artifact>; + fn main() { + let allow_wide_range = std::env::var("NODEJS_ALLOW_WIDE_RANGE") + .map(|val| val == "true") + .unwrap_or(false); + let matches = Command::new("resolve_version") .arg(arg!()) .arg(arg!()) + .arg(arg!()) .arg(arg!(--os ).value_parser(value_parser!(Os))) .arg(arg!(--arch ).value_parser(value_parser!(Arch))) .get_matches(); @@ -27,6 +37,11 @@ fn main() { .get_one::("node_version") .expect("required argument"); + let lts_major_version = matches + .get_one::("lts_major_version") + .map(|v| v.parse::().expect("must be a positive number")) + .expect("required argument"); + let os = match matches.get_one::("os") { Some(os) => *os, None => consts::OS.parse::().unwrap_or_else(|e| { @@ -48,32 +63,88 @@ fn main() { std::process::exit(VERSION_REQS_EXIT_CODE); }); - let inv_contents = std::fs::read_to_string(inventory_path).unwrap_or_else(|e| { + let inventory_contents = std::fs::read_to_string(inventory_path).unwrap_or_else(|e| { eprintln!("Error reading '{inventory_path}': {e}"); std::process::exit(INVENTORY_EXIT_CODE); }); - let inv: libherokubuildpack::inventory::Inventory> = - toml::from_str(&inv_contents).unwrap_or_else(|e| { - eprintln!("Error parsing '{inventory_path}': {e}"); - std::process::exit(INVENTORY_EXIT_CODE); - }); - - let version = inv.resolve(os, arch, &version_requirements); + let node_inventory: NodeInventory = toml::from_str(&inventory_contents).unwrap_or_else(|e| { + eprintln!("Error parsing '{inventory_path}': {e}"); + std::process::exit(INVENTORY_EXIT_CODE); + }); - if let Some(version) = version { + if let Some((artifact, uses_wide_range, lts_upper_bound_enforced)) = resolve_node_artifact( + &node_inventory, + os, + arch, + &version_requirements, + lts_major_version, + allow_wide_range, + ) { println!( - "{} {} {} {}", - version.version, - version.url, - version.checksum.name, - hex::encode(&version.checksum.value) + "{}", + serde_json::json!({ + "version": artifact.version, + "url": artifact.url, + "checksum_type": artifact.checksum.name, + "checksum_value": hex::encode(&artifact.checksum.value), + "uses_wide_range": *uses_wide_range, + "lts_upper_bound_enforced": *lts_upper_bound_enforced, + }) ); } else { println!("No result"); } } +fn resolve_node_artifact<'a>( + node_inventory: &'a NodeInventory, + os: Os, + arch: Arch, + requirement: &Requirement, + lts_major_version: i64, + allow_wide_range: bool, +) -> Option<(&'a NodeArtifact, UsesWideRange, LtsUpperBoundEnforced)> { + let lts_range_value = format!("{lts_major_version}.x"); + let lts_range = Requirement::from_str(<s_range_value) + .unwrap_or_else(|_| panic!("Range {lts_range_value} should be valid")); + + if let Some(resolved_artifact) = node_inventory.resolve(os, arch, requirement) + && let Some(highest_lts_artifact) = node_inventory.resolve(os, arch, <s_range) + { + let uses_wide_range = + if requirement.satisfies(&Version::new(resolved_artifact.version.major - 1, 0, 0)) + || requirement.satisfies(&Version::new(resolved_artifact.version.major + 1, 0, 0)) + { + UsesWideRange(true) + } else { + UsesWideRange(false) + }; + + let lts_upper_bound_enforced = if allow_wide_range { + LtsUpperBoundEnforced(false) + } else if resolved_artifact.version > highest_lts_artifact.version + && let Some(min_version) = requirement.deref().min_version() + && min_version <= highest_lts_artifact.version + { + LtsUpperBoundEnforced(true) + } else { + LtsUpperBoundEnforced(false) + }; + Some(( + if *lts_upper_bound_enforced { + highest_lts_artifact + } else { + resolved_artifact + }, + uses_wide_range, + lts_upper_bound_enforced, + )) + } else { + None + } +} + pub struct Requirement(Range); impl FromStr for Requirement { @@ -108,11 +179,42 @@ impl VersionRequirement for Requirement { } } +impl Deref for Requirement { + type Target = Range; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +struct UsesWideRange(bool); + +impl Deref for UsesWideRange { + type Target = bool; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +struct LtsUpperBoundEnforced(bool); + +impl Deref for LtsUpperBoundEnforced { + type Target = bool; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} #[cfg(test)] mod tests { use super::*; + const TEST_LTS_MAJOR_VERSION: i64 = 24; + const ALLOW_WIDE_RANGE: bool = true; + const DISALLOW_WIDE_RANGE: bool = false; + #[test] fn parse_handles_latest() { let result = Requirement::from_str("latest"); @@ -221,4 +323,264 @@ mod tests { let result = Requirement::from_str("12.%"); assert!(result.is_err()); } + + #[test] + fn resolve_version_when_wide_range_used_and_version_is_downgraded_to_lts() { + let wide_requirement = Requirement::from_str(">= 22").unwrap(); + let inventory = create_inventory(); + let (artifact, show_wide_range_warning, show_downgrade_warning) = resolve_node_artifact( + &inventory, + Os::Linux, + Arch::Amd64, + &wide_requirement, + TEST_LTS_MAJOR_VERSION, + DISALLOW_WIDE_RANGE, + ) + .unwrap(); + assert_eq!(artifact.version.major, 24); + assert!(*show_wide_range_warning); + assert!(*show_downgrade_warning); + } + + #[test] + fn resolve_version_when_narrow_range_used_and_version_is_not_downgraded_to_lts() { + let wide_requirement = Requirement::from_str("22.x").unwrap(); + let inventory = create_inventory(); + let (artifact, show_wide_range_warning, show_downgrade_warning) = resolve_node_artifact( + &inventory, + Os::Linux, + Arch::Amd64, + &wide_requirement, + TEST_LTS_MAJOR_VERSION, + DISALLOW_WIDE_RANGE, + ) + .unwrap(); + assert_eq!(artifact.version.major, 22); + assert!(!*show_wide_range_warning); + assert!(!*show_downgrade_warning); + } + + #[test] + fn resolve_version_when_exact_range_used_and_version_is_not_downgraded_to_lts() { + let wide_requirement = Requirement::from_str("22.21.0").unwrap(); + let inventory = create_inventory(); + let (artifact, show_wide_range_warning, show_downgrade_warning) = resolve_node_artifact( + &inventory, + Os::Linux, + Arch::Amd64, + &wide_requirement, + TEST_LTS_MAJOR_VERSION, + DISALLOW_WIDE_RANGE, + ) + .unwrap(); + assert_eq!(artifact.version.major, 22); + assert!(!*show_wide_range_warning); + assert!(!*show_downgrade_warning); + } + + #[test] + fn resolve_version_when_wide_range_used_and_version_is_lts() { + let wide_requirement = Requirement::from_str(">= 24").unwrap(); + let inventory = create_inventory(); + let (artifact, show_wide_range_warning, show_downgrade_warning) = resolve_node_artifact( + &inventory, + Os::Linux, + Arch::Amd64, + &wide_requirement, + TEST_LTS_MAJOR_VERSION, + DISALLOW_WIDE_RANGE, + ) + .unwrap(); + assert_eq!(artifact.version.major, 24); + assert!(*show_wide_range_warning); + assert!(*show_downgrade_warning); + } + + #[test] + fn resolve_version_when_narrow_range_used_and_version_is_lts() { + let wide_requirement = Requirement::from_str("24.x").unwrap(); + let inventory = create_inventory(); + let (artifact, show_wide_range_warning, show_downgrade_warning) = resolve_node_artifact( + &inventory, + Os::Linux, + Arch::Amd64, + &wide_requirement, + TEST_LTS_MAJOR_VERSION, + DISALLOW_WIDE_RANGE, + ) + .unwrap(); + assert_eq!(artifact.version.major, 24); + assert!(!*show_wide_range_warning); + assert!(!*show_downgrade_warning); + } + + #[test] + fn resolve_version_when_exact_lts_range_used() { + let wide_requirement = Requirement::from_str("24.10.0").unwrap(); + let inventory = create_inventory(); + let (artifact, show_wide_range_warning, show_downgrade_warning) = resolve_node_artifact( + &inventory, + Os::Linux, + Arch::Amd64, + &wide_requirement, + TEST_LTS_MAJOR_VERSION, + DISALLOW_WIDE_RANGE, + ) + .unwrap(); + assert_eq!(artifact.version.major, 24); + assert!(!*show_wide_range_warning); + assert!(!*show_downgrade_warning); + } + + #[test] + fn resolve_version_when_wide_range_is_used_and_explicitly_requesting_range_beyond_lts() { + let wide_requirement = Requirement::from_str(">=25.x").unwrap(); + let inventory = create_inventory(); + let (artifact, show_wide_range_warning, show_downgrade_warning) = resolve_node_artifact( + &inventory, + Os::Linux, + Arch::Amd64, + &wide_requirement, + TEST_LTS_MAJOR_VERSION, + DISALLOW_WIDE_RANGE, + ) + .unwrap(); + assert_eq!(artifact.version.major, 25); + assert!(*show_wide_range_warning); + assert!(!*show_downgrade_warning); + } + + #[test] + fn resolve_version_when_narrow_range_is_used_and_explicitly_requesting_range_beyond_lts() { + let wide_requirement = Requirement::from_str("25.x").unwrap(); + let inventory = create_inventory(); + let (artifact, show_wide_range_warning, show_downgrade_warning) = resolve_node_artifact( + &inventory, + Os::Linux, + Arch::Amd64, + &wide_requirement, + TEST_LTS_MAJOR_VERSION, + DISALLOW_WIDE_RANGE, + ) + .unwrap(); + assert_eq!(artifact.version.major, 25); + assert!(!*show_wide_range_warning); + assert!(!*show_downgrade_warning); + } + + #[test] + fn resolve_version_when_exact_range_is_used_and_explicitly_requesting_range_beyond_lts() { + let wide_requirement = Requirement::from_str("25.0.0").unwrap(); + let inventory = create_inventory(); + let (artifact, show_wide_range_warning, show_downgrade_warning) = resolve_node_artifact( + &inventory, + Os::Linux, + Arch::Amd64, + &wide_requirement, + TEST_LTS_MAJOR_VERSION, + DISALLOW_WIDE_RANGE, + ) + .unwrap(); + assert_eq!(artifact.version.major, 25); + assert!(!*show_wide_range_warning); + assert!(!*show_downgrade_warning); + } + + #[test] + fn resolve_version_with_complex_range_with_upper_bound_within_lts() { + let wide_requirement = Requirement::from_str(">=22.x <25.x").unwrap(); + let inventory = create_inventory(); + let (artifact, show_wide_range_warning, show_downgrade_warning) = resolve_node_artifact( + &inventory, + Os::Linux, + Arch::Amd64, + &wide_requirement, + TEST_LTS_MAJOR_VERSION, + DISALLOW_WIDE_RANGE, + ) + .unwrap(); + assert_eq!(artifact.version.major, 24); + assert!(*show_wide_range_warning); + assert!(!*show_downgrade_warning); + } + + #[test] + fn resolve_version_with_complex_range_with_upper_bound_beyond_lts() { + let wide_requirement = Requirement::from_str(">=25.x <27.x").unwrap(); + let inventory = create_inventory(); + let (artifact, show_wide_range_warning, show_downgrade_warning) = resolve_node_artifact( + &inventory, + Os::Linux, + Arch::Amd64, + &wide_requirement, + TEST_LTS_MAJOR_VERSION, + DISALLOW_WIDE_RANGE, + ) + .unwrap(); + assert_eq!(artifact.version.major, 25); + assert!(*show_wide_range_warning); + assert!(!*show_downgrade_warning); + } + + #[test] + fn resolve_version_with_complex_range_with_lower_and_upper_bounds_within_lts() { + let wide_requirement = Requirement::from_str(">=24.x <25.x").unwrap(); + let inventory = create_inventory(); + let (artifact, show_wide_range_warning, show_downgrade_warning) = resolve_node_artifact( + &inventory, + Os::Linux, + Arch::Amd64, + &wide_requirement, + TEST_LTS_MAJOR_VERSION, + DISALLOW_WIDE_RANGE, + ) + .unwrap(); + assert_eq!(artifact.version.major, 24); + assert!(!*show_wide_range_warning); + assert!(!*show_downgrade_warning); + } + + #[test] + fn resolve_version_with_wide_range_environment_override_to_prevent_downgrade() { + let wide_requirement = Requirement::from_str(">=22.x").unwrap(); + let inventory = create_inventory(); + let (artifact, show_wide_range_warning, show_downgrade_warning) = resolve_node_artifact( + &inventory, + Os::Linux, + Arch::Amd64, + &wide_requirement, + TEST_LTS_MAJOR_VERSION, + ALLOW_WIDE_RANGE, + ) + .unwrap(); + assert_eq!(artifact.version.major, 25); + assert!(*show_wide_range_warning); + assert!(!*show_downgrade_warning); + } + + fn create_inventory() -> NodeInventory { + let contents = r#" + [[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" + arch = "amd64" + url = "https://nodejs.org/download/release/v24.10.0/node-v24.10.0-linux-x64.tar.gz" + checksum = "sha256:2b03c5417ce0b1076780df00e01da373bead3b4b80d1c78c1ad10ee7b918d90c" + + [[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" + "#; + NodeInventory::from_str(contents).unwrap() + } } diff --git a/test/fixtures/version-bounds-explicitly-greater-than-lts/package-lock.json b/test/fixtures/version-bounds-explicitly-greater-than-lts/package-lock.json new file mode 100644 index 000000000..0ad366a5e --- /dev/null +++ b/test/fixtures/version-bounds-explicitly-greater-than-lts/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "version-bounds-explicitly-greater-than-lts", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "version-bounds-explicitly-greater-than-lts", + "engines": { + "node": ">=25" + } + } + } +} diff --git a/test/fixtures/version-bounds-explicitly-greater-than-lts/package.json b/test/fixtures/version-bounds-explicitly-greater-than-lts/package.json new file mode 100644 index 000000000..476b3977e --- /dev/null +++ b/test/fixtures/version-bounds-explicitly-greater-than-lts/package.json @@ -0,0 +1,6 @@ +{ + "name": "version-bounds-explicitly-greater-than-lts", + "engines": { + "node": ">=25" + } +} diff --git a/test/fixtures/version-bounds-narrow/package-lock.json b/test/fixtures/version-bounds-narrow/package-lock.json new file mode 100644 index 000000000..a62502fbc --- /dev/null +++ b/test/fixtures/version-bounds-narrow/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "version-bounds-narrow", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "version-bounds-narrow", + "engines": { + "node": ">=22.x" + } + } + } +} diff --git a/test/fixtures/version-bounds-narrow/package.json b/test/fixtures/version-bounds-narrow/package.json new file mode 100644 index 000000000..5c4d32ed5 --- /dev/null +++ b/test/fixtures/version-bounds-narrow/package.json @@ -0,0 +1,6 @@ +{ + "name": "version-bounds-narrow", + "engines": { + "node": "22.x" + } +} diff --git a/test/fixtures/version-bounds-wide/package-lock.json b/test/fixtures/version-bounds-wide/package-lock.json new file mode 100644 index 000000000..07173dd28 --- /dev/null +++ b/test/fixtures/version-bounds-wide/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "version-bounds-wide", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "version-bounds-wide", + "engines": { + "node": ">=22" + } + } + } +} diff --git a/test/fixtures/version-bounds-wide/package.json b/test/fixtures/version-bounds-wide/package.json new file mode 100644 index 000000000..d415f29ab --- /dev/null +++ b/test/fixtures/version-bounds-wide/package.json @@ -0,0 +1,6 @@ +{ + "name": "version-bounds-wide", + "engines": { + "node": ">=22" + } +} diff --git a/test/run b/test/run index 1f2a4f700..1e55ec040 100755 --- a/test/run +++ b/test/run @@ -2106,6 +2106,37 @@ testErrorYarnLockfileOutOfSync() { assertMetricEqualsString "${cache_dir}" "failure" "yarn-lockfile-out-of-sync" } +# Node version bounds +testOptOutOfWideVersionDowngrade() { + env_dir=$(mktmpdir) + echo "true" > "$env_dir/NODEJS_ALLOW_WIDE_RANGE" + compile "version-bounds-wide" "$(mktmpdir)" "$env_dir" + assertCaptured "! The requested Node.js version is using a wide range" + assertNotCaptured "! The resolved Node.js version has been limited to the Active LTS" + assertCapturedSuccess +} + +testVersionBoundsWide() { + compile "version-bounds-wide" + assertCaptured "! The requested Node.js version is using a wide range" + assertCaptured "! The resolved Node.js version has been limited to the Active LTS" + assertCapturedSuccess +} + +testVersionBoundsNarrow() { + compile "version-bounds-narrow" + assertNotCaptured "! The requested Node.js version is using a wide range" + assertNotCaptured "! The resolved Node.js version has been limited to the Active LTS" + assertCapturedSuccess +} + +testVersionBoundsExplicitlyGreaterThanLTS() { + compile "version-bounds-explicitly-greater-than-lts" + assertCaptured "! The requested Node.js version is using a wide range" + assertNotCaptured "! The resolved Node.js version has been limited to the Active LTS" + assertCapturedSuccess +} + # Utils pushd "$(dirname 0)" >/dev/null