Skip to content

fix(utils): drop the Node crypto import, and tighten what the package ships - #274

Merged
allanbowe merged 6 commits into
mainfrom
fix/base64-browser-safe
Oct 5, 2026
Merged

allanbowe merged 6 commits into
mainfrom
fix/base64-browser-safe

Conversation

@sasjs-dev

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

Copy link
Copy Markdown
Contributor

What

Four changes to the package, all about what consumers download and what this repo runs.

Drop the Node crypto import. encodeToBase64 prepends 100 random bytes (200 hex characters) to the value and base64s the result; decodeFromBase64 strips the prefix by fixed offset. The prefix is obfuscation padding, not a secret, so it does not need Node's crypto module - the Web Crypto global does the job on every runtime we support (node 22 is the floor across the estate; browsers and the VS Code extension host expose crypto.getRandomValues).

Ship only the built output. The package had no files allowlist, so npm fell back to .gitignore and the tarball carried .git-hooks, .github, .vscode, scripts/, updateCli, updateServer, CONTRIBUTING.md, PULL_REQUEST_TEMPLATE.md and a dead checkNodeVersion.js. It now carries the build output and README only: 364 files -> 347.

Supported node. engines.node was >=14; 14, 16, 18 and 20 are all EOL. Raised to >=22, the oldest supported LTS.

Release tooling. semantic-release and its two plugins are no longer devDependencies. They live in release/ with their own package.json and lockfile, installed by the publish workflow with npm ci --prefix release, so their tree - micromatch -> braces, and the npm package @semantic-release/npm bundles - stays out of the package's dependencies and out of npm audit, while the resolved versions and their integrity hashes are pinned.

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). 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.

Why it matters

The adapter's browser bundle currently drags in crypto-browserify -> browserify-sign -> parse-asn1 -> asn1.js -> vm-browserify: 555 KB of AES/RSA/ECDSA polyfills plus an eval() from the vm shim, pulled in solely by this one import. That dual-use shape (dynamic code execution plus a full crypto suite in one bundle) is what makes npm's publish scanner slow to clear a package, and it is dead weight for consumers that never call encodeToBase64.

Scope, stated precisely: this holds for deep imports, which is what the adapter uses (@sasjs/utils/types, /error, /file, /time, /auth, /input, /logger). The barrel still re-enters crypto, because src/index.ts re-exports ./fs and fs/hash.ts imports createHash. That is #275.

Verification

  • base64.spec.ts pins the 200-character hex prefix (the offset decodeFromBase64 strips) and asserts each call is unique
  • npx tsc --noEmit: clean
  • npx jest --coverage: 39 suites / 440 tests pass, base64.ts 100%
  • npm run lint: clean
  • emitted build/utils/base64.js contains no require('crypto')
  • staged publishInit + npm pack --dry-run: 347 files (was 364), build output and README only
  • package-lock.json: 747 -> 315 entries; release/package-lock.json: 479 entries
  • npm audit --production --audit-level=low: 0 vulnerabilities (the CI gate)
  • node release/audit.mjs: 12 exempt, exit 0

Related

Companion change to the estate-wide engines bump to >=22 (the oldest supported LTS): sasjs/cli#1479, sasjs/lint#251, sasjs/sas-language#8, sasjs/test-framework#280, sasjs/adapter#919.

Follow-ups filed rather than folded in: #275 (the barrel re-exports fs, so a bare @sasjs/utils import still pulls the crypto chain) and #276 (encodeToBase64/decodeFromBase64 depend on Buffer).

encodeToBase64 only needs 100 random bytes as obfuscation padding, so the
Node `crypto` import buys nothing the Web Crypto global does not provide. The
prefix is not a secret - decodeFromBase64 strips it by fixed offset - and
every supported runtime exposes crypto.getRandomValues (node 22 is now the
floor across the estate, browsers and the VS Code extension host all have it).

Dropping the import removes the crypto-browserify -> asn1.js -> vm-browserify
chain (555 KB of AES/RSA/ECDSA polyfills plus the `eval()` in vm-browserify's
shim) from any browser bundle that pulls in @sasjs/utils. That dual-use shape
is what makes npm's publish scanner slow to clear a package, and it is dead
weight for consumers that never call encodeToBase64.

Verification:
- base64.spec.ts pins the 200-char hex prefix (the offset decodeFromBase64
  strips) and asserts each call is unique
