Skip to content

fix(ci): accept non-closing issue references in PR linking, and add the #36850 spec - #37193

Merged
nicobytes merged 13 commits into
mainfrom
nicobytes/36850-upgrade-github-actions-to-node-24-runtime-majors
Aug 28, 2026
Merged

fix(ci): accept non-closing issue references in PR linking, and add the #36850 spec#37193
nicobytes merged 13 commits into
mainfrom
nicobytes/36850-upgrade-github-actions-to-node-24-runtime-majors

Conversation

@nicobytes

@nicobytes nicobytes commented Aug 24, 2026

Copy link
Copy Markdown
Member

What this is

Spec-Kit PR 1 of 2 — the spec, plus one approved fix to the check that was blocking it.

Per SPEC_KIT_QUICK_START.md §3, the gate is approval, not merge: once a reviewer approves this spec, planning starts. Don't wait on the merge queue. PR 2 will carry the implementation and link back here.

Issue linking — Refs #36850, a deliberately non-closing link. The parent is answered by PR 2
— the actual sweep of 124 action references — not by this spec, so a closing keyword here would retire
the parent the moment PR 1 merges with every stale pin still in place. The Issue Linking gate had no
way to express that, which is why this check was red. Fixing that gate is now part of this PR (see
"The second change" below), so it is green on a non-closing reference.

Follow-up scope is tracked in #37194.

Specification phase for #36850 (see the note above on why the link is non-closing). Sequencing gate is clear — #36838 landed on 2026-08-04.

The second change: issue_comp_link-issue-to-pr.yml

Separately approved, and the reason this PR is no longer spec-only. The linking gate recognised
only closing keywords — fixes / closes / resolves — so a PR that advances an issue a later
PR resolves had no way to pass it. Worse, when the link came from the branch name instead of the body,
the workflow PATCHed This PR fixes: #N into the description, converting a non-closing situation into
a closing one behind the author's back. The only two outcomes were "retire the parent early" or "leave
the check red".

Three fixes:

  1. Non-closing references count as linkage. refs, references, related to, part of,
    contributes to — in the same-repo, org/repo#N and full-URL forms — satisfy the gate and set
    is_closing_link=false. Evaluated only after every closing form fails, so an explicit Fixes #N
    still outranks a Refs #N elsewhere in the body.
  2. The body PATCH is skipped for non-closing links. The PR-list comment on the issue thread still
    runs, so the link is tracked either way — the workflow just stops rewriting author intent.
  3. Worktree branches are matched. nicobytes/36850-… / oidacra/37132-… are the dominant layout
    on this repo and the old ^([0-9]+)- anchor missed every one. Digits must follow the slash directly,
    so gh-readonly-queue/main/pr-N-<sha> still does not match.

Plus one drive-by: the failure comment's printf carried 21 %s for 22 arguments, so bash restarted
the format and the last line rendered out of place. It is now built from an array.

