diff --git a/.github/workflows/update-versions.yml b/.github/workflows/update-versions.yml index c1d0c40..746fd9b 100644 --- a/.github/workflows/update-versions.yml +++ b/.github/workflows/update-versions.yml @@ -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: @@ -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: diff --git a/scripts/fetch-node-versions.sh b/scripts/fetch-node-versions.sh index 48a8b3e..1c6449f 100755 --- a/scripts/fetch-node-versions.sh +++ b/scripts/fetch-node-versions.sh @@ -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() { @@ -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" @@ -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" @@ -380,4 +398,6 @@ main() { esac } -main "$@" +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi diff --git a/scripts/sort-versions.sh b/scripts/sort-versions.sh new file mode 100755 index 0000000..85809a7 --- /dev/null +++ b/scripts/sort-versions.sh @@ -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 diff --git a/tests/test-fetch-node-versions.sh b/tests/test-fetch-node-versions.sh new file mode 100755 index 0000000..1dd670b --- /dev/null +++ b/tests/test-fetch-node-versions.sh @@ -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 diff --git a/versions.json b/versions.json index 30d0cec..202d5f9 100644 --- a/versions.json +++ b/versions.json @@ -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", @@ -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" } } }