- npx tsc --noEmit: clean
- npx jest --coverage: 39 suites, 440 tests pass; base64.ts 100%
- npm run lint: clean
- emitted build/utils/base64.js contains no require('crypto')
@sasjs-dev
sasjs-dev Bot force-pushed the fix/base64-browser-safe branch from 0da3ad8 to 896589b Compare October 5, 2026 09:44

@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...head diff (2 files, +67/-3) plus hardening checks: dependency pinning (all exact), .npmrc (ignore-scripts + save-exact), lockfile, pre-commit gitleaks hook (pinned v8.30.1, native hook verified not neutered), CI audit step (npm audit --production --audit-level=low, no continue-on-error), depcheck triage, and npm audit against the lockfile - all clean.

  • src/utils/base64.ts:33-33 - encodeToBase64 still calls Buffer.from(randomHexPrefix() + str).toString('base64'), so the browser bundle keeps a Buffer dependency unless the bundler polyfills it. The stated goal (no crypto shim in the browser bundle) is met - the crypto-browserify chain came from require('crypto') alone, and src/fs/hash.ts:3 still imports createHash from 'crypto' but nothing the adapter's browser bundle reaches imports the utils barrel's fs module (adapter imports are all deep: @sasjs/utils/types, /error, /file, /time, /auth, /input, /logger; only fs/internal/helper.ts pulls the utils barrel from fs, and the adapter never imports fs). However, if a consumer imports encodeToBase64 from the @sasjs/utils barrel, the barrel's export * from './fs' (src/index.ts:11) drags in fs/hash.ts and its crypto import right back - the crypto-free claim holds only for deep imports (which is what the adapter does). For the barrel path, Buffer also remains: browsers lack Buffer natively, so a bundler will inject a polyfill for base64.ts's Buffer usage. If the goal is a fully browser-clean encode path, replacing Buffer with btoa/TextEncoder (with a Buffer fallback for Node < 19) would remove the last browser-hostile dependency in this file. Advisory: the change is a strict improvement over crypto.randomBytes; the residual Buffer use is the remaining gap.

  • src/utils/base64.ts:19-26 - Math.random fallback fires only when the global crypto object is absent (Node < 19 without the Web Crypto global). The comment says this is sufficient for padding, which matches the format: decodeFromBase64 (src/utils/base64.ts:37-44) strips a fixed 200-char offset regardless of content, so predictability of the prefix does not break decoding. The one nuance worth noting: the prefix's stated job includes making the encoded value unique and stopping base64-of-a-password from being obvious; Math.random output is predictable, so on such runtimes two rapid successive calls could produce colliding prefixes. Not a security boundary (the prefix was never secret - Node's crypto.randomBytes was used for the same padding), but if uniqueness matters, Date.now() concatenated into the Math.random bytes would harden it. Advisory only.

  • src/utils/spec/base64.spec.ts:33-36 - the fallback test mutates globalThis.crypto via Object.defineProperty with a plain {} - this passes the typeof crypto !== 'undefined' check and forces the Math.random branch as intended, but note the restore in the finally block re-defines the property; if the surrounding environment defines crypto as non-configurable (some runtimes), the test's defineProperty would throw before the try block starts, failing the suite rather than skipping. Minor robustness note; the repo's CI (test (22)) passed on the head, so this is not currently biting.

  • The 200-char prefix length and 100-byte size (PREFIX_HEX_LENGTH = 200 hex chars from 100 bytes) preserve decode compatibility with existing stored credentials: decodeFromBase64's substring(200) offset is unchanged, so values encoded by the old crypto.randomBytes(100).toString('hex') prefix decode identically under the new implementation. No stored-credential break.

  • Commit subject "fix(utils): encode credentials without the Node crypto module" is a valid Conventional Commit with a release-driving type (fix) for a user-visible change; the repo releases via semantic-release (publish.yml), so this ships a patch release as intended.

2 findings above for review (residual Buffer use and the barrel-path crypto re-entry worth documenting or fixing, plus the minor test-robustness note). The format-compatibility and hardening checks are clean; CI is green on the head.

semantic-release and its two plugins were devDependencies. That tree is where
the unfixable advisories live - micromatch -> braces, and the npm package
@semantic-release/npm bundles - and none of them has a patched release.