Tests.github/workflows/tests/link-issue-to-pr.test.sh, 29 cases. It extracts the two parsing
steps from the workflow itself and runs them with the GitHub API stubbed, so it exercises the real
regexes rather than copies of them. Included are the closing-keyword paths that must not change.
Against the pre-change workflow, 18 of the 29 fail. Not CI-wired: .github/filters.yaml routes nothing
from .github/workflows/** to a build, and this workflow has no checkout step — that gap is exactly
what PR 2's lint job closes.

What to review

Two things, in this order:

  1. The spec — is this the right problem, scoped right, with measurable criteria? That is the gate
    for planning to start.
  2. The workflow fix — a shared merge gate, so worth a careful read even though it is small.

The problem

GitHub retired the Node 20 action runtime. 124 references to 8 actions across 51 files under .github/ are still on node20-era majors (some as old as checkout@v2, setup-node@v2-beta). Today that means deprecation annotations on every run; when the runner drops the compatibility shim, it means the build, test and release pipeline stops working. ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION is explicitly excluded.

Three decisions in the spec worth your attention

1. AC-005 deviates from an AC written in the issue. The issue asks for package-manager-cache: false where pnpm caching is manual. That precondition doesn't exist here: setup-node reads only $GITHUB_WORKSPACE/package.json, and this repo has no root package.json (only a stray package-lock.json). core-web/package.json's packageManager: pnpm@10.17.1 is invisible to it, and v6+ limits auto-caching to npm anyway. Auto-caching cannot fire at any of the 8 sites. The spec documents the deviation rather than adding 8 lines of verifiably dead config that would get cargo-culted. This is the deviation that needs your sign-off.

2. Scope grew by 4 actions beyond the issue's list. The runner emits the annotation per job, listing that job's stale actions. Bumping only the issue's 6 clears Initial Artifact Build — satisfying the issue's literal AC — but leaves annotations on initialize, label-pr and test. Adding github-script→v8, dorny/paths-filter→v4.0.3, dawidd6/action-download-artifact→v24 and docker/login-action→v4.6.0 closes the PR pipeline completely. github-script deliberately stops at v8, a pure node24 bump with no behavior change. (An earlier draft justified this by claiming v9 breaks require() — that was wrong, and is corrected in the spec: v9 still injects require via wrapRequire. v9's real cost is Octokit v5 → v7 under 14 inline scripts, which is a different review from a runtime bump.)

3. The guard script + lint job ship in PR 2 as a droppable batch-0 commit. .github/filters.yaml's backend filter only matches cicd_comp_*.yml, cicd_1-pr.yml and core-cicd/**/action.yml — so roughly 38 of the 51 files PR 2 touches get no build, no test and no lint today. That gap is why this issue exists. Constitution Principle V also needs a committed, confirmed-failing test, and there is no unit/integration/Postman layer for workflow YAML. If you disagree, it's one self-contained commit to drop.

Risk assessment already done

Two items looked dangerous and were run to ground against upstream source, so PR 2 doesn't have to re-litigate them:

  • checkout@v7's fork-PR block cannot fire in this repo. Reading src/unsafe-pr-checkout-helper.ts and src/input-helper.ts at tag v7.0.1: the guard isn't even reached for a default self-checkout. cicd_post-workflow-reporting.yml:58 is a bare - uses: actions/checkout@v4 with no with: block; cicd_publish-pr-test-image.yml has no checkout step at all and is gated to non-forks. Conclusion: no v6 pins, and no allow-unsafe-pr-checkout: true — adding it would permanently disarm a real protection against a risk we don't have.
  • download-artifact@v8's digest-mismatch: error default is the one genuine green-to-red risk, concentrated on the multi-GB maven-repo artifact and on finalize's build-reports-* pattern (N artifacts = N chances to fail, on every PR and merge-queue run). PR 2 lands v8 with explicit digest-mismatch: warn first — making the bump provably behavior-neutral — then flips to error in a separate revertable commit.

No product surface is touched: no Java, no Angular, no DB, no ES mapping, no API contract. Nothing falls under ROLLBACK_UNSAFE_CATEGORIES.md.

