diff --git a/.github/workflows/check-node-versions.yml b/.github/workflows/check-node-versions.yml index 953721faa..72179611e 100644 --- a/.github/workflows/check-node-versions.yml +++ b/.github/workflows/check-node-versions.yml @@ -7,8 +7,9 @@ on: - cron: "0 6 * * 1" permissions: - contents: read + contents: write issues: write + pull-requests: write jobs: check-node-versions: @@ -390,6 +391,132 @@ jobs: 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 </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 -install.sh\` - 4. Update \`NODE_VERSION\` in \`ct/.sh\` (update section) if applicable - 5. Check off the item above once done + 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
Full report diff --git a/.github/workflows/node-drift-pr-closed.yml b/.github/workflows/node-drift-pr-closed.yml new file mode 100644 index 000000000..8a61dfe25 --- /dev/null +++ b/.github/workflows/node-drift-pr-closed.yml @@ -0,0 +1,55 @@ +name: Record closed Node.js drift PR + +# The drift check opens one PR per script and treats a closed one as a verdict: +# that bump is not proposed again. That decision is invisible on the report +# unless it is written there, so record it as a comment naming the script. + +on: + pull_request: + types: [closed] + +jobs: + record: + if: > + github.repository == 'community-scripts/ProxmoxVE' && + github.event.pull_request.merged == false && + startsWith(github.event.pull_request.head.ref, 'node-drift/') + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - uses: actions/github-script@v9 + with: + script: | + const pr = context.payload.pull_request; + const owner = context.repo.owner; + const repo = context.repo.repo; + + // node-drift/--to- + const m = pr.head.ref.match(/^node-drift\/(.+)-(\d+)-to-(\d+)$/); + if (!m) return; + const [, slug, from, to] = m; + + // A close by the stale bot is a lapsed review, not a decision on + // the bump, and the drift check reopens those. Saying "declined" + // here would contradict it. + const stale = pr.labels.some((l) => l.name === 'stale'); + + const issues = await github.rest.issues.listForRepo({ + owner, repo, state: 'open', labels: 'automated,dependencies', per_page: 100, + }); + const issue = issues.data.find( + (i) => !i.pull_request && i.title === '[Automated] Node.js Version Drift Report', + ); + if (!issue) return; + + const body = stale + ? `\`${slug}\` — PR #${pr.number} (Node ${from} → ${to}) was closed as stale. ` + + `No decision was recorded, so the next drift check opens it again.` + : `\`${slug}\` — PR #${pr.number} (Node ${from} → ${to}) was closed without merging. ` + + `The script keeps Node ${from} and this bump will not be proposed again. ` + + `If it was closed by mistake, reopen the PR.`; + + await github.rest.issues.createComment({ + owner, repo, issue_number: issue.number, body, + });