Run it the way sasjs/cli does instead: npx fetches the pinned trio for the
release run only, so none of it enters the lockfile. The tree drops from 747
lock entries to 315, and the dead `semantic-release` script goes with it.

Verification:
- npm ci: 305 packages
- npm audit --production --audit-level=low: 0 vulnerabilities (the CI gate)
- npm run lint: clean
- npx jest: 39 suites, 440 tests pass

The full-tree audit still reports advisories. They come from jest's own tree,
which carries micromatch -> braces as well, not from the release tooling.
@sasjs-dev

sasjs-dev Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Thanks - splitting these into addressed, stale, and deliberately deferred.

Residual Buffer - agreed it is the remaining gap, and it is pre-existing: the original used Buffer.from too. Deferring, for a concrete reason: the UTF-8-correct replacement is TextEncoder + btoa on encode and atob + TextDecoder on decode, and this function stores CLI credentials, so a naive btoa swap would corrupt non-ASCII passwords. It also would not shrink the adapter bundle - the adapter already needs the Buffer polyfill, it uses Buffer.from directly in getAccessTokenForViya.ts and refreshTokensForViya.ts. Happy to do it as a follow-up if the goal is a fully browser-clean encode path on its own merits.

Barrel-path crypto re-entry - correct, and worth stating plainly rather than leaving implied: the crypto-free claim holds for deep imports, which is what the adapter uses, not for the barrel. src/index.ts re-exports ./fs and fs/hash.ts imports createHash. The commit message overstates it; I have corrected the PR body. Fixing the barrel itself changes what src/index.ts exports, so that is a public API change and a separate discussion.

Math.random fallback (base64.ts:19-26) and the defineProperty test (base64.spec.ts:33-36) - both are gone from the current head. You reviewed the first commit (+67/-3). With the estate floor now node >=22, the fallback was dead code for an unsupported runtime, so the head calls crypto.getRandomValues directly and the spec drops that test (head is +33/-3 plus the release-tooling commit).

Format compatibility and the commit subject - agreed on both; no stored-credential break, and fix ships a patch as intended.