Deliberately deferred to follow-ups

  • 4 first-party actions still on runs.using: 'node16' (issue-fetcher, issue-labeler, changelog-report, rc-changelog) — these need dist/ rebuilds, and a regenerated ncc bundle can't be diff-reviewed the way YAML can. node16 is more deprecated than node20, so this should be filed immediately.
  • Cold-path third-party actions (release/nightly/scheduled). Two aren't mechanical: aws-actions/configure-aws-credentials v1→v6 needs OIDC and org-level trust-policy work, and slackapi/slack-github-action v1→v2+ changes the payload format.
  • .github/dependabot.yml for the github-actions ecosystem, and extending .github/filters.yaml so .github/** gets real validation. These two are the structural fix for this class of drift — they're what makes the next runtime bump routine.

🤖 Generated with Claude Code

Issue-resolution spec (Spec-Kit PR 1) for upgrading the 124 in-scope
GitHub Actions references under .github/ to node24 majors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 24, 2026 23:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR is Spec-Kit PR 1/2 for issue #36850, documenting the problem, scope, risks, and acceptance criteria for upgrading GitHub Actions used under .github/ to Node 24–compatible majors to eliminate Node 20 runtime deprecation annotations and prevent future CI breakage.

Changes:

  • Adds a complete spec.md describing the CI drift problem, in-scope actions, and upgrade targets.
  • Defines acceptance criteria and a proposed guard approach (script + lint job) to prevent future action-version drift.
  • Documents key risk areas (notably download-artifact v8 behavior) and explicitly deferred follow-ups.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread specs/36850-upgrade-github-actions-node-24/spec.md Outdated
Comment thread specs/36850-upgrade-github-actions-node-24/spec.md
Comment thread specs/36850-upgrade-github-actions-node-24/spec.md Outdated
@claude

claude Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Claude finished @nicobytes's task in 3m 37s —— View job


Code Review — PR #37193

  • Read the workflow changes (issue_comp_link-issue-to-pr.yml)
  • Read the test script (link-issue-to-pr.test.sh)
  • Review the docs + spec
  • Post review findings

I focused on the executable change (the workflow + its test), since spec.md and GIT_WORKFLOWS.md are documentation and the spec review points (AC-005, AC-007, the root-package.json rationale) were already resolved in the review thread.

New Issues

  • 🟡 Medium: .github/workflows/issue_comp_link-issue-to-pr.yml:71-73gh api --paginate … --jq '…' 2>/dev/null | head -1 can fail the step under the workflow's default shell (bash -eo pipefail). When head -1 closes the pipe after the first match, gh may receive SIGPIPE on stdout while more pages remain to fetch, exit 141, and — with pipefail — make the command-substitution assignment fail under -e, aborting this merge-gating step. Ironically this is the fix's own happy path: it only bites when a connected event exists and there are further timeline pages after the one it's found on (high-activity PRs). On fix(ci): accept non-closing issue references in PR linking, and add the #36850 spec #37193 itself the event was on the last page (33rd of 37 ≈ page 2 of 2), so it wouldn't reproduce there.

    • Assumption: gh streams per-page output (rather than buffering all pages before printing) and exits non-zero on SIGPIPE — standard Go stdout behavior.
    • What to verify: run gh api --paginate <endpoint> --jq '…' | head -1; echo $? against a resource with several pages and confirm exit status, then confirm the step's shell is bash -eo pipefail (GitHub's default — no defaults.run.shell override here).
    • Fix: decouple the exit code from the pipe, e.g. capture first, filter second:
      connected_all=$(gh api --paginate "repos/${{ github.repository }}/issues/$pr_number/timeline" \
        --jq '.[] | select(.event == "connected") | .source.issue.number' 2>/dev/null || true)
      connected_issue=$(printf '%s\n' "$connected_all" | head -1)
      Fix this →
  • 🟡 Medium: .github/workflows/tests/link-issue-to-pr.test.sh:18 — the harness runs set -uo pipefail (no -e) and sources the extracted steps in subshells that also lack -e/pipefail, whereas the real workflow executes them under bash -eo pipefail. Combined with the stubbed gh(), the 29 cases cannot observe a step that fails only because of pipe/exit-code semantics (the finding above being exactly that class). Consider sourcing the extracted step under set -eo pipefail so the harness matches the runtime shell — the stubs still make it deterministic.

Notes (non-blocking)

  • The branch regex (^|/)([0-9]+)- correctly matches worktree branches and rejects gh-readonly-queue/main/pr-N-<sha> (digits must follow the slash directly) — verified against every branch_case. It does share the pre-existing false-positive on date-like leading segments (e.g. 2024-fix2024), but that's unchanged from the old ^([0-9]+)- anchor and the branch path is last-resort (after closingIssuesReferences), so not worth changing here.
  • The non-closing/closing precedence is sound: the refs/part of branch is only reached after all closing forms (#N, owner/repo#N, full URL) fail, so Fixes #N always outranks Refs #N. Same-repo-only scoping for non-closing refs is deliberate and tested.
  • is_closing_link plumbing is safe: it's written to $GITHUB_OUTPUT only on the non-closing branch, and determine_issue defaults it via ${IS_CLOSING_LINK:-true}, so every other path still closes on merge.
  • The printf drive-by is consistent: the failure comment is now a quoted heredoc (flush-left after YAML block-scalar dedent → valid markdown), and the remaining printf at line 682 has 6 %s for 6 args.

Everything else in the diff checks out. The two findings are Medium (non-blocking) — worth a look before this becomes a required check, but neither blocks merge.

· branch nicobytes/36850-upgrade-github-actions-to-node-24-runtime-majors

- github-script v9: correct the deferral rationale. v9 does NOT break
  require() — src/main.ts still injects wrapRequire (a Proxy over
  __non_webpack_require__ handling bare and ./-relative IDs), verified
  against tag v9.0.0. The real exposure is Octokit v5->v7 under 14
  inline scripts; require('@actions/github') and const getOctokit have
  0 occurrences here.
- AC-005: make the criterion auditable against both of setup-node's
  caching paths — no cache:/cache-dependency-path at any call site
  (explicit), and no root package.json (automatic, added in v5).
- AC-007: drop the hard-coded 124/51 baseline from the criterion; the
  count drifts with main, so the AC is now non-zero -> zero.
- Define the headline count (executable uses: lines in yml/yaml) and
  correct the README item: 8 stale refs across 7 files, including three
  prose mentions, not 5 examples.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merging main brought in #37150, which deleted core-web/libs/dotcms and
stripped `npm run build:docs:dotcms` from publish_docs.yml itself. Only
build:docs:dotcms-models survives, so the cleanup follow-up should lean
towards deletion rather than revival.

Re-verified post-merge: setup-node@v2-beta + node-version 16.13.2 are
still present there, so the in-scope pin bump is unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nicobytes

Copy link
Copy Markdown
Member Author

Thanks — all three review points are addressed in 1d216ff and c3a5c23. On the Medium, you caught a real error in my rationale, but the proposed replacement isn't right either, so I went to the source.

github-script v9 and require()

You're correct that the three cited sites don't require('@actions/github') — they do require('fs') (×2) and require('./.github/scripts/qa-stuck-check/find-stuck-issues.js'). My rationale was wrong.

But the suggested stronger reading — that v9's ESM scope makes any require unavailable — doesn't hold. Verified against tag v9.0.0:

  • src/main.ts still injects it: require: wrapRequire and __original_require__: __non_webpack_require__.
  • src/wrap-require.ts is a Proxy over __non_webpack_require__ that handles both forms — moduleID.startsWith('.')path.resolve then delegate; otherwise target.resolve(moduleID, {paths: [process.cwd()]}).
  • src/async-function.ts still declares require: NodeRequire and __original_require__: NodeRequire in AsyncFunctionArguments.

So require('fs') and the local-module require would both keep working on v9. What v9 actually breaks, per its release notes: require('@actions/github') specifically (that package is now ESM-only) — 0 occurrences here; and getOctokit becoming an injected parameter, making const/let getOctokit a SyntaxError — also 0 occurrences here.

So why still stop at v8?

Not because v9 breaks — it probably doesn't, for this repo. Because v8 is a zero-behavior-change node24 bump, which is the entire ask of this issue, whereas v9 additionally moves Octokit v5 → v7 underneath 14 inline scripts. Auditing 14 scripts against an Octokit major is a different review from a runtime bump, and mixing it in is how a mechanical PR becomes an unreviewable one. The spec now says exactly that, and drops the false require() claim.

Net effect: same conclusion, correct reason, and the deferred v9 audit is now scoped to the right break.

Also worth flagging, unrelated to this PR

Merging main into this branch surfaced that .specify/feature.json is now tracked on main (added by #37150), containing {"feature_directory":"specs/36950-remove-dead-core-web-libs"}. Per SPEC_KIT_QUICK_START.md that file is local, untracked, and committed to neither PR — it's a per-developer pointer. Tracked, it silently redirects everyone's next Spec-Kit command to someone else's feature directory. Out of scope here (this PR is spec-only), but it should be git rm --cached'd and gitignored.

AC-001 requires residual Node deprecation annotations to be attributable
to a *named* follow-up issue; the spec described one without naming it.
Points the four deferral sites at #37194.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nicobytes nicobytes linked an issue Aug 25, 2026 that may be closed by this pull request
8 tasks
…36850)

The issue-linking gate only recognised closing keywords, so a PR that legitimately
advances an issue without resolving it had no way to pass. Worse, when the link came
from the branch name instead of the body, the workflow PATCHed "This PR fixes: #N"
into the description — turning a non-closing situation into a closing one behind the
author's back. A Spec-Kit PR 1 therefore had to either retire its parent issue on
merge or leave the check red.

- Accept `refs`, `references`, `related to`, `part of`, `contributes to` (same-repo,
  org/repo#N and full-URL forms) as valid linkage, carried through as
  is_closing_link=false. Checked only after every closing form fails, so an explicit
  `Fixes #N` still wins.
- Skip the body PATCH when the link is non-closing. The PR-list comment on the issue
  thread still runs, so the link is tracked either way.
- Match worktree-style branches (`nicobytes/36850-foo`), the dominant layout on this
  repo, which the old `^([0-9]+)-` anchor missed entirely. Digits must follow the
  slash directly, so `gh-readonly-queue/main/pr-N-<sha>` still does not match.
- Rebuild the failure comment from an array: the printf format carried 21 `%s` for
  22 arguments, so bash restarted the format and the last line rendered out of place.

Tests extract the two parsing steps from the workflow itself and run them with the
GitHub API stubbed, so they exercise the real regexes rather than copies. 29 cases,
including the closing-keyword paths that must not change. Not CI-wired — .github/**
has no build route today; that lands with the workflow lint job in PR 2.

Refs #36850

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nicobytes
nicobytes requested a review from a team as a code owner August 25, 2026 22:48
@github-actions github-actions Bot added Area : CI/CD PR changes GitHub Actions/workflows Area : Documentation PR changes documentation files labels Aug 25, 2026
The timeline endpoint defaults to 30 events per page and the lookup was unpaginated,
so on any PR with review history the "connected" event fell off the end. A
sidebar-linked PR then read as unlinked — harmless while the only question was
"linked or not", but wrong now that is_closing_link exists: the workflow reported an
issue as surviving the merge while GitHub was going to close it.

Found on #37193, where the connected event is the 33rd of 37: the check passed via
the body reference and reported closing=false, while the sidebar link means #36850
does close on merge.

Switches to `gh api --paginate` and keeps the failure path honest — the previous
`$(gh ... | head -1) || fallback` form reads head's exit status, so a failing gh
would never have reached the fallback.

Refs #36850

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nicobytes added a commit that referenced this pull request Aug 25, 2026
The timeline endpoint defaults to 30 events per page and the lookup was unpaginated,
so on any PR with review history the "connected" event fell off the end. A
sidebar-linked PR then read as unlinked — harmless while the only question was
"linked or not", but wrong now that is_closing_link exists: the workflow reported an
issue as surviving the merge while GitHub was going to close it.

Found on #37193, where the connected event is the 33rd of 37: the check passed via
the body reference and reported closing=false, while the sidebar link means #36850
does close on merge.

Switches to `gh api --paginate` and keeps the failure path honest — the previous
`$(gh ... | head -1) || fallback` form reads head's exit status, so a failing gh
would never have reached the fallback.

Refs #36850

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#36850)

Review feedback: too much code for "accept Refs #N". Two cuts, no behaviour change
for anything anyone asked for.

- Drop the cross-repo and full-URL variants of the non-closing reference. They were
  symmetry with the closing paths, not a requirement — nobody has needed a
  non-closing link to a private-repo issue, and each form is another branch in a
  merge gate. Same-repo `#N` only, pinned by tests so the limitation is explicit.
- Drop the six explicit `is_closing_link=true` writes. determine_issue already reads
  the flag as `${IS_CLOSING_LINK:-true}`, so every closing path was already covered;
  only the non-closing path needs to say anything.

Net: the feature is 20 lines instead of 66.

Refs #36850

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review feedback, second pass: still too much code. 115 added lines -> 51.

- Revert the failure comment to its printf form. Adding the two new bullets is now
  two argument lines plus a corrected format string, instead of a 47-line rewrite.
  (The format still needed correcting: it carried 21 "%s" for 22 arguments, so bash
  restarted the format and the last line rendered out of place. Now 24 for 24,
  verified by rendering.)
- Drop the if/else I added around the timeline lookup. The original had no error
  branch there and a failed lookup already degrades to "no sidebar link", which is
  the same outcome the branch produced.
- Tighten the comments on every remaining hunk.

No behaviour change from the previous commit; the 30 tests are untouched and green.

Refs #36850

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nollymar
nollymar previously approved these changes Aug 27, 2026

@nollymar nollymar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving link-issue-to-pr changes only

…de-24-runtime-majors

Resolves the conflict in .github/workflows/issue_comp_link-issue-to-pr.yml.

Both sides edited the "Issue Linking Required" comment body, and both changes
are wanted, so this is a semantic merge rather than picking a side:

- main (6bde00d) replaced the fragile `printf "%s\n\n%s..."` format string with
  a quoted heredoc, because the format string kept drifting out of alignment
  with its arguments as lines were added, breaking list and heading rendering.
- this branch had added two bullets to the printf form: the `Refs #123` /
  `Part of #123` option (link without closing on merge) and the
  `user/123-feature-description` worktree branch pattern.

Kept main's heredoc structure and ported both bullets into it.

Verified: link-issue-to-pr.test.sh passes 30/30. Note the suite needs jq --
without it every body-parsing case fails silently and only the branch-name
cases pass, which looks like a regression and is not one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nicobytes
nicobytes force-pushed the nicobytes/36850-upgrade-github-actions-to-node-24-runtime-majors branch from 19feb7b to 606375c Compare August 28, 2026 10:49
The guard script and its cicd_pr_actions-lint.yml job are removed from this
change and deferred to #37194. The upgrade does not need them, and a version
bump and a new permanent CI job are two changes with two different reviewers:
one judging versions, the other judging whether the team wants more CI to own.

AC-007 is rewritten rather than deleted, because the honest consequence has to
be on the record: this change now ships NO executable test. That is a wider
Principle V exception than the one first recorded, and it costs three things --
no confirmed-failing (Red) state demonstrable in CI; the ~38 files that
.github/filters.yaml routes to no build keep getting no validation, which is the
root cause of this issue; and nothing prevents recurrence until #37194 lands.

Also corrected while here: the workflow_dispatch dry-run list named
cicd_scheduled_qa-stuck-check.yml and cicd_pr_qa-stuck-check-validate.yml, both
of which #37173 deleted from main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nicobytes nicobytes changed the title docs(ci): spec for GitHub Actions Node 24 runtime upgrade (#36850) fix(ci): accept non-closing issue references in PR linking, and add the #36850 spec Aug 28, 2026
@nicobytes
nicobytes enabled auto-merge August 28, 2026 14:45
@nicobytes
nicobytes added this pull request to the merge queue Aug 28, 2026
Merged via the queue into main with commit 5682950 Aug 28, 2026
47 checks passed
@nicobytes
nicobytes deleted the nicobytes/36850-upgrade-github-actions-to-node-24-runtime-majors branch August 28, 2026 15:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback Area : CI/CD PR changes GitHub Actions/workflows Area : Documentation PR changes documentation files

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Upgrade GitHub Actions to Node 24 runtime majors

3 participants