diff --git a/.github/workflows/check-node-versions.yml b/.github/workflows/check-node-versions.yml index fc2a13095..8fb1bb918 100644 --- a/.github/workflows/check-node-versions.yml +++ b/.github/workflows/check-node-versions.yml @@ -110,22 +110,94 @@ jobs: } # Extract Node major from engines.node in package.json - # Sets: ENGINES_NODE_RAW (raw string), ENGINES_MIN_MAJOR + # Sets: ENGINES_NODE_RAW (raw string), ENGINES_MIN_MAJOR, ENGINES_IS_MINIMUM extract_engines_node() { local content="$1" ENGINES_NODE_RAW="" ENGINES_MIN_MAJOR="" + ENGINES_IS_MINIMUM="false" ENGINES_NODE_RAW=$(echo "$content" | jq -r '.engines.node // empty' 2>/dev/null || echo "") if [[ -z "$ENGINES_NODE_RAW" ]]; then return fi + # Detect if constraint is a minimum (>=, ^) vs exact pinning + if [[ "$ENGINES_NODE_RAW" =~ ^(\>=|\^|\~) ]]; then + ENGINES_IS_MINIMUM="true" + fi + # Extract the first number (major) from the constraint # Handles: ">=24.13.1", "^22", ">=18.0.0", ">=18.15.0 <19 || ^20", etc. ENGINES_MIN_MAJOR=$(echo "$ENGINES_NODE_RAW" | grep -oP '\d+' | head -1 || echo "") } + # Check if our_version satisfies an engines.node constraint + # Returns 0 if satisfied, 1 if not + # Usage: version_satisfies_engines "22" ">=18.0.0" "true" + version_satisfies_engines() { + local our="$1" + local min_major="$2" + local is_minimum="$3" + + if [[ -z "$min_major" || -z "$our" ]]; then + return 1 + fi + + if [[ "$is_minimum" == "true" ]]; then + # >= or ^ constraint: our version must be >= min_major + if [[ "$our" -ge "$min_major" ]]; then + return 0 + fi + fi + return 1 + } + + # Search for files in subdirectories via GitHub API tree + # Usage: find_repo_file "owner/repo" "branch" "filename" => sets REPLY to raw URL or empty + find_repo_file() { + local repo="$1" + local branch="$2" + local filename="$3" + REPLY="" + + # Try root first (fast) + local root_url="https://raw.githubusercontent.com/${repo}/${branch}/${filename}" + if curl -sfI "$root_url" >/dev/null 2>&1; then + REPLY="$root_url" + return + fi + + # Search via GitHub API tree (recursive) + local tree_url="https://api.github.com/repos/${repo}/git/trees/${branch}?recursive=1" + local tree_json + tree_json=$(curl -sf -H "Authorization: token $GH_TOKEN" "$tree_url" 2>/dev/null || echo "") + if [[ -z "$tree_json" ]]; then + return + fi + + # Find first matching path (prefer shorter/root-level paths) + local match_path + match_path=$(echo "$tree_json" | jq -r --arg fn "$filename" \ + '.tree[]? | select(.path | endswith("/" + $fn) or . == $fn) | .path' 2>/dev/null \ + | sort | head -1 || echo "") + + if [[ -n "$match_path" ]]; then + REPLY="https://raw.githubusercontent.com/${repo}/${branch}/${match_path}" + fi + } + + # Extract Node major from .nvmrc or .node-version + # Sets: NVMRC_NODE_MAJOR + extract_nvmrc_node() { + local content="$1" + NVMRC_NODE_MAJOR="" + # .nvmrc/.node-version typically has: "v22.9.0", "22", "lts/iron", etc. + local ver + ver=$(echo "$content" | tr -d '[:space:]' | grep -oP '^v?\K[0-9]+' | head -1 || echo "") + NVMRC_NODE_MAJOR="$ver" + } + # Collect results declare -a issue_scripts=() declare -a report_lines=() @@ -143,7 +215,10 @@ jobs: slug=$(basename "$script" | sed 's/-install\.sh$//') # Extract Source URL (GitHub only) - source_url=$(head -20 "$script" | grep -oP '(?<=# Source: )https://github\.com/[^\s]+' | head -1 || echo "") + # Supports both: + # # Source: https://github.com/owner/repo + # # Source: https://example.com | Github: https://github.com/owner/repo + source_url=$(head -20 "$script" | grep -oP 'https://github\.com/[^\s|]+' | head -1 || echo "") if [[ -z "$source_url" ]]; then report_lines+=("| \`$slug\` | — | — | — | — | ⏭️ No GitHub source |") continue @@ -167,12 +242,23 @@ jobs: fi fi - # Fetch upstream Dockerfile + # Determine default branch via GitHub API (fast, single call) + detected_branch="" + api_default=$(curl -sf -H "Authorization: token $GH_TOKEN" \ + "https://api.github.com/repos/${repo}" 2>/dev/null \ + | jq -r '.default_branch // empty' 2>/dev/null || echo "") + if [[ -n "$api_default" ]]; then + detected_branch="$api_default" + else + detected_branch="main" + fi + + # Fetch upstream Dockerfile (root + subdirectories) df_content="" - for branch in main master dev; do - df_content=$(curl -sf "https://raw.githubusercontent.com/${repo}/${branch}/Dockerfile" 2>/dev/null || echo "") - [[ -n "$df_content" ]] && break - done + find_repo_file "$repo" "$detected_branch" "Dockerfile" + if [[ -n "$REPLY" ]]; then + df_content=$(curl -sf "$REPLY" 2>/dev/null || echo "") + fi DF_NODE_MAJOR="" DF_SOURCE="" @@ -180,19 +266,35 @@ jobs: extract_dockerfile_node "$df_content" fi - # Fetch upstream package.json + # Fetch upstream package.json (root + subdirectories) pkg_content="" - for branch in main master dev; do - pkg_content=$(curl -sf "https://raw.githubusercontent.com/${repo}/${branch}/package.json" 2>/dev/null || echo "") - [[ -n "$pkg_content" ]] && break - done + find_repo_file "$repo" "$detected_branch" "package.json" + if [[ -n "$REPLY" ]]; then + pkg_content=$(curl -sf "$REPLY" 2>/dev/null || echo "") + fi ENGINES_NODE_RAW="" ENGINES_MIN_MAJOR="" + ENGINES_IS_MINIMUM="false" if [[ -n "$pkg_content" ]]; then extract_engines_node "$pkg_content" fi + # Fallback: check .nvmrc or .node-version + NVMRC_NODE_MAJOR="" + if [[ -z "$DF_NODE_MAJOR" && -z "$ENGINES_MIN_MAJOR" ]]; then + for nvmfile in .nvmrc .node-version; do + find_repo_file "$repo" "$detected_branch" "$nvmfile" + if [[ -n "$REPLY" ]]; then + nvmrc_content=$(curl -sf "$REPLY" 2>/dev/null || echo "") + if [[ -n "$nvmrc_content" ]]; then + extract_nvmrc_node "$nvmrc_content" + [[ -n "$NVMRC_NODE_MAJOR" ]] && break + fi + fi + done + fi + # Determine upstream recommended major version upstream_major="" upstream_hint="" @@ -203,6 +305,9 @@ jobs: elif [[ -n "$ENGINES_MIN_MAJOR" ]]; then upstream_major="$ENGINES_MIN_MAJOR" upstream_hint="engines: $ENGINES_NODE_RAW" + elif [[ -n "$NVMRC_NODE_MAJOR" ]]; then + upstream_major="$NVMRC_NODE_MAJOR" + upstream_hint=".nvmrc/.node-version" fi # Build display values @@ -214,13 +319,23 @@ jobs: if [[ "$our_version" == "dynamic" ]]; then status="🔄 Dynamic" elif [[ "$our_version" == "unset" ]]; then - status="⚠️ NODE_VERSION not set" + if [[ -n "$upstream_major" ]]; then + status="⚠️ NODE_VERSION not set (upstream=$upstream_major via $upstream_hint)" + else + status="⚠️ NODE_VERSION not set (no upstream info found)" + fi issue_scripts+=("$slug|$our_version|$upstream_major|$upstream_hint|$repo") drift_count=$((drift_count + 1)) elif [[ -n "$upstream_major" && "$our_version" != "$upstream_major" ]]; then - status="🔸 Drift → upstream=$upstream_major ($upstream_hint)" - issue_scripts+=("$slug|$our_version|$upstream_major|$upstream_hint|$repo") - drift_count=$((drift_count + 1)) + # Check if engines.node is a minimum constraint that our version satisfies + if [[ -z "$DF_NODE_MAJOR" && "$ENGINES_IS_MINIMUM" == "true" ]] && \ + version_satisfies_engines "$our_version" "$ENGINES_MIN_MAJOR" "$ENGINES_IS_MINIMUM"; then + status="✅ (engines: $ENGINES_NODE_RAW — ours: $our_version satisfies)" + else + status="🔸 Drift → upstream=$upstream_major ($upstream_hint)" + issue_scripts+=("$slug|$our_version|$upstream_major|$upstream_hint|$repo") + drift_count=$((drift_count + 1)) + fi fi report_lines+=("| \`$slug\` | $our_version | $engines_display | $dockerfile_display | [$repo](https://github.com/$repo) | $status |")