Skip to content

Bump to Node 24, upgrade GitHub Actions, fix primaries-required bug, add prettier#1

Merged
O-Mutt merged 19 commits into
mainfrom
chore/bumpNode
May 19, 2026
Merged

Bump to Node 24, upgrade GitHub Actions, fix primaries-required bug, add prettier#1
O-Mutt merged 19 commits into
mainfrom
chore/bumpNode

Conversation

@O-Mutt

@O-Mutt O-Mutt commented May 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • Node 24 runtime — action now runs on node24; a new .nvmrc (containing 24) is the single source of truth and workflows source the Node version from it instead of hardcoding
  • GitHub Actions upgraded to latest majorsactions/checkout@v6, actions/setup-node@v6, actions/upload-artifact@v7, github/codeql-action/*@v4; deprecated v2/v3 references removed
  • Behavior bug fixcore.getInput returns "" (not undefined) for unset inputs, so the prior primariesRequired !== undefined guard always ran the busy-skip path. A busy primary was silently skipped even when the caller didn't request a free primary count. New parsePrimariesRequired() returns undefined for unset/empty/zero/NaN and only treats positive integers as a real requirement
  • index.js refactor for readability — extracted parsePrimariesRequired, resolveApiPath, and reportFallback helpers so main() reads top-to-bottom; the duplicated fallback-on-error block is now one function call; runner loop flattened with early continues; core.summary.write() properly awaited; use-runner output is always a JSON-stringified array regardless of code path
  • API reliability — 10s socket timeout on the GitHub runners API call, plus the GitHub-recommended Accept: application/vnd.github+json and X-GitHub-Api-Version: 2022-11-28 headers
  • Tooling — added Prettier (with eslint-config-prettier so ESLint and Prettier stop fighting), npm run format / npm run format:check scripts, single-quote ES5-trailing-comma config matching the existing codebase style; bumped ecmaVersion: 2018 → latest to match Node 24
  • CI — new lint job runs npm run lint and npm run format:check; units job now uses setup-node so tests run on the declared Node version
  • Cleanup — removed wait.js (dead template leftover, never imported) and .eslintrc.json (legacy config that ESLint 9 ignores in favor of eslint.config.mjs)
  • Tests grew 5 → 18 — covering the primaries-required regression, non-200 responses, label-superset matching, header verification, plus unit tests for the new helpers

Why

The codebase was on Node 16 in workflows / Node 20 in the action runtime, with several deprecated action versions that would soon stop receiving security fixes. While doing that bump, an audit surfaced a real behavior bug in the runner-availability logic plus enough small readability wins to be worth landing together.

Test plan

  • npm run all passes locally on Node 24 (lint, format:check, build, tests)
  • check-dist workflow passes on the new runtime (dist/ regenerated against Node 24)
  • CodeQL workflow runs cleanly on github/codeql-action@v4
  • units-test workflow's new lint job passes
  • units-test workflow's test and test-output jobs still exercise the action against the GitHub API
  • Manually verify the action works against a repo that has only busy self-hosted primaries with primaries-required unset (should select the primary, not fall back — the bug fix)

Commits

Split into 6 logical commits for easier review:

  1. chore(node): bump runtime to Node 24, manage version via .nvmrc
  2. ci: upgrade workflow actions to latest majors, add lint and format-check jobs
  3. chore: remove dead wait.js and legacy .eslintrc.json
  4. refactor(index): fix unset primaries-required handling, extract helpers, harden API call
  5. chore(tooling): add Prettier with CI format check, modernize ESLint config
  6. build: rebuild dist/ for Node 24 runtime

Follow-ups (not in this PR)

  • npm audit reports 9 transitive-dep vulnerabilities (4 moderate, 5 high) — worth a separate npm audit fix PR
  • .claude/, .claude-flow/, .mcp.json, and CLAUDE.md are local Claude Code tooling — worth adding to .gitignore so they can't be accidentally committed

O-Mutt added 19 commits May 19, 2026 04:42
Update contribution credit for O-Mutt
Adds an .nvmrc as a single source of truth for the Node version and
switches the action's JavaScript runtime from node20 to node24. The
package.json engines field documents the requirement for local devs.
…eck jobs

- actions/checkout v4 -> v6
- actions/setup-node v3.8.1 -> v6, reading Node version from .nvmrc
  instead of hardcoded 16.x
- actions/upload-artifact v3 -> v7
- github/codeql-action/* v2 -> v4
- new lint job runs `npm run lint` and `npm run format:check`
- units job now uses setup-node so tests run on the declared Node version
wait.js was leftover from the actions/template-javascript-action starter
and was never imported. .eslintrc.json is ignored by ESLint 9 in favor of
the flat config in eslint.config.mjs.
…rs, harden API call

Behavior fix:
- core.getInput returns "" not undefined for an unset input, so the
  prior `primariesRequired !== undefined` guard always evaluated true.
  That made the busy-skip path run even when the user did not set
  primaries-required, silently rejecting busy primaries. The new
  parsePrimariesRequired() returns undefined for unset/empty/zero/NaN
  and only treats positive integers as a real requirement.

Structure:
- Extract parsePrimariesRequired, resolveApiPath, and reportFallback so
  main() reads top-to-bottom and the duplicated fallback-on-error block
  becomes a single function call.
- Flatten the runner loop with early continues.
- Always emit `use-runner` as a JSON-stringified array regardless of
  which code path produces it.
- Await core.summary.write() so the summary cannot be dropped on a fast
  process exit.

Reliability:
- 10s socket timeout on the runners API call.
- Send the GitHub-recommended `Accept: application/vnd.github+json` and
  `X-GitHub-Api-Version: 2022-11-28` headers.

Tests grow from 5 to 18, covering the primaries-required regression,
non-200 responses, label-superset matching, header verification, and
unit tests for parsePrimariesRequired and resolveApiPath.
…onfig

- Add prettier and eslint-config-prettier devDependencies
- .prettierrc.json: single quotes, ES5 trailing commas, semis, LF endings
  to match the existing codebase style
- .prettierignore excludes dist, lockfiles, license/codeowners, and the
  local-only .claude tooling directories
- npm scripts: `format` writes, `format:check` is CI-friendly, and `all`
  now runs format:check alongside lint/test/build
- eslint.config.mjs: append eslint-config-prettier so ESLint stops
  flagging things Prettier owns; bump ecmaVersion 2018 -> latest to
  match Node 24; ignore the local .claude dirs
- Format README and codeql-config to match the new style
Regenerate the bundled action entrypoint with @vercel/ncc under Node
24.10.0 so check-dist passes on the new runtime.
- Rename action to "Self-Hosted Runner Fallback" so the Marketplace
  listing and the README title agree
- Update author metadata to reflect current ownership
- Replace the inaccurate "repo and actions scope" guidance on
  github-token with the actual scope matrix: classic PAT with repo for
  user repos, admin:org for organizations, manage_runners:enterprise
  for enterprises
- Document the use-runner output as a JSON-encoded array and remind
  consumers to wrap it in fromJson(...)

README:
- Add Marketplace, build, and license badges pointing at the publishing
  fork (O-Mutt/runner-fallback-action)
- Lead with a "what / why" tagline and a 10-line copy-pasteable quick
  start before the input tables
- Split inputs into Required / Scope (mutually exclusive) / Optional
  with a defaults column
- Add a dedicated Outputs section
- Add a Permissions section that maps each scope to its required token
  type and permission
- Add a Versioning section that recommends pinning to a major tag or a
  commit SHA and tracks the Node-runtime lineage of v1 vs v2
- Add a Contributing section pointing at `npm run all` and explaining
  the dist/ commit policy
- Update the example reference from @v1 to @v2 to match the upcoming
  release
`npm ci` was failing on all GitHub-hosted runners because the previously
committed package-lock.json was missing several platform-specific binary
entries (@unrs/resolver-binding-*, @napi-rs/wasm-runtime, @emnapi/*) — a
known npm 10+ bug where `npm install` does not always record every
platform variant of optional dependencies.

Regenerated the lockfile from scratch (rm -rf node_modules
package-lock.json && npm install) so all platform variants are listed.
The fresh resolution also picked up newer compatible patch versions of a
few transitive dependencies of @actions/core, which in turn pulled in
undici for the http client; rebuilt dist/ accordingly.
The `test` and `test-multiple-labels` jobs ran the action with
`github-token: ${{ secrets.TEST_GITHUB_TOKEN }}` and that secret is not
configured in this fork, so the action correctly failed with
"Input required and not supplied: github-token".

Fall back to the auto-injected `secrets.GITHUB_TOKEN` when the test
secret is unset. That token lacks runner-admin scope so the API call
will 401, but with `fallback-on-error: true` the action emits the
fallback runner instead of failing, which lets these jobs exercise the
fallback-on-error code path even without bespoke secrets. Maintainers
who configure TEST_GITHUB_TOKEN still get the primary-path coverage.
- Rename .github/workflows/test.yml to ci.yml and the workflow display
  name from 'units-test' to 'CI'. The workflow does lint, format-check,
  unit tests, and runs the action against the GitHub API — "CI" is a
  more accurate label for that scope.
- Add a concurrency group that cancels superseded runs on the same PR
  while still running every push to main, so we stop paying for stale
  PR commits after a force-push.
- Update the README badge URL and label.
CODEOWNERS still had the actions/template-action default
"@actions/actions-runtime", which is not a real reviewer and silently
disabled automatic review assignment. Set the owner to the current
maintainer.

.gitignore now excludes .claude/, .claude-flow/, .mcp.json, and
CLAUDE.md so contributors using Claude Code locally cannot accidentally
commit their personal agent configuration.
SECURITY.md documents which majors receive fixes, points reporters at
GitHub's private vulnerability reporting flow, and scopes what we
consider in vs. out (upstream-dep CVEs route through Dependabot).

CONTRIBUTING.md captures the conventions a new contributor needs:
.nvmrc as the Node source of truth, the committed dist/ policy enforced
by check-dist, the Prettier+ESLint+Jest tooling, and the release flow
that re-points the floating @v2 tag for Marketplace consumers.
`@actions/core@1.11.1` ships its own bundled `@actions/http-client@2.2.3`
which pinned `undici <=6.23.0`. That undici range is affected by:

- GHSA-g9mf-h72j-4rw9 (unbounded decompression chain, high)
- GHSA-2mjp-6q6p-2qxm (HTTP request/response smuggling)
- GHSA-vrm6-8vpv-qv8q (unbounded memory in WebSocket permessage-deflate)
- GHSA-v9p9-hfj2-hcw8 (unhandled exception in WebSocket client)
- GHSA-4992-7rv2-5pvq (CRLF injection via `upgrade` option)

`npm audit fix` cannot reach the nested undici because it would have to
bump `@actions/core` across a major. Pinning `undici` via npm
`overrides` at the root forces every transitive resolution onto the
patched 6.24 line without dragging in a `@actions/core` major bump.

`npm audit` now reports 0 vulnerabilities. The smaller, fixed undici
also reduces the bundled dist/ from ~1.93 MB to ~1.10 MB.
Previously the Quick start referenced \`secrets.RUNNER_API_TOKEN\`
without explaining where it came from, and required-fields detail lived
only in a reference table. A first-time Marketplace user had to
assemble the setup process themselves.

Added three sections without disturbing the reference material:

- **Prerequisites** — what state the repo/org/enterprise must be in
  before any of this works (runner registered, admin-scoped token,
  token stored as a secret).
- **Setup** — five numbered steps walking a new user from
  "create token in GitHub UI" through "consume the output with
  fromJson". Each step names the exact GitHub menu path so users do
  not have to guess.
- **Troubleshooting** — a symptom/cause/fix table covering the
  failures we have seen most often: empty token secret, 401 from a
  wrong-scope token, 404 from a misspelled org slug, fallback being
  picked when primaries appear online (primaries-required tuning),
  fromJson omissions, and bursty-load races that the concurrency
  block in the Full example fixes.

The required-fields table is unchanged and remains the formal
reference. README is 211 lines, still well under the 500-line limit.
…cy signals

Three badges (CI / static Marketplace / License) is enough to say "this
exists and is MIT", but does not signal whether the action is actively
maintained, currently safe to use, or which version is on the
Marketplace right now. Reworked the badge row to:

- Keep the CI status badge as the primary "is main green?" signal
- Add the CodeQL workflow badge so consumers can see we run static
  security analysis on every push and PR
- Replace the static "Marketplace" badge with a dynamic
  `github/v/release` badge that auto-updates to the current released
  tag — still links to the Marketplace listing page
- Add a Snyk vulnerabilities badge via shields.io so a glance is
  enough to see whether any known transitive CVE has cropped up
- Add a "last commit" badge so users can immediately tell the project
  is being maintained (or warn them if it goes quiet)
- Keep the License badge as the rightmost reassurance
- New \`npm run test:coverage\` script runs \`jest --coverage\`
- jest config in package.json collects coverage on index.js and emits
  lcov + text-summary + json-summary reporters
- Coverage thresholds set just below current values (branches 75,
  functions 70, lines 50, statements 55) so CI fails on regression
  but passes on the existing baseline. Improving these is a follow-up
  that needs main()/reportFallback() tests with @actions/core mocked.
- CI units job now runs test:coverage and uploads lcov.info via
  codecov/codecov-action@v5. CODECOV_TOKEN is optional for public
  repos but supported when present.

README badges: add Codecov coverage badge, add GitHub stars badge.

Coverage output ignored via existing .gitignore entry.
Adds a release-please workflow so version bumps, CHANGELOG entries, and
GitHub Releases are generated from Conventional Commit history instead
of being tagged by hand.

Pieces:

- .github/workflows/release-please.yml runs on every push to main. It
  invokes googleapis/release-please-action@v4 in manifest mode, then
  runs a follow-up step that re-points the floating major tag (e.g.
  v2 -> v2.0.0) after a release is cut. Marketplace consumers pinned
  to @v2 pick up new releases automatically without action from the
  maintainer.

- .release-please-config.json declares the package as release-type
  "node", forces the first release to 2.0.0 via "release-as", uses
  the merge-base with main (dc7732f) as bootstrap-sha so the v2.0.0
  changelog includes commits from this PR forward only, and pulls in
  docs/build/ci/chore sections in addition to feat/fix.

- .release-please-manifest.json pins the last released baseline to
  the current package.json version (1.0.2). release-please updates
  this file in the release PR when a new version ships.

- CONTRIBUTING.md updates the Conventions section to call out that
  Conventional Commits drive the version bump, and replaces the
  manual-tag "Releasing" section with the release-please flow plus a
  short note about the first-time Marketplace tickbox.

Requires `contents: write` and `pull-requests: write` on the
GITHUB_TOKEN, declared at the workflow level.
…orm wasm fallbacks

npm 11 on macOS still records the lockfile with platform-specific
optional dependencies missing for variants that the current host does
not need. jest@30 -> @jest/core -> jest-resolve -> unrs-resolver pulls
@unrs/resolver-binding-wasm32-wasi, whose sub-dependencies
@emnapi/core and @emnapi/runtime were therefore absent from the
lockfile when it was generated on darwin-arm64. Linux runners then
fail \`npm ci\` with "Missing: @emnapi/core@1.10.0 from lock file"
because they exercise the wasm fallback path.

Regenerated the lockfile inside an official \`node:24\` Linux container
(\`docker run --rm -v $PWD:/app -w /app node:24 sh -c "npm install --no-audit --no-fund"\`),
which records every cross-platform variant and lands the missing
@emnapi/* entries. Verified \`npm ci\` now succeeds both locally on
macOS and inside the Linux container. dist/ is byte-identical, so no
rebuild needed.

If this bug bites again on a future regen, run the same docker
one-liner before committing the lockfile.
@codecov-commenter

Copy link
Copy Markdown

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

Thanks for integrating Codecov - We've got you covered ☂️

@O-Mutt O-Mutt merged commit 40bb207 into main May 19, 2026
8 checks passed
@O-Mutt O-Mutt deleted the chore/bumpNode branch May 19, 2026 11:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants