mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2026-09-08 15:06:22 +00:00
628 lines
25 KiB
YAML
Generated
628 lines
25 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
|
|
)
|
|
|
|
# Labels go on at creation, not in a second call: they are what
|
|
# exempts the PR from the template check, and that check runs on
|
|
# "opened" — a label added a moment later can arrive too late.
|
|
#
|
|
# "keep-open" rather than "automated pr" for that exemption: both
|
|
# skip the template check, but changelog-pr.yml drops every PR
|
|
# carrying "automated pr" (it marks its own PR with it), so that
|
|
# label would keep these bumps out of the changelog entirely.
|
|
# "update script" and "bugfix" place them under Updated Scripts →
|
|
# Bug Fixes; the autolabeler cannot do it, since a PR opened with
|
|
# GITHUB_TOKEN never triggers its pull_request_target run.
|
|
if url=$(gh pr create \
|
|
--title "${slug}: bump Node.js from ${our} to ${upstream}" \
|
|
--body "$body" \
|
|
--base main \
|
|
--head "$branch" \
|
|
--label "keep-open" \
|
|
--label "update script" \
|
|
--label "bugfix" 2>/dev/null); then
|
|
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
|