@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 at the new head (5 files; the delta since the last review drops the Math.random fallback and the release tooling) plus hardening checks: pinning, .npmrc, lockfile, gitleaks hook, CI audit gate, depcheck triage, secrets scan, and npm audit against the head lockfile. CI (test (22)) is green on the head. The prior review's test-robustness finding is resolved by the fallback's removal. Findings on the current head:

  • src/utils/base64.ts:9-12 - the Math.random fallback is gone and randomHexPrefix calls crypto.getRandomValues directly, so encodeToBase64 now throws ReferenceError on any runtime without the Web Crypto global: Node < 19 (18 and earlier lack globalThis.crypto), and older browsers (pre-2021 Safari, pre-37 Chrome without the flag). utils ships no "engines" field, so npm cannot warn consumers either. decodeFromBase64 is unaffected (pure Buffer), but every encode call hard-fails on such runtimes. Either declare the floor ("engines": {"node": ">=19"} - matching the fix's own claim of the oldest supported LTS elsewhere in the org) or keep a guarded fallback. If intentional, the breaking change deserves a note in the release.

  • src/utils/base64.ts:15 - encodeToBase64 still uses Buffer.from(...).toString('base64'), so the barrel path keeps a browser polyfill dependency; only the crypto shim was removed. The adapter is unaffected (its imports are deep), but "export * from './fs'" at src/index.ts:11 drags fs/hash.ts:3 "import { createHash } from 'crypto'" back into any barrel consumer's bundle, re-introducing the very crypto-browserify alias chain this PR removes for deep imports. If the barrel is meant to be browser-clean too, re-exporting base64 directly and keeping fs off the public barrel (or splitting fs into a subpath export) is the structural fix. Advisory, unchanged in substance from the prior review.

  • .github/workflows/publish.yml:72-78 - the release toolchain is now "npx -p @semantic-release/exec@6.0.3 -p @semantic-release/git@10.0.1 -p semantic-release@25.0.9 -- semantic-release". Pinning correct; npx auto-installs in non-interactive CI without prompting (verified under npm 11). Same un-locked-transitive-tree caveat as the org's other release workflows: the top levels are pinned but the resolved tree is fetched ad hoc at release time, so a compromised transitive dependency executes with publish credentials. Consider a lockfile (or npm-shrinkwrap) for the toolchain. Advisory.

  • The 200-hex prefix format is unchanged (PREFIX_HEX_LENGTH = 200, decode strips substring(200)), so existing stored credentials decode identically. Verified against the new spec tests at src/utils/spec/base64.spec.ts:18-31. No break.

3 findings above for review.

Review finding: with the Math.random fallback gone, encodeToBase64 calls
crypto.getRandomValues directly, so it throws on any runtime without the Web
Crypto global - node 18 and earlier, and pre-2021 browsers. utils declared no
"engines" field, so npm could not warn a consumer either.

Declare ">=22": the estate's floor, and the oldest supported LTS. The lockfile
root is regenerated so the two agree.

Verification:
- npx jest: 39 suites, 440 tests pass
- npm run lint: clean
- package-lock.json root now records engines >=22
@sasjs-dev

sasjs-dev Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

All three addressed or answered.

  • missing engines (e3d2669): correct, and an omission on my part. With the fallback gone the function hard-fails below node 19 and utils gave npm nothing to warn with. Now declares >=22, the estate floor, with the lockfile root regenerated to match.
  • Buffer / barrel: unchanged in substance, and I am not fixing it in this PR. The barrel point is a public API change (src/index.ts re-exports ./fs), and the Buffer swap needs TextEncoder+btoa / atob+TextDecoder to stay UTF-8-correct - this function stores CLI credentials, so a naive swap corrupts non-ASCII passwords. Both are real follow-ups; neither is a defect in this change.
  • npx integrity: accepted and stated plainly - top levels pinned, transitive tree fetched at release time, weaker than a lockfile. The old arrangement had the lockfile and the unfixable advisories; this has neither the lock nor the advisories. A dedicated toolchain lockfile would close it if you want that.

@sasjs-dev

sasjs-dev Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

The two items I deferred now have issues, so they are tracked rather than dropped:

Neither blocks this PR.

sasjs-dev Bot added 2 commits October 5, 2026 12:39
…ling from a locked tree

Two changes to what leaves this repo.

`files` allowlist: the package had none, so npm fell back to `.gitignore` and the
tarball carried `.git-hooks`, `.github`, `.vscode`, `scripts/`, `updateCli`,
`updateServer`, `CONTRIBUTING.md`, `PULL_REQUEST_TEMPLATE.md` and a dead
`checkNodeVersion.js` - 364 files. It now carries the build output and the
README: 347 files, nothing that is not part of the library. This also keeps the
new `release/` tree out of the package.

Release tooling: it now lives in `release/` with its own package.json and
lockfile, installed by the publish workflow with `npm ci --prefix release`.
Locked, so the resolved versions and their integrity hashes are pinned rather
than fetched fresh at release time.

Verification:
- staged publishInit + npm pack --dry-run: 347 files, build output only
- npx jest: 39 suites, 440 tests pass; npm run lint: clean
- 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.
@sasjs-dev sasjs-dev Bot changed the title fix(utils): encode credentials without the Node crypto module fix(utils): drop the Node crypto import, and tighten what the package ships Oct 5, 2026
…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 6cf8bfa; the delta since the prior review at 02fa4b9 is the engines declaration (package.json:7-9 ">=22", lockfile root synced), a files allowlist, the release-tooling lock tree, and the scoped release audit script. The prior node-floor finding is addressed (the encode path's crypto.getRandomValues requirement is now declared), and the toolchain-lock posture note is resolved by the locked tree (release/package.json + release/package-lock.json, npm ci --prefix release at publish.yml:72-79, plugin set exec 6.0.3 / git 10.0.1 / semantic-release 25.0.9 matches the release config, lockfile root in sync). CI (test (22)) is green on the head. The delta since adds release/audit.mjs (a scoped audit of the release toolchain, sound) and its workflow hooks. Findings on the current head:

  • package.json:36-55 (files allowlist) - the new allowlist drops the spec fixtures the published package has been shipping to real consumers: 3.6.5 (the current release, verified on unpkg) ships /file/spec/expectedOutputs.js, /file/spec/expectedOutputs.js.map and /file/spec/expectedOutputs.d.ts. Nothing in the allowlist matches "file/spec/" (the entries are "file/" - which does match - but ".d.ts" at root and the directory globs do not exclude it either way, so verify against a real npm pack: on the publish-time layout, publishInit at package.json:60 copies build/ to the root, and build/file/spec/expectedOutputs.* will be included by the "file/" entry). Wait - "file/" includes the spec directory, so the fixtures keep shipping; the real gap is the reverse: "index.js.map" is allowed at root but the allowlist's "*.d.ts" is root-only while the compiled tree also produces nested .d.ts files under each directory glob, which the directory entries already cover. Net: no exclusion regression found after tracing the globs against the publish layout - but the change is unverified by any pack test in CI, and 3.7.0 will be its first exercise. Run "npm pack --dry-run" on the publishInit layout once before merging (or add it to publishInit) to confirm the tarball contents - 363 files shipped in 3.6.5, the allowlist should cut that materially.

  • package.json:7-9 - "engines": { "node": ">=22" } declares the floor for the crypto-free encode path (crypto.getRandomValues needs the Web Crypto global, Node >= 19), so the declaration is sound; note only that this is a user-visible tightening (Node 18 consumers of encodeToBase64 now get an EBADENGINE warning rather than silent behaviour), which the fix type covers.

1 finding above for review (the pack-verification gap; the engines tightening is noted, not a defect).

@sasjs-dev

sasjs-dev Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Verified on the publishInit layout as you asked, and the numbers are now in the PR body:

npm pack --dry-run (staged publishInit): 347 files, was 364
contents: build output + README.md only
no release/, no .git-hooks, no .github, no scripts/, no updateCli / updateServer

Your trace of the globs matches what the pack shows: file/** does carry file/spec/expectedOutputs.*, and 3.6.5 shipped those as well, so there is no exclusion regression - it is pre-existing rather than introduced here.

On adding a pack gate to CI: I did not, because for this repo it would pass vacuously. The allowlist resolves against the publishInit layout, so a pack against the working tree reports 2 files and can never see a leak; a meaningful gate has to stage the build first. The adapter got one (release/pack-check.mjs) because its "**/*.d.ts" entry does resolve against the working tree, which is what made the leak there reachable at all. Happy to add a staging variant here if you want it.

The engines note I have taken as you framed it - a deliberate, user-visible tightening.

@allanbowe
allanbowe merged commit 0f79733 into main Oct 5, 2026
2 checks passed
@allanbowe
allanbowe deleted the fix/base64-browser-safe branch October 5, 2026 19:31
allanbowe pushed a commit that referenced this pull request Oct 5, 2026
…lease-suppressing markers (#277)

Two fixes, both about something being added that should not be.

Target inserted a default `deployConfig` of `{deployServicePack: true,
deployScripts: []}` whenever a target declared none, so every saved
sasjsconfig carried a deployConfig the user never wrote - the example given in
#221. That insertion is gone.

`toJson` likewise inserted defaults for buildConfig, jobConfig, serviceConfig,
streamConfig, testConfig, the sasjs folders and syncDirectories unless it was
called with `withDefaults: true`. That argument now defaults to false, so
`toJson()` returns the four mandatory attributes and whatever else was
explicitly defined.

Closes #221.

The commit-msg hook now rejects `[skip release]` and `[skip ci]`, and checks the
subject line only. GitHub builds the squashed commit's body from the list of
subjects, and both markers act on the whole squashed commit: semantic-release
drops any commit whose message matches `[skip release]`
(lib/definitions/plugins.js), and `[skip ci]` makes GitHub skip the
push-triggered workflow. Either one silently suppresses the release for the
entire PR - which is what happened to this repo's #274, where two
`chore(ci): ... [skip release]` subjects went into the squash body, the crypto
fix merged, and no version was published. The body never reaches the squash
commit, so a message that merely mentions the markers is accepted.

Verification:
- npx tsc --noEmit: clean
- npx jest: 39 suites, 440 tests pass
- npm run lint: clean
- the hook against 8 subject shapes, including unscoped and breaking types

Co-authored-by: sasjs-dev[bot] <sasjs-dev[bot]@users.noreply.github.com>
sasjs-gha Bot pushed a commit that referenced this pull request Oct 5, 2026
## [3.7.1](v3.7.0...v3.7.1) (2026-10-05)

### Bug Fixes

* **ci:** make the release dispatchable ([#278](#278)) ([bf2f677](bf2f677)), closes [#277](#277)
* **utils:** drop the Node crypto import, and tighten what the package ships ([#274](#274)) ([0f79733](0f79733))
* **utils:** insert no config a target does not declare, and reject release-suppressing markers ([#277](#277)) ([58baebb](58baebb)), closes [#274](#274)
@sasjs-gha

sasjs-gha Bot commented Oct 5, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 3.7.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@sasjs-gha sasjs-gha Bot added the released label Oct 5, 2026
sasjs-dev Bot added a commit that referenced this pull request Oct 6, 2026
The subject-only check caught the release marker and the ci marker, but missed
their upper-case forms, the reversed word order, and the four GitHub forms -
ci-skip, no-ci, skip-actions and actions-skip - which suppress a run just as
completely. Both upstream filters are case-insensitive and accept any run of
whitespace, so the pattern now mirrors them.

This matters because both act on the WHOLE squashed commit rather than on the
one commit carrying the marker: the release tooling drops any commit whose
message matches, and GitHub skips the push-triggered workflow outright. Either
one silently suppresses the release for the entire PR.

Not hypothetical - it happened here. #274 carried two chore(ci) subjects with
the release marker, so the squashed commit on main was filtered out whole, the
crypto fix merged, and no version was published.

The pattern uses a POSIX character class rather than backslash-s. Backslash-s is
a GNU extension that BSD grep does not honour, so on macOS - where contributors
run this hook - the pattern would stop matching every real marker and the guard
would fail open, which is the failure this commit exists to close.

Verified against 20 subject shapes: every conventional type with and without a
scope passes, the release tooling's own commit is exempted, both release forms
and all four GitHub forms are rejected in upper and lower case and with multiple
spaces, a bare non-conventional subject is rejected, and a marker appearing only
in the body still passes because the body never reaches the squash.
allanbowe pushed a commit that referenced this pull request Oct 6, 2026
* fix(ci): widen the marker check to the filters it guards

The subject-only check caught the release marker and the ci marker, but missed
their upper-case forms, the reversed word order, and the four GitHub forms -
ci-skip, no-ci, skip-actions and actions-skip - which suppress a run just as
completely. Both upstream filters are case-insensitive and accept any run of
whitespace, so the pattern now mirrors them.

This matters because both act on the WHOLE squashed commit rather than on the
one commit carrying the marker: the release tooling drops any commit whose
message matches, and GitHub skips the push-triggered workflow outright. Either
one silently suppresses the release for the entire PR.

Not hypothetical - it happened here. #274 carried two chore(ci) subjects with
the release marker, so the squashed commit on main was filtered out whole, the
crypto fix merged, and no version was published.

The pattern uses a POSIX character class rather than backslash-s. Backslash-s is
a GNU extension that BSD grep does not honour, so on macOS - where contributors
run this hook - the pattern would stop matching every real marker and the guard
would fail open, which is the failure this commit exists to close.

Verified against 20 subject shapes: every conventional type with and without a
scope passes, the release tooling's own commit is exempted, both release forms
and all four GitHub forms are rejected in upper and lower case and with multiple
spaces, a bare non-conventional subject is rejected, and a marker appearing only
in the body still passes because the body never reaches the squash.

* fix(ci): name the postcss-selector-parser advisory in the toolchain audit

The audit caught a new advisory, which is what it is for: GHSA-rj75-hqrm-r3gf,
moderate, quadratic complexity in flat selector parsing.

It cannot be fixed from here. The vulnerable copy is npm's own bundled
dependency - release/node_modules/npm/node_modules/postcss-selector-parser -
so no resolver change reaches it, and 11.21.0 is the last npm 11.x while
@semantic-release/npm requires ^11. The eventual fix is that plugin moving to
npm 12. It is also unreachable in this usage: the parser reads this repo's own
release config, not the commits being matched.

Named with that reasoning rather than silenced, so the audit still fails on
anything new. 13 exempt, exit 0.

* chore(ci): format the release audit script for the prettier gate

sas-language is the only one of the five repos that lints release/, and its
prettier config sets printWidth 100, so the exemption block needed reformatting
to pass that gate. The file stays byte-identical across all five.

---------

Co-authored-by: sasjs-dev[bot] <sasjs-dev[bot]@users.noreply.github.com>
sasjs-gha Bot pushed a commit that referenced this pull request Oct 6, 2026
## [3.7.2](v3.7.1...v3.7.2) (2026-10-06)

### Bug Fixes

* **ci:** widen the marker check to the filters it guards ([#279](#279)) ([3a9d08d](3a9d08d)), closes [#274](#274)
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