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
32 changes: 25 additions & 7 deletions .github/workflows/update-versions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,27 +42,44 @@ jobs:
if git diff --quiet && git diff --staged --quiet; then
echo "No changes to versions.json."
echo "sha=${{ github.sha }}" >> "$GITHUB_OUTPUT"
else
elif [[ "${{ github.ref_name }}" != "main" ]]; then
git commit -m "chore: update Node.js versions [skip ci]"
git push
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
echo "commit='chore: update Node.js versions'" >> "$GITHUB_OUTPUT"
echo "commit=chore: update Node.js versions" >> "$GITHUB_OUTPUT"
else
echo "sha=${{ github.sha }}" >> "$GITHUB_OUTPUT"
echo "commit=chore: update Node.js versions" >> "$GITHUB_OUTPUT"
fi
- name: Create pull request
if: ${{ steps.commit.outputs.commit }}
if: steps.commit.outputs.commit != '' && ( github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' )
uses: peter-evans/create-pull-request@v8
with:
commit-message: ${{ steps.update.outputs.commit }}
title: ${{ steps.update.outputs.commit }}
commit-message: ${{ steps.commit.outputs.commit }}
title: ${{ steps.commit.outputs.commit }}
body: |
Automatically created pull-request to update Node versions.
delete-branch: true
reviewers: davidnbr

path-filter:
runs-on: ubuntu-latest
outputs:
versions: ${{ steps.paths.outputs.versions }}
steps:
- uses: actions/checkout@v6
- name: Path filtering
id: paths
uses: dorny/paths-filter@v4
with:
filters: |
versions:
- versions.json

generate-matrix:
if: ${{ needs.update.outputs.commit_message }}
if: needs.update.outputs.commit_message != '' || needs.path-filter.outputs.versions == 'true'
runs-on: ubuntu-latest
needs: [update]
needs: [update, path-filter]
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
Expand All @@ -75,6 +92,7 @@ jobs:
echo "matrix=$JSON" >> "$GITHUB_OUTPUT"

build:
if: github.ref == 'refs/heads/main'
runs-on: ${{ matrix.runner.os }}
needs: [generate-matrix, update]
strategy:
Expand Down
38 changes: 29 additions & 9 deletions scripts/fetch-node-versions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,31 @@ resolve_attr_name() {
fi
}

# Extract Node.js major.minor from commit messages like:
# "nodejs_22: 22.14.0 -> 22.15.0"
# "nodejs: 20.8.0 -> 20.9.0"
# "update nodejs to 22.12.0"
# Extract the major.minor of the version *landing* at this commit's rev, from
# messages like:
# "nodejs_22: 22.14.0 -> 22.15.0" -> 22.15 (TO side; this rev ships 22.15.0)
# "nodejs-16_x: 16.20.1 -> 16.20.2" -> 16.20
# "nodejs: 20.8.0 -> 20.9.0" -> 20.9
# "nodejs_25: init at 25.2.1" -> 25.2
# "nodejs_24: bump to 24.15.0" -> 24.15 (last-resort: first X.Y after attr)
#
# Only matches version-bump commits for the nodejs/nodejs_NN/nodejs-NN_x
# attrs themselves. Deliberately does NOT match "nodejs_latest: ... -> ..."
# or other aliases - those bumps don't correspond to this rev shipping that
# version under the matched attr, and would otherwise pick the wrong rev
# (e.g. a rev where the version has since been marked EOL/removed).
extract_version() {
local -r msg="$1"
# Match patterns like "22.14.0" or "22.14" preceded by a word boundary context
echo "$msg" | grep -oP '(?:^|[\s:>])(\d+\.\d+)(?:\.\d+)?' | grep -oE '[0-9]+\.[0-9]+' | head -1 || true
local -r attr_re='^nodejs(?:-[0-9]+_x|_[0-9]+)?:'
local v

v=$(echo "$msg" | grep -oP "${attr_re}\s*[0-9]+\.[0-9]+(?:\.[0-9]+)?(?:-\S+)?\s*->\s*\K[0-9]+\.[0-9]+" | head -1)
[[ -n "$v" ]] && { echo "$v"; return; }

v=$(echo "$msg" | grep -oiP "${attr_re}\s*init at\s+\K[0-9]+\.[0-9]+" | head -1)
[[ -n "$v" ]] && { echo "$v"; return; }

echo "$msg" | grep -oP "${attr_re}.*?\K[0-9]+\.[0-9]+" | head -1 || true
}

is_valid_version() {
Expand Down Expand Up @@ -224,7 +241,8 @@ add_version() {
local tmp
tmp=$(make_temp)
jq --arg v "$version" --arg r "$rev" --arg s "$sha256" --arg a "$attr" \
'.versions[$v] = { version: $v, rev: $r, sha256: $s, attr: $a }' \
'.versions[$v] = { version: $v, rev: $r, sha256: $s, attr: $a }
| .versions |= (to_entries | sort_by(.key | split(".") | map(tonumber)) | from_entries)' \
"$VERSIONS_FILE" >"$tmp" && mv "$tmp" "$VERSIONS_FILE"

log info "Added $version"
Expand All @@ -246,7 +264,7 @@ cmd_discover() {
log step "Searching GitHub..."

local -a commits
mapfile -t commits < <(fetch_commits "nodejs update repo:${NIXPKGS_REPO}")
mapfile -t commits < <(fetch_commits "nodejs repo:${NIXPKGS_REPO}")

((${#commits[@]} == 0)) && die "No commits found"

Expand Down Expand Up @@ -380,4 +398,6 @@ main() {
esac
}

main "$@"
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
8 changes: 8 additions & 0 deletions scripts/sort-versions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash

BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BASE_FILE="$BASE_DIR/$1"
TMP_FILE="$BASE_DIR/$1.tmp"

jq '.versions |= (to_entries | sort_by(.key | split(".") | map(tonumber)) | from_entries)' "$BASE_FILE" >"$TMP_FILE"
mv $TMP_FILE $BASE_FILE
99 changes: 99 additions & 0 deletions tests/test-fetch-node-versions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
# Unit tests for scripts/fetch-node-versions.sh's helper functions.
# Pure-logic tests only - no network access.
#
# Usage: ./tests/test-fetch-node-versions.sh

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

GREEN='\033[0;32m' RED='\033[0;31m' NC='\033[0m'
FAILURES=0

# shellcheck source=../scripts/fetch-node-versions.sh
source "$SCRIPT_DIR/scripts/fetch-node-versions.sh"

assert_eq() {
local desc="$1" expected="$2" actual="$3"
if [[ "$expected" == "$actual" ]]; then
echo -e "${GREEN} PASS${NC} $desc"
else
echo -e "${RED} FAIL${NC} $desc — expected '$expected', got '$actual'"
FAILURES=$((FAILURES + 1))
fi
}

assert_true() {
local desc="$1"
shift
if "$@" >/dev/null 2>&1; then
echo -e "${GREEN} PASS${NC} $desc"
else
echo -e "${RED} FAIL${NC} $desc"
FAILURES=$((FAILURES + 1))
fi
}

assert_false() {
local desc="$1"
shift
if ! "$@" >/dev/null 2>&1; then
echo -e "${GREEN} PASS${NC} $desc"
else
echo -e "${RED} FAIL${NC} $desc"
FAILURES=$((FAILURES + 1))
fi
}

echo ""
echo "fetch-node-versions.sh unit tests"
echo "──────────────────────────────────────────────"

echo "extract_version"
assert_eq "FROM -> TO bump uses TO" "25.9" "$(extract_version 'nodejs_25: 25.8.2 -> 25.9.0')"
assert_eq "legacy nodejs-NN_x attr" "16.20" "$(extract_version 'nodejs-16_x: 16.20.1 -> 16.20.2')"
assert_eq "bare nodejs attr" "20.9" "$(extract_version 'nodejs: 20.8.0 -> 20.9.0')"
assert_eq "init at" "25.2" "$(extract_version 'nodejs_25: init at 25.2.1 (#452389)')"
assert_eq "rc -> rc" "26.0" "$(extract_version 'nodejs_26: 26.0.0-rc.1 -> 26.0.0-rc.2')"
assert_eq "rc -> final" "26.0" "$(extract_version 'nodejs_26: 26.0.0-rc.2 -> 26.0.0')"
assert_eq "minor bump with PR suffix" "22.22" "$(extract_version 'nodejs_22: 22.22.2 -> 22.22.3 (#519938)')"
assert_eq "loose 'bump to' fallback" "24.15" "$(extract_version 'nodejs_24: bump to 24.15.0')"
assert_eq "ignores nodejs_latest bump" "" "$(extract_version 'nodejs_latest: 25.9.0 -> 26.0.0-rc.1')"
assert_eq "ignores nodejs_slim bump" "" "$(extract_version 'nodejs_slim: 25.0.0 -> 26.0.0')"
assert_eq "ignores brace-expansion msg" "" "$(extract_version 'nodejs_{20,22}: disable broken openssl tests')"
assert_eq "ignores non-version commit" "" "$(extract_version 'nodejs_24: skip tests failing on Darwin')"
assert_eq "ignores merge PR title" "" "$(extract_version 'Merge pull request #263285 from marsam/update-nodejs')"
assert_eq "ignores unrelated mention" "" "$(extract_version 'mainsail: update nodejs to version 22')"

echo ""
echo "is_valid_version"
assert_true "22.22 is valid" is_valid_version "22.22"
assert_true "16.20 is valid" is_valid_version "16.20"
assert_false "22 (no minor) invalid" is_valid_version "22"
assert_false "22.22.0 invalid" is_valid_version "22.22.0"
assert_false "empty string invalid" is_valid_version ""

echo ""
echo "is_supported_major"
assert_true "14 supported (MIN_MAJOR)" is_supported_major 14
assert_true "26 supported (MAX_MAJOR)" is_supported_major 26
assert_true "22 supported" is_supported_major 22
assert_false "13 unsupported (< MIN)" is_supported_major 13
assert_false "27 unsupported (> MAX)" is_supported_major 27

echo ""
echo "version_exists / get_existing_versions (against real versions.json)"
assert_true "14.21 exists" version_exists "14.21"
assert_true "26.2 exists" version_exists "26.2"
assert_false "99.99 missing" version_exists "99.99"
existing="$(get_existing_versions)"
assert_eq "get_existing_versions includes 14.21" "14.21" "$(grep -Fx '14.21' <<<"$existing")"

echo "──────────────────────────────────────────────"
if [[ $FAILURES -eq 0 ]]; then
echo -e "${GREEN}All tests passed${NC}"
else
echo -e "${RED}${FAILURES} test(s) failed${NC}"
exit 1
fi
72 changes: 72 additions & 0 deletions versions.json
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,24 @@
"sha256": "1f29niwab1n3xmwp5zss0b03wzyzr1nslzx2d3aiz7pvyxrimyzd",
"attr": "nodejs_24"
},
"24.13": {
"version": "24.13",
"rev": "9c0e2056b3c16190aafe67e4d29e530fc1f8c7c7",
"sha256": "0pkjlvjpvhsm078v0qb9i0y1vx3rgdpvk8wikvbxn0wldm0xkwps",
"attr": "nodejs_24"
},
"24.14": {
"version": "24.14",
"rev": "0968bb28e2dd918c3228895ef76ffda0fdf1b5f3",
"sha256": "19730d82hvl9s7l8brl3q5xl9qkhjnza1gi9666wgl87cwigq0x3",
"attr": "nodejs_24"
},
"24.15": {
"version": "24.15",
"rev": "0968bb28e2dd918c3228895ef76ffda0fdf1b5f3",
"sha256": "19730d82hvl9s7l8brl3q5xl9qkhjnza1gi9666wgl87cwigq0x3",
"attr": "nodejs_24"
},
"25.2": {
"version": "25.2",
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
Expand All @@ -197,6 +215,60 @@
"rev": "13b0f9e6ac78abbbb736c635d87845c4f4bee51b",
"sha256": "0js8a1abl5mi1j0l9wiasis8srm1a7wjrd3scwb8xpq3i0amvz8r",
"attr": "nodejs_25"
},
"25.5": {
"version": "25.5",
"rev": "0801ac780b7116dda2c7b396f7b49c01367853c4",
"sha256": "1w4yfkvkgf6a3l51s7nfq8ncjirc0brfbr62y05wzcmfxqq4vrrr",
"attr": "nodejs_25"
},
"25.6": {
"version": "25.6",
"rev": "699d7d142df2cfacc4837b6b8d9d051b1b508759",
"sha256": "0f94sywarrsk0gldfa92nkw3sdc5v3b1xanhwpq2qicv3navd9a9",
"attr": "nodejs_25"
},
"25.7": {
"version": "25.7",
"rev": "0d60a07834d66731ade3c0368db89e29eb722f7d",
"sha256": "0jv21jn1iaf2h2na063r1q91xd2nxncrgjxdnbadr0i0b6gyzci3",
"attr": "nodejs_25"
},
"25.8": {
"version": "25.8",
"rev": "94d8af17424dfc818024d98a59a0a1e0013495e6",
"sha256": "1yymvjk5ibxf7j9331i85bdnkmb74hvlha1156znfgsv5h5v6k64",
"attr": "nodejs_25"
},
"25.9": {
"version": "25.9",
"rev": "94d8af17424dfc818024d98a59a0a1e0013495e6",
"sha256": "1yymvjk5ibxf7j9331i85bdnkmb74hvlha1156znfgsv5h5v6k64",
"attr": "nodejs_25"
},
"26.0": {
"version": "26.0",
"rev": "2ebf37ab0d456633dc5c29df64e1f97739eb7e56",
"sha256": "0c86sbsasjjxqs86v829kbvq6jdgywrs00rnixn5r6ihh7qycdf5",
"attr": "nodejs_26"
},
"26.1": {
"version": "26.1",
"rev": "0f94bb0d8954e8368a8ef716135c89f3143de178",
"sha256": "18jjhvbhb7s2qlnpq4phd8jjxywxdymi5l8p3q5icc6dygw16p00",
"attr": "nodejs_26"
},
"26.2": {
"version": "26.2",
"rev": "302d9e77f10a189ad64227c5a0e474e4ff5fb9eb",
"sha256": "03y4gzn98ls2yrafq9hycx1j2zsbydc1cx6wqmrfyjz828njrg5f",
"attr": "nodejs_26"
},
"26.3": {
"version": "26.3",
"rev": "302d9e77f10a189ad64227c5a0e474e4ff5fb9eb",
"sha256": "03y4gzn98ls2yrafq9hycx1j2zsbydc1cx6wqmrfyjz828njrg5f",
"attr": "nodejs_26"
}
}
}
Loading