Skip to content

fix(package): require node >= 22 and stop installing the release tooling - #8

Merged
allanbowe merged 6 commits into
mainfrom
fix/node-engines-lts
Oct 5, 2026
Merged

allanbowe merged 6 commits into
mainfrom
fix/node-engines-lts

Conversation

@sasjs-dev

@sasjs-dev sasjs-dev Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

What

Three changes, all about what this repo installs and runs.

Engines. engines.node was >=20. Node 20 reached EOL on 2026-04-30 (18 on 2025-04-30); the oldest line still receiving security fixes is 22, which is also what CI runs. Raised to >=22. .npmrc sets engine-strict=true, so this is enforced on install rather than warned about.

The refresh job. It pinned node 20 explicitly, which would now fail npm ci against the raised floor. Moved to 22. The next scheduled run was Monday, so this would have gone red unattended.

Release tooling. semantic-release and @semantic-release/exec are no longer devDependencies, and the toolchain no longer resolves from npx at release time either. It lives in release/ with its own package.json and lockfile, installed by the publish workflow with npm ci --prefix release. That pins the resolved versions and their integrity hashes rather than fetching the transitive tree fresh on every release - a tree that runs with publish credentials.

With the toolchain out of this repo's dependency graph, the standing workarounds go too:

  • scripts/audit.mjs deleted (its only job was naming the unfixable advisories)
  • the overrides for ip-address / undici deleted
  • the two audit steps collapse to one npm audit --audit-level=low, which passes with zero exemptions

