mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2026-09-08 06:46:28 +00:00
This workflow now opens one PR per drifting script instead of batching all Node.js version bumps together. Each PR includes the exact version change, a test command, and a clear decision path for manual review or closure. The summary issue also tracks each script's PR state so drift reporting stays in sync across runs.
615 lines
24 KiB
YAML
Generated
615 lines
24 KiB
YAML
Generated
name: Check Node.js Version Drift
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
schedule:
|
|
# Runs weekly on Monday at 06:00 UTC
|
|
- cron: "0 6 * * 1"
|
|
|
|
permissions:
|
|
contents: write
|
|
issues: write
|
|
pull-requests: write
|
|
|
|
jobs:
|
|
check-node-versions:
|
|
if: github.repository == 'community-scripts/ProxmoxVE'
|
|
runs-on: coolify-runner
|
|
|
|
steps:
|
|
- name: Checkout Repository
|
|
uses: actions/checkout@v7
|
|
with:
|
|
ref: main
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
sudo apt-get update -qq
|
|
sudo apt-get install -y -qq jq curl > /dev/null 2>&1
|
|
|
|
- name: Check upstream Node.js versions
|
|
id: check
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
echo "================================================"
|
|
echo " Checking Node.js version drift in install scripts"
|
|
echo "================================================"
|
|
|
|
# Alpine version -> Node major cache (populated on demand)
|
|
declare -A ALPINE_NODE_CACHE
|
|
|
|
# Resolve Node.js major version from Alpine package registry
|
|
# Usage: resolve_alpine_node "3.21" => sets REPLY to major version (e.g. "22")
|
|
resolve_alpine_node() {
|
|
local alpine_ver="$1"
|
|
if [[ -n "${ALPINE_NODE_CACHE[$alpine_ver]+x}" ]]; then
|
|
REPLY="${ALPINE_NODE_CACHE[$alpine_ver]}"
|
|
return
|
|
fi
|
|
|
|
local full_ver=""
|
|
local arch_label url_arch url page
|
|
for arch_pair in "amd64:x86_64" "arm64:aarch64"; do
|
|
arch_label="${arch_pair%%:*}"
|
|
url_arch="${arch_pair#*:}"
|
|
url="https://pkgs.alpinelinux.org/package/v${alpine_ver}/main/${url_arch}/nodejs"
|
|
page=$(curl -sf "$url" 2>/dev/null || echo "")
|
|
if [[ -n "$page" ]]; then
|
|
# Parse: "Version | 24.13.0-r1" or similar table row.
|
|
full_ver=$(echo "$page" | grep -oP 'Version\s*\|\s*\K[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "")
|
|
if [[ -z "$full_ver" ]]; then
|
|
# Fallback: look for version pattern after "Version".
|
|
full_ver=$(echo "$page" | grep -oP '(?<=Version</td><td>)[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "")
|
|
fi
|
|
fi
|
|
if [[ -n "$full_ver" ]]; then
|
|
echo "Resolved Alpine ${alpine_ver} nodejs via ${arch_label}"
|
|
break
|
|
fi
|
|
done
|
|
|
|
local major=""
|
|
if [[ -n "$full_ver" ]]; then
|
|
major="${full_ver%%.*}"
|
|
fi
|
|
|
|
ALPINE_NODE_CACHE[$alpine_ver]="$major"
|
|
REPLY="$major"
|
|
}
|
|
|
|
# Extract Node major from a Dockerfile content
|
|
# Sets: DF_NODE_MAJOR, DF_SOURCE (description of where we found it)
|
|
extract_dockerfile_node() {
|
|
local content="$1"
|
|
DF_NODE_MAJOR=""
|
|
DF_SOURCE=""
|
|
|
|
# 1) FROM node:XX (e.g. node:24-alpine, node:22.9.0-bookworm-slim, node:20)
|
|
local node_from
|
|
node_from=$(echo "$content" | grep -oP '(?i)FROM\s+(--platform=[^\s]+\s+)?node:\K[0-9]+' | head -1 || echo "")
|
|
if [[ -n "$node_from" ]]; then
|
|
DF_NODE_MAJOR="$node_from"
|
|
DF_SOURCE="FROM node:${node_from}"
|
|
return
|
|
fi
|
|
|
|
# 2) nodesource/setup_XX.x
|
|
local nodesource
|
|
nodesource=$(echo "$content" | grep -oP 'nodesource/setup_\K[0-9]+' | head -1 || echo "")
|
|
if [[ -n "$nodesource" ]]; then
|
|
DF_NODE_MAJOR="$nodesource"
|
|
DF_SOURCE="nodesource/setup_${nodesource}.x"
|
|
return
|
|
fi
|
|
|
|
# 3) FROM alpine:X.Y — resolve via Alpine packages
|
|
local alpine_ver
|
|
alpine_ver=$(echo "$content" | grep -oP '(?i)FROM\s+(--platform=[^\s]+\s+)?alpine:\K[0-9]+\.[0-9]+' | head -1 || echo "")
|
|
if [[ -n "$alpine_ver" ]]; then
|
|
resolve_alpine_node "$alpine_ver"
|
|
if [[ -n "$REPLY" ]]; then
|
|
DF_NODE_MAJOR="$REPLY"
|
|
DF_SOURCE="alpine:${alpine_ver} (pkg: nodejs ${DF_NODE_MAJOR})"
|
|
return
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# Extract Node major from engines.node in package.json
|
|
# 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=()
|
|
total=0
|
|
checked=0
|
|
drift_count=0
|
|
|
|
for script in install/*-install.sh; do
|
|
[[ ! -f "$script" ]] && continue
|
|
if ! grep -q 'setup_nodejs' "$script"; then
|
|
continue
|
|
fi
|
|
|
|
total=$((total + 1))
|
|
slug=$(basename "$script" | sed 's/-install\.sh$//')
|
|
|
|
# Extract Source URL (GitHub only) from the "# Source:" line
|
|
# Supports both:
|
|
# # Source: https://github.com/owner/repo
|
|
# # Source: https://example.com | Github: https://github.com/owner/repo
|
|
# NOTE: Must filter for "# Source:" line first to avoid matching the License URL
|
|
source_url=$(head -20 "$script" | grep -i '# Source:' | grep -oP 'https://github\.com/[^\s|]+' | head -1 || echo "")
|
|
if [[ -z "$source_url" ]]; then
|
|
report_lines+=("| \`$slug\` | — | — | — | — | ⏭️ No GitHub source |")
|
|
continue
|
|
fi
|
|
|
|
repo=$(echo "$source_url" | sed -E 's|https://github\.com/||; s|/$||; s|\.git$||')
|
|
if [[ -z "$repo" || "$repo" != */* ]]; then
|
|
report_lines+=("| \`$slug\` | — | — | — | — | ⏭️ Invalid repo |")
|
|
continue
|
|
fi
|
|
|
|
checked=$((checked + 1))
|
|
|
|
# Extract our NODE_VERSION
|
|
our_version=$(grep -oP 'NODE_VERSION=\K"?\d+' "$script" | head -1 | grep -oP '\d+' || echo "")
|
|
if [[ -z "$our_version" ]]; then
|
|
if grep -q 'NODE_VERSION=\$(' "$script"; then
|
|
our_version="dynamic"
|
|
else
|
|
our_version="unset"
|
|
fi
|
|
fi
|
|
|
|
# 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=""
|
|
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=""
|
|
if [[ -n "$df_content" ]]; then
|
|
extract_dockerfile_node "$df_content"
|
|
fi
|
|
|
|
# Fetch upstream package.json (root + subdirectories)
|
|
pkg_content=""
|
|
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=""
|
|
|
|
if [[ -n "$DF_NODE_MAJOR" ]]; then
|
|
upstream_major="$DF_NODE_MAJOR"
|
|
upstream_hint="$DF_SOURCE"
|
|
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
|
|
engines_display="${ENGINES_NODE_RAW:-—}"
|
|
dockerfile_display="${DF_SOURCE:-—}"
|
|
|
|
# Compare
|
|
status="✅"
|
|
if [[ "$our_version" == "dynamic" ]]; then
|
|
status="🔄 Dynamic"
|
|
elif [[ "$our_version" == "unset" ]]; then
|
|
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
|
|
if (( our_version < upstream_major )); then
|
|
# 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
|
|
else
|
|
status="✅ Ahead of upstream ($upstream_major via $upstream_hint)"
|
|
fi
|
|
fi
|
|
|
|
report_lines+=("| \`$slug\` | $our_version | $engines_display | $dockerfile_display | [$repo](https://github.com/$repo) | $status |")
|
|
|
|
# Rate-limit to avoid GitHub secondary rate limits
|
|
sleep 0.3
|
|
|
|
done
|
|
|
|
# Print summary
|
|
echo ""
|
|
echo "========================================="
|
|
echo " Total scripts with setup_nodejs: $total"
|
|
echo " Checked (with GitHub source): $checked"
|
|
echo " Version drift detected: $drift_count"
|
|
echo "========================================="
|
|
|
|
# Export
|
|
{
|
|
echo "drift_count=$drift_count"
|
|
echo "total=$total"
|
|
echo "checked=$checked"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
# Save issue details for next step
|
|
printf '%s\n' "${issue_scripts[@]}" > /tmp/drift_scripts.txt 2>/dev/null || touch /tmp/drift_scripts.txt
|
|
|
|
# Save full report
|
|
{
|
|
echo "## Node.js Version Drift Report"
|
|
echo ""
|
|
echo "**Generated:** $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
echo "**Scripts checked:** $total | **With GitHub source:** $checked | **Drift detected:** $drift_count"
|
|
echo ""
|
|
echo "| Script | Our Version | engines.node | Dockerfile | Upstream Repo | Status |"
|
|
echo "|--------|-------------|-------------|------------|---------------|--------|"
|
|
printf '%s\n' "${report_lines[@]}" | sort
|
|
} > /tmp/drift_report.md
|
|
|
|
cat /tmp/drift_report.md
|
|
|
|
# One PR per script, not one for all of them: the bumps are judged
|
|
# individually (an app can be deliberately held back while the next is a
|
|
# safe follow), and a combined PR is blocked by its worst member.
|
|
- name: Open a pull request per drifting script
|
|
if: steps.check.outputs.drift_count != '0'
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
|
|
: >/tmp/drift_prs.txt
|
|
|
|
while IFS='|' read -r slug our upstream hint repo; do
|
|
[[ -z "$slug" ]] && continue
|
|
|
|
# "dynamic" and "unset" have no literal to rewrite, and a missing
|
|
# upstream major is nothing to rewrite it to.
|
|
if [[ ! "$our" =~ ^[0-9]+$ || ! "$upstream" =~ ^[0-9]+$ ]]; then
|
|
printf '%s|manual||\n' "$slug" >>/tmp/drift_prs.txt
|
|
continue
|
|
fi
|
|
|
|
# The branch name carries the exact bump, so the PR for it answers
|
|
# the question on its own: open means it is waiting for a review,
|
|
# closed means someone said no to this bump. A later upstream
|
|
# release changes the name and gets its own PR.
|
|
branch="node-drift/${slug}-${our}-to-${upstream}"
|
|
|
|
existing=$(gh pr list --head "$branch" --state all \
|
|
--json number,state,url,labels --jq '.[0] // empty' 2>/dev/null || echo "")
|
|
|
|
if [[ -n "$existing" ]]; then
|
|
state=$(jq -r '.state' <<<"$existing")
|
|
url=$(jq -r '.url' <<<"$existing")
|
|
stale=$(jq -r '[.labels[]?.name] | index("stale") // empty' <<<"$existing")
|
|
|
|
case "$state" in
|
|
OPEN)
|
|
printf '%s|open|%s\n' "$slug" "$url" >>/tmp/drift_prs.txt
|
|
continue
|
|
;;
|
|
MERGED)
|
|
printf '%s|merged|%s\n' "$slug" "$url" >>/tmp/drift_prs.txt
|
|
continue
|
|
;;
|
|
*)
|
|
# The stale bot only closes what a human labelled "stale", so
|
|
# that close carries no verdict on the bump — reopen the case.
|
|
# A close without it is a decision, and it stands.
|
|
if [[ -z "$stale" ]]; then
|
|
printf '%s|declined|%s\n' "$slug" "$url" >>/tmp/drift_prs.txt
|
|
continue
|
|
fi
|
|
git push origin --delete "$branch" >/dev/null 2>&1 || true
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
git checkout -q -B "$branch"
|
|
|
|
changed=()
|
|
for f in "install/${slug}-install.sh" "ct/${slug}.sh"; do
|
|
[[ -f "$f" ]] || continue
|
|
if grep -qE "NODE_VERSION=\"?${our}\"?" "$f"; then
|
|
sed -i -E "s/(NODE_VERSION=)\"?${our}\"?/\1\"${upstream}\"/g" "$f"
|
|
changed+=("$f")
|
|
fi
|
|
done
|
|
|
|
if [[ ${#changed[@]} -eq 0 ]]; then
|
|
printf '%s|manual||\n' "$slug" >>/tmp/drift_prs.txt
|
|
git checkout -q main
|
|
continue
|
|
fi
|
|
|
|
git add "${changed[@]}"
|
|
git commit -q -m "${slug}: bump Node.js from ${our} to ${upstream}"
|
|
|
|
if ! git push -q -u origin "$branch" 2>/dev/null; then
|
|
printf '%s|failed||\n' "$slug" >>/tmp/drift_prs.txt
|
|
git checkout -q main
|
|
continue
|
|
fi
|
|
|
|
# The test command is written here rather than left to
|
|
# pr-test-command.yml: a PR opened with GITHUB_TOKEN does not start
|
|
# another workflow, so that comment would never arrive.
|
|
body=$(cat <<PR_EOF
|
|
\`${slug}\` pins Node **${our}**, upstream indicates **${upstream}** (${hint}) — [upstream repo](https://github.com/${repo}).
|
|
|
|
Opened automatically by the weekly Node.js version drift check. **Nothing here has been tested.** Run it against a host before merging:
|
|
|
|
\`\`\`bash
|
|
export COMMUNITY_SCRIPTS_URL=https://raw.githubusercontent.com/${GITHUB_REPOSITORY}/${branch}
|
|
bash -c "\$(curl -fsSL "\$COMMUNITY_SCRIPTS_URL/ct/${slug}.sh")"
|
|
\`\`\`
|
|
|
|
Both lines are needed — each script pins \`_CS_DEFAULT_URL\` to \`main\`, and that pin is what fills \`COMMUNITY_SCRIPTS_URL\` when it is unset.
|
|
|
|
If the bump is wrong — upstream ships a newer Node than the app needs, or this script is deliberately held back — **close this PR**. It gets recorded on the drift report and this exact bump is not proposed again.
|
|
PR_EOF
|
|
)
|
|
|
|
if url=$(gh pr create \
|
|
--title "${slug}: bump Node.js from ${our} to ${upstream}" \
|
|
--body "$body" \
|
|
--base main \
|
|
--head "$branch" 2>/dev/null); then
|
|
gh pr edit "$url" --add-label "automated pr" >/dev/null 2>&1 || true
|
|
printf '%s|open|%s\n' "$slug" "$url" >>/tmp/drift_prs.txt
|
|
else
|
|
printf '%s|failed||\n' "$slug" >>/tmp/drift_prs.txt
|
|
fi
|
|
|
|
git checkout -q main
|
|
done </tmp/drift_scripts.txt
|
|
|
|
echo "Pull request status per script:"
|
|
cat /tmp/drift_prs.txt
|
|
|
|
- name: Create or update summary issue
|
|
if: steps.check.outputs.drift_count != '0'
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
TITLE="[Automated] Node.js Version Drift Report"
|
|
DATE=$(date -u +%Y-%m-%d)
|
|
DRIFT_COUNT="${{ steps.check.outputs.drift_count }}"
|
|
TOTAL="${{ steps.check.outputs.total }}"
|
|
CHECKED="${{ steps.check.outputs.checked }}"
|
|
|
|
# Build checklist from drift data. Each item carries the state of its
|
|
# own PR, so the list tracks itself instead of relying on someone
|
|
# ticking a box that the next run would overwrite anyway.
|
|
CHECKLIST=""
|
|
while IFS='|' read -r slug our_version upstream_major upstream_hint repo; do
|
|
[[ -z "$slug" ]] && continue
|
|
|
|
pr_state=""
|
|
pr_url=""
|
|
if [[ -f /tmp/drift_prs.txt ]]; then
|
|
pr_line=$(grep -m1 "^${slug}|" /tmp/drift_prs.txt || echo "")
|
|
pr_state=$(cut -d'|' -f2 <<<"$pr_line")
|
|
pr_url=$(cut -d'|' -f3 <<<"$pr_line")
|
|
fi
|
|
|
|
case "$pr_state" in
|
|
open) mark="- [ ]"; note="— ${pr_url}" ;;
|
|
declined) mark="- [x]"; note="— declined in ${pr_url}, not proposed again" ;;
|
|
merged) mark="- [x]"; note="— merged in ${pr_url}" ;;
|
|
manual) mark="- [ ]"; note="— needs a manual change (no fixed \`NODE_VERSION\` to rewrite)" ;;
|
|
failed) mark="- [ ]"; note="— could not open a PR automatically" ;;
|
|
*) mark="- [ ]"; note="" ;;
|
|
esac
|
|
|
|
CHECKLIST+="${mark} **\`${slug}\`** — ours: \`${our_version}\` → upstream: \`${upstream_major}\` (${upstream_hint}) — [repo](https://github.com/${repo}) ${note}"$'\n'
|
|
done < /tmp/drift_scripts.txt
|
|
|
|
# Build full report table
|
|
REPORT=$(cat /tmp/drift_report.md)
|
|
|
|
BODY=$(cat <<ISSUE_EOF
|
|
## Node.js Version Drift Report — ${DATE}
|
|
|
|
**${DRIFT_COUNT}** script(s) with version drift detected (out of ${CHECKED} checked / ${TOTAL} total).
|
|
|
|
### Scripts requiring investigation
|
|
|
|
${CHECKLIST}
|
|
|
|
### How to resolve
|
|
|
|
Each item above has its own pull request with the bump already applied and a ready-to-run test command in the description.
|
|
|
|
1. Check the upstream Dockerfile / package.json to confirm the required Node.js version
|
|
2. Run the test command from the PR against a host
|
|
3. Merge the PR if it works — the item disappears from this list on the next run
|
|
4. Close the PR if the bump is wrong: the script stays as it is and this exact bump is never proposed again
|
|
|
|
<details>
|
|
<summary>Full report</summary>
|
|
|
|
${REPORT}
|
|
|
|
</details>
|
|
|
|
---
|
|
*This issue is automatically created/updated weekly by the Node.js version drift check workflow.*
|
|
*Last updated: ${DATE}*
|
|
ISSUE_EOF
|
|
)
|
|
|
|
# Check if a matching open issue already exists
|
|
EXISTING=$(gh issue list --state open --label "automated,dependencies" --search "\"[Automated] Node.js Version Drift Report\"" --json number --jq '.[0].number // empty' 2>/dev/null || echo "")
|
|
|
|
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 summary issue"
|
|
fi
|
|
|
|
- name: Close issue if no drift
|
|
if: steps.check.outputs.drift_count == '0'
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
EXISTING=$(gh issue list --state open --label "automated,dependencies" --search "\"[Automated] Node.js Version Drift Report\"" --json number --jq '.[0].number // empty' 2>/dev/null || echo "")
|
|
if [[ -n "$EXISTING" ]]; then
|
|
gh issue close "$EXISTING" --comment "All Node.js versions are in sync with upstream. Closing automatically."
|
|
echo "Closed issue #$EXISTING"
|
|
fi
|