diff --git a/.github/workflows/check-node-versions.yml b/.github/workflows/check-node-versions.yml new file mode 100644 index 000000000..31b90ddd0 --- /dev/null +++ b/.github/workflows/check-node-versions.yml @@ -0,0 +1,200 @@ +name: Check Node.js Version Drift + +on: + workflow_dispatch: + schedule: + # Runs weekly on Monday at 06:00 UTC + - cron: "0 6 * * 1" + +permissions: + contents: read + issues: write + +env: + REPORT_FILE: node-version-report.md + +jobs: + check-node-versions: + if: github.repository == 'community-scripts/ProxmoxVE' + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + ref: main + + - name: Check upstream Node.js versions + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + echo "================================================" + echo " Checking Node.js version drift in install scripts" + echo "================================================" + + # Collect results + declare -a mismatches=() + declare -a results=() + total=0 + skipped=0 + + for script in install/*-install.sh; do + [[ ! -f "$script" ]] && continue + + # Check if script uses setup_nodejs + if ! grep -q 'setup_nodejs' "$script"; then + continue + fi + + total=$((total + 1)) + slug=$(basename "$script" | sed 's/-install\.sh$//') + + # Extract Source URL from header (lines 1-10) + source_url=$(head -20 "$script" | grep -oP '(?<=# Source: )https://github\.com/[^ ]+' | head -1) + if [[ -z "$source_url" ]]; then + skipped=$((skipped + 1)) + results+=("| $slug | - | - | - | - | ⏭️ No GitHub source |") + continue + fi + + # Extract owner/repo from URL + repo=$(echo "$source_url" | sed -E 's|https://github\.com/||; s|/$||; s|\.git$||') + if [[ -z "$repo" || "$repo" != */* ]]; then + skipped=$((skipped + 1)) + results+=("| $slug | - | - | - | - | ⏭️ Invalid repo: $repo |") + continue + fi + + # Extract current NODE_VERSION from script + # Handles: NODE_VERSION="22", NODE_VERSION="24", or dynamic (curl) + current_version=$(grep -oP 'NODE_VERSION="(\d+)"' "$script" | head -1 | grep -oP '\d+' || echo "") + if [[ -z "$current_version" ]]; then + # Check for dynamic version (e.g. curl-based) + if grep -q 'NODE_VERSION=\$(' "$script"; then + current_version="dynamic" + else + current_version="unset" + fi + fi + + # Fetch upstream package.json engines.node + engines_node="" + pkg_raw=$(curl -sf "https://raw.githubusercontent.com/${repo}/HEAD/package.json" 2>/dev/null || echo "") + if [[ -n "$pkg_raw" ]]; then + engines_node=$(echo "$pkg_raw" | jq -r '.engines.node // empty' 2>/dev/null || echo "") + fi + + # Fetch upstream Dockerfile FROM node:XX + dockerfile_node="" + for branch in main master; do + df_raw=$(curl -sf "https://raw.githubusercontent.com/${repo}/${branch}/Dockerfile" 2>/dev/null || echo "") + if [[ -n "$df_raw" ]]; then + # Extract node version from FROM node:XX or nodesource/setup_XX.x + dockerfile_node=$(echo "$df_raw" | grep -oP '(?<=FROM node:)\d+' | head -1 || echo "") + if [[ -z "$dockerfile_node" ]]; then + dockerfile_node=$(echo "$df_raw" | grep -oP '(?<=nodesource/setup_)\d+' | head -1 || echo "") + fi + break + fi + done + + # Determine upstream recommended version + upstream_version="" + if [[ -n "$dockerfile_node" ]]; then + upstream_version="$dockerfile_node" + fi + + # Parse engines.node for minimum major version + engines_min="" + if [[ -n "$engines_node" ]]; then + engines_min=$(echo "$engines_node" | grep -oP '\d+' | head -1 || echo "") + fi + + # Determine status + status="✅" + if [[ "$current_version" == "dynamic" ]]; then + status="🔄 Dynamic" + elif [[ "$current_version" == "unset" ]]; then + status="⚠️ No NODE_VERSION set" + mismatches+=("$slug") + elif [[ -n "$upstream_version" && "$current_version" != "$upstream_version" ]]; then + status="🔸 Drift (upstream: $upstream_version)" + mismatches+=("$slug") + elif [[ -n "$engines_min" && "$engines_node" == ">="* ]]; then + # Check if engines requires a higher version than what we use + if [[ "$engines_min" -gt "$current_version" ]]; then + status="🔴 Below minimum (engines: $engines_node)" + mismatches+=("$slug") + fi + fi + + engines_display="${engines_node:-—}" + dockerfile_display="${dockerfile_node:-—}" + results+=("| $slug | $current_version | $engines_display | $dockerfile_display | $repo | $status |") + done + + # Build report + { + echo "## Node.js Version Drift Report" + echo "" + echo "Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "Scripts checked: $total | Skipped: $skipped | Mismatches: ${#mismatches[@]}" + echo "" + echo "| Script | Our Version | engines.node | Dockerfile | Upstream Repo | Status |" + echo "|--------|-------------|-------------|------------|---------------|--------|" + printf '%s\n' "${results[@]}" | sort + echo "" + if [[ ${#mismatches[@]} -gt 0 ]]; then + echo "### Scripts requiring attention" + echo "" + for m in "${mismatches[@]}"; do + echo "- \`$m\`" + done + else + echo "All Node.js versions are in sync with upstream. ✅" + fi + } > "$REPORT_FILE" + + cat "$REPORT_FILE" + + # Export for next steps + echo "mismatch_count=${#mismatches[@]}" >> "$GITHUB_OUTPUT" + echo "total=$total" >> "$GITHUB_OUTPUT" + id: check + + - name: Create or update issue on drift + if: steps.check.outputs.mismatch_count != '0' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TITLE="[Automated] Node.js version drift detected" + BODY=$(cat "$REPORT_FILE") + + # Check if issue already exists + EXISTING=$(gh issue list --label "automated,dependencies" --search "$TITLE" --state open --json number --jq '.[0].number // empty') + + if [[ -n "$EXISTING" ]]; then + gh issue edit "$EXISTING" --body "$BODY" + echo "Updated existing issue #$EXISTING" + else + gh issue create \ + --title "$TITLE" \ + --body "$BODY" \ + --label "automated,dependencies" + echo "Created new issue" + fi + + - name: Close issue if no drift + if: steps.check.outputs.mismatch_count == '0' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TITLE="[Automated] Node.js version drift detected" + EXISTING=$(gh issue list --label "automated,dependencies" --search "$TITLE" --state open --json number --jq '.[0].number // empty') + + if [[ -n "$EXISTING" ]]; then + gh issue close "$EXISTING" --comment "All Node.js versions are now in sync with upstream. Closing automatically." + echo "Closed issue #$EXISTING" + fi