The toolchain's own advisories, in their own scope. Keeping that tree out of the package's audit is correct - it is not shipped - but it must not become invisible either: it runs in CI with publish credentials. release/audit.mjs audits it separately and by name. Twelve advisories are exempt, each with the reason it cannot be fixed from this repo (the ranges cover the newest published version) and the reason it is unreachable here (the braces denial of service needs an attacker-controlled brace pattern, and the patterns come from this repo's own release config, not from the commits being matched). Anything else fails the build, and an exemption that stops appearing is reported so it can be deleted rather than left to rot. It uses --package-lock-only, so it needs no install and gates every PR as well as the release.

Verification

  • package-lock.json: 469 -> 13 entries (prettier, typescript and their deps); release/package-lock.json: 453
  • npm audit --audit-level=low: 0 vulnerabilities
  • node release/audit.mjs: 12 exempt, exit 0
  • npm run lint: clean
  • npm test: 15 pass
  • every plugin in the release config resolves from release/node_modules

Notes

Running the tool from release/ instead of npx does not change what the tool does - it still executes at release time with publish credentials. What it changes is that the resolved versions and integrity hashes are pinned, and that the package's own audit is no longer the only place the risk could have shown up.

The declared node floor permits end-of-life releases - node 14 and 16 reached
EOL in 2023, 18 on 2025-04-30, and 20 on 2026-04-30. The oldest line still
receiving security fixes is 22, which is also the version CI runs.

Raise engines.node to >=22 so the declaration matches the runtimes this
package is tested and supported on.

Verification: the only package.json change is the engines.node value, and CI
runs on node 22.
semantic-release was a devDependency, and its tree is where every advisory in
this repo came from: micromatch -> braces, and the npm package that
@semantic-release/npm bundles (ip-address, undici, brace-expansion,
http-cache-semantics). None of them has a patched release, so the audit carried
twelve standing exemptions and two overrides to work around them.

Run it the way sasjs/cli does instead - npx fetches the pinned pair for the
release run only, so none of it enters the lockfile. The tree drops from 469
lock entries to 13, the overrides go, and scripts/audit.mjs - whose whole
purpose was to name the unfixable advisories - is deleted. `npm audit
--audit-level=low` now passes with no exemptions, in CI and in publish.

The advisories are unfixable and unreachable: the braces DoS needs an
attacker-controlled brace pattern, and the patterns here come from the repo's
own release config, not from the commits being matched. Not auditing the tool
therefore loses nothing.

Verification:
- npm audit --audit-level=low: 0 vulnerabilities
- npm run lint: clean
- npm test: 15 pass
- package-lock.json: 13 entries (prettier, typescript and their deps)
@sasjs-dev
sasjs-dev Bot force-pushed the fix/node-engines-lts branch from c81975e to 20066e2 Compare October 5, 2026 10:08
@sasjs-dev sasjs-dev Bot changed the title fix(package): require the oldest supported LTS (node >= 22) fix(package): require node >= 22 and stop installing the release tooling Oct 5, 2026

@sasjs-dev sasjs-dev Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reviewed the full base...main diff (6 files: engines >=22, release tooling removed from devDependencies, audit.mjs deleted, overrides removed, workflows switched to plain strict audits, README updated) plus hardening checks at the head: pinning (all exact), .npmrc (ignore-scripts + save-exact + engine-strict), lockfile present and regenerated, gitleaks native hook (pinned @nogoo9/gitleaks binary, not neutered), CI audit gate (npm audit --audit-level=low across dev - strict, clean at the head, verified with npm audit --package-lock-only: 0 vulnerabilities), depcheck triage (clean; typescript is invoked via build:ts's tsc, a script-injection false positive), and secrets scan. CI (build, server) is green on the head. This is a coherent rework: with semantic-release out of the tree, the exemption-based audit script loses its reason and the plain audit is now clean. Findings:

  • .github/workflows/refresh.yml:23 - the refresh job still pins "node-version: 20" while package.json:72-73 now requires >=22. The two other workflows were bumped to 22 but this one was missed. Not cosmetic: .npmrc sets engine-strict=true and package.json is the manifest the job installs, so "npm ci" (refresh.yml:25, run without --ignore-scripts) fails immediately on node 20 with ENOTSUP (verified locally: engine-strict + a manifest engines conflict makes npm ci exit 1). The scheduled refresh will break on its next Monday run. Bump to 22.

  • .github/workflows/publish.yml:78-84 - "npx -p @semantic-release/exec@7.1.0 -p semantic-release@25.0.9 -- semantic-release": pinned top levels, and npx auto-installs without prompting in non-interactive CI (verified under npm 11). Same caveat as the org's other release workflows: the transitive tree is resolved fresh at release time, not locked, so a compromised transitive package executes with publish credentials. The old tree was at least lockfile-pinned (if advisory-ridden); the new one has no integrity lock at all. A dedicated lockfile or npm-shrinkwrap for the toolchain would pin the resolved tree. Advisory - the tradeoff is explicit in the comment, so this is a posture note, not a defect claim.

  • scripts/audit.mjs deletion: verified no remaining references - "npm run audit" script removed from package.json, no workflow calls it, README rewritten to describe the new posture. Dead code removal confirmed clean.

  • The overrides removal (ip-address/undici) is safe: those advisories lived inside the npm package @semantic-release/npm bundled, which is no longer installed anywhere (lockfile regenerated, full audit clean). Verified against the head lockfile audit.

2 findings above for review (refresh.yml node pin, release-toolchain lock posture).

.npmrc sets engine-strict=true, so the weekly refresh job's node 20 would now
fail `npm ci` outright against the >=22 manifest - and the next scheduled run
is Monday. Move it to 22, matching the other two jobs in the workflow.
@sasjs-dev

sasjs-dev Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Both addressed.

  • refresh.yml: correct, and thank you - that one was a real break I missed. engine-strict=true means the weekly refresh job would have failed npm ci outright on its next Monday run. Bumped to 22 in e591c8a.
  • npx integrity: accepted as a posture note, and the trade is now stated at the step. Plainly: the top levels are pinned, the transitive tree is fetched at release time, so this is weaker than the old lockfile-pinned devDependency - which is the point, since that tree was where every unfixable advisory came from. Neither arrangement is strictly better; this one trades an integrity lock for a clean audit and no standing exemptions. A dedicated toolchain lockfile would close the gap if you want it.
  • The confirmations (audit.mjs fully unreferenced, overrides safe to drop) are appreciated - those were the two parts I most wanted a second pair of eyes on.

sasjs-dev Bot added 3 commits October 5, 2026 12:39
The release toolchain now lives in `release/` with its own package.json and
lockfile, installed by the publish workflow with `npm ci --prefix release`,
rather than being resolved from npx at release time. Locked, so the resolved
versions and their integrity hashes are pinned - the gap raised in review, where
npx pinned only the top levels and fetched the transitive tree fresh.

The package's own `files` allowlist already keeps `release/` out of the tarball.

Verification: every plugin in the release config resolves from
release/node_modules.
The toolchain in release/ carries twelve advisories that cannot be fixed from
this repo - the ranges cover the newest published version - and that code runs
with publish credentials. Moving it out of the package's dependencies kept it
out of the package's `npm audit`, which is right; it also made it invisible,
which is not.

release/audit.mjs reports it separately and by name: the twelve are exempt with
their reasons, anything else fails the build, and an exemption that stops
appearing is reported so it can be deleted rather than left to rot. It uses
--package-lock-only, so it needs no install and runs on every PR as well as in
the release.

Verification: node release/audit.mjs reports 12 exempt and exits 0.
…p release]

sas-language runs `prettier --check .`, which covers release/ - it is the only
one of the five repos that checks that directory - and it rejected the new
script. Formatted with that config (printWidth 100).

The file is kept byte-identical in all five repos, so the same content goes to
the four that do not check it. Otherwise the copies drift and a later fix lands
in some of them only.

Verification: `prettier --check .` clean, `node release/audit.mjs` still reports
12 exempt and exits 0, in all five.

@sasjs-dev sasjs-dev Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Re-review at the new head 45df3c8; the delta since the prior review at 20066e2 is the refresh.yml node bump (refresh.yml:23 now 22 - the prior finding, addressed) and the release-tooling lock tree (release/package.json + release/package-lock.json, npm ci --prefix release at publish.yml, semantic-release run from release/node_modules/.bin). The lock tree matches the release config's plugin set (@semantic-release/exec 7.1.0, semantic-release 25.0.9 - the release config uses commit-analyzer, release-notes-generator, exec and github, all resolved inside semantic-release's own tree or the locked tree, verified present in the lockfile). This also addresses the prior toolchain-lock posture note: the resolved tree and its integrity hashes are now pinned rather than fetched ad hoc with publish credentials. CI (build, server) is green on the head. The delta since the prior review adds release/audit.mjs - a scoped audit of the release toolchain with named, justified exemptions for the advisories that cannot be fixed from this repo (braces, brace-expansion, ip-address, undici inside the npm package @semantic-release/npm bundles) - sound. Hardening checks at the head all passed in the prior review and the delta does not touch them.

No blocking issues.

@sasjs-dev

sasjs-dev Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Thanks - nothing to action from this one.

For the record, the only addition since the review before this one is release/audit.mjs and its workflow hooks, which is the scoped toolchain audit you confirmed sound. Worth noting that this is the only one of the five repos whose prettier --check . covers release/, which is why the script is formatted to this repo's config and kept byte-identical in the other four.

@allanbowe
allanbowe merged commit bac6e07 into main Oct 5, 2026
2 checks passed
@allanbowe
allanbowe deleted the fix/node-engines-lts branch October 5, 2026 19:28
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 0.3.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant