Skip to content

Drop Node.js 18 support (now Node 20+ only), modernize the dev toolchain, and increase test coverage to 100% - #1032

Open
yinzara wants to merge 2 commits into
commitizen:masterfrom
yinzara:build/node-20-toolchain
Open

Drop Node.js 18 support (now Node 20+ only), modernize the dev toolchain, and increase test coverage to 100%#1032
yinzara wants to merge 2 commits into
commitizen:masterfrom
yinzara:build/node-20-toolchain

Conversation

@yinzara

@yinzara yinzara commented Sep 8, 2026

Copy link
Copy Markdown

Raise the supported Node.js baseline to >= 20 and bring the test, git-hook, coverage and dependency toolchain up to currently-maintained versions. Several dependency upgrades (notably glob 13 and pnpm 10+) surfaced latent runtime bugs, which are fixed here as well.

src/cli was excluded from coverage entirely and several modules elsewhere were only exercised incidentally, leaving overall coverage around 75%. This PR adds focused unit tests (proxyquire + sinon) for the previously untested code and tightens the gate to 100%.

Fixes #1020 and #1006

Runtime fixes

  • config discovery (src/configLoader/findup.js): switch to the named import { globSync } from 'glob'. glob v9+ removed the default export, so with glob 13 glob resolved to undefined and every config lookup threw TypeError: Cannot read properties of undefined (reading 'sync').

  • commitizen init --pnpm (src/commitizen/init.js, src/commitizen/adapter.js):

    • forward the exact option into getInstallStringMappings() and key the pnpm --save-exact flag off exact instead of saveExact (which is never set on the pnpm path). commitizen init --pnpm --save-exact now pins an exact version instead of silently writing a caret range.
    • pass --ignore-scripts to the pnpm install. pnpm >= 10 turns "ignored build scripts" (ERR_PNPM_IGNORED_BUILDS) into a fatal error, which aborted the install before config.commitizen.path was written. Commitizen adapters never need their dependencies' lifecycle scripts.

Dependency upgrades

  • mocha 9.2.2 -> 12.0.0
  • nyc 15.1.0 -> 18.0.0
  • babel-plugin-istanbul 6.1.1 -> 7.0.1
  • nodemon 2.0.22 -> 3.1.14
  • sinon 12.0.1 -> 22.1.0
  • uuid 8.3.2 -> 11.1.1
  • conventional-changelog-conventionalcommits 4.6.3 -> 7.0.2
  • glob 7.2.3 -> 13.0.6 (runtime dependency)
  • fs-extra 9.1.0 -> 11.4.0 (runtime dependency)
  • find-node-modules ^2.1.2 -> ^2.1.3 (runtime dependency)

Dependency removals and replacements

  • Move cz-conventional-changelog from dependencies to devDependencies. Nothing in the runtime code (src/, bin/) requires it -- it only appears as example text in commitizen init help output. Its real uses are this repo's own config.commitizen.path (dogfooding commits) and the test suite. As a runtime dependency it needlessly pulled itself and its @commitlint/* subtree into every consumer of commitizen.
  • ghooks -> husky 9. Drop the config.ghooks block, add .husky/pre-commit (npm run test && npm run check-coverage), and run husky from the prepare script. Replace the in-publish / not-in-install build-on- publish hack with a standard prepublishOnly script; in-publish removed.
  • Remove codecov. The npm codecov uploader is end-of-life and the report-coverage script was unused: Azure Pipelines already publishes coverage from npm run write-coverage (cobertura). Stale codecov.io and travis.ci badges removed from the README.
  • Remove mocha-multi-reporters and mocha-junit-reporter. Both are unmaintained and throw "Class constructor Base cannot be invoked without 'new'" on mocha >= 11. Replaced with a small in-repo composite reporter, test/reporters/multi-reporter.cjs, that drives mocha's built-in spec and xunit reporters from a single run and fans done() out to flush the JUnit XML file. test/mochareporters.json deleted.

Test harness

  • test-actual now runs mocha --require @babel/register ... directly, so it works when invoked on its own and not only through the nyc wrapper. The test files use extensionless ESM imports that only resolve once Babel rewrites them to require.
  • JUnit output is written to ./junit-testresults.xml via --reporter-option output=; Azure's PublishTestResults glob (**/junit-*.xml) still matches. Added to .gitignore.
  • babel.config.js: @babel/preset-env target bumped from node 10 to node 20.

CI

jobs/build.yml:

  • Node matrix is now 20, 22, 24 and 26 (Node 18 removed); maxParallel 3 -> 4.
  • Node install task NodeTool@0 -> UseNode@1.
  • pnpm bumped 9 -> 12.
  • PublishTestResults@2 now points at the exact junit-testresults.xml under
    $(System.DefaultWorkingDirectory) instead of the recursive **/junit-*.xml
    glob, so it never walks the artifacts tree (which is what triggered the
    Windows EPERM: operation not permitted, stat failure on pnpm's .pnpm
    junctions).
  • PublishCodeCoverageResults@1 -> @2; drop the removed codeCoverageTool
    input and point summaryFileLocation at the single
    $(System.DefaultWorkingDirectory)/coverage/cobertura-coverage.xml rather
    than a recursive glob.

New / expanded tests

  • test/tests/cli.js: exercises the whole CLI entry layer.
    • cli/commitizen.js bootstrap: the init sub-command (calls init() with cwd/name/parsed args, logs instead of throwing on failure, errors when no adapter name is given), the usage-help path for every other invocation, argv slicing and the environment/argv parameter defaults.
    • cli/strategies/git.js: spawns git commit with the raw args and inherited stdio, rethrows a child-process error, and only logs (never spawns) in debug mode.
    • cli/strategies/git-cz.js: --amend delegates to the plain git strategy, the normal path wires inquirer/prompter/options into commit(), --retry and --hook set the right flags, -a/--all stages all files, a clean staging area throws unless --allow-empty is present, and errors from the staging check and the commit callback are rethrown; a successful commit exits 0.
  • test/tests/git.js (new): git/log and git/whatChanged (success + throw on git error); git/commit non-hook mode (spawn args, stdio, error event, exit code 0 / 128-with-config-warning / other non-zero, single-callback guard) and --hook mode (w write, the Windows r+ fallback, write failure, and fallback failure).
  • test/tests/commitizen-commit.js (new): cache-dir creation failure is logged and aborts, the normal prompter -> commit -> cache flow, an Error from the adapter prompter is propagated without committing, and retryLastCommit replays the cached attempt.
  • test/tests/cache.js (new): create / merge / overwrite / read / missing-key / missing-file behaviour of the commitizen cache helpers.
  • test/tests/adapter-unit.js (new): getInstallStringMappings for npm/yarn/pnpm and the unknown-manager fallback (including the exact vs saveExact distinction), generateInstallAdapterCommand, and addPathToAdapterConfig indentation detection / two-space fallback / the "already configured" guard branch.
  • test/tests/init-unit.js (new): required-argument errors, an install failure is logged rather than thrown, and includeCommitizen also runs the commitizen install.
  • test/tests/util.js: adds getParsedJsonFromFile / getParsedPackageJsonFromPath (parse success, missing file, invalid JSON) and isInTest.
  • test/tests/configLoader.js: adds loader (explicit path, discovery, nothing-found, default cwd), findup (walks up parent directories, returns undefined at the root, honours the filter callback), and the empty/missing config-path branches of getContent / getNormalizedConfig.
  • test/tests/index.js: registers the new suites.

Source / config

  • package.json: drop src/cli from nyc.exclude so the CLI layer is measured, and raise check-coverage thresholds from 80 to 100 for statements, branches, functions and lines.
  • src/cli/parsers.js, src/cli/strategies.js: mark as /* istanbul ignore file */ -- they are pure re-export barrels, matching src/git.js, src/commitizen.js and src/configLoader.js.
  • src/configLoader/getNormalizedConfig.js: /* istanbul ignore if */ on the czConfig deprecation warning, which is deliberately suppressed while the test globals are present and so cannot be reached from the suite.

Result: coverage goes from ~75% to 100% across statements, branches, functions and lines; npm test and npm run check-coverage both pass.

Housekeeping

  • .gitignore: add .idea, .omc, junit-testresults.xml.
  • package-lock.json regenerated.

BREAKING CHANGE: Node.js 18 is no longer supported. The minimum supported runtime is now Node.js 20 (engines.node is >= 20), and npm 9 or newer is recommended. Installing or running commitizen on Node.js 18 or earlier will fail the engine check. This is required because upgraded tooling and transitive dependencies (mocha 12, nyc 18, glob 13's minimatch/lru-cache, and the pnpm 10+ CLI used in CI) have themselves dropped Node.js 18.

@yinzara
yinzara force-pushed the build/node-20-toolchain branch from 0c04d7e to 7d7cdde Compare September 8, 2026 16:02
Raise the supported Node.js baseline to >= 20 and bring the test, git-hook,
coverage and dependency toolchain up to currently-maintained versions.
Several dependency upgrades (notably glob 13 and pnpm 10+) surfaced latent
runtime bugs, which are fixed here as well.

Runtime fixes
-------------
* config discovery (src/configLoader/findup.js): switch to the named
  `import { globSync } from 'glob'`. glob v9+ removed the default export, so
  with glob 13 `glob` resolved to `undefined` and every config lookup threw
  `TypeError: Cannot read properties of undefined (reading 'sync')`.

* `commitizen init --pnpm` (src/commitizen/init.js, src/commitizen/adapter.js):
  - forward the `exact` option into `getInstallStringMappings()` and key the
    pnpm `--save-exact` flag off `exact` instead of `saveExact` (which is never
    set on the pnpm path). `commitizen init --pnpm --save-exact` now pins an
    exact version instead of silently writing a caret range.
  - pass `--ignore-scripts` to the pnpm install. pnpm >= 10 turns
    "ignored build scripts" (ERR_PNPM_IGNORED_BUILDS) into a fatal error, which
    aborted the install before `config.commitizen.path` was written. Commitizen
    adapters never need their dependencies' lifecycle scripts.

Dependency upgrades
-------------------
* mocha              9.2.2  -> 12.0.0
* nyc                15.1.0 -> 18.0.0
* babel-plugin-istanbul 6.1.1 -> 7.0.1
* nodemon            2.0.22 -> 3.1.14
* sinon              12.0.1 -> 22.1.0
* uuid               8.3.2  -> 11.1.1
* conventional-changelog-conventionalcommits 4.6.3 -> 7.0.2
* glob               7.2.3  -> 13.0.6   (runtime dependency)
* fs-extra           9.1.0  -> 11.4.0   (runtime dependency)
* find-node-modules  ^2.1.2 -> ^2.1.3   (runtime dependency)

Dependency removals and replacements
------------------------------------
* Move `cz-conventional-changelog` from `dependencies` to `devDependencies`.
  Nothing in the runtime code (src/, bin/) requires it -- it only appears as
  example text in `commitizen init` help output. Its real uses are this repo's
  own `config.commitizen.path` (dogfooding commits) and the test suite. As a
  runtime dependency it needlessly pulled itself and its `@commitlint/*`
  subtree into every consumer of `commitizen`.
* ghooks -> husky 9. Drop the `config.ghooks` block, add `.husky/pre-commit`
  (`npm run test && npm run check-coverage`), and run `husky` from the
  `prepare` script. Replace the `in-publish` / `not-in-install` build-on-
  publish hack with a standard `prepublishOnly` script; `in-publish` removed.
* Remove `codecov`. The npm `codecov` uploader is end-of-life and the
  `report-coverage` script was unused: Azure Pipelines already publishes
  coverage from `npm run write-coverage` (cobertura). Stale `codecov.io` and
  `travis.ci` badges removed from the README.
* Remove `mocha-multi-reporters` and `mocha-junit-reporter`. Both are
  unmaintained and throw "Class constructor Base cannot be invoked without
  'new'" on mocha >= 11. Replaced with a small in-repo composite reporter,
  test/reporters/multi-reporter.cjs, that drives mocha's built-in `spec` and
  `xunit` reporters from a single run and fans `done()` out to flush the
  JUnit XML file. `test/mochareporters.json` deleted.

Test harness
------------
* `test-actual` now runs `mocha --require @babel/register ...` directly, so it
  works when invoked on its own and not only through the `nyc` wrapper. The
  test files use extensionless ESM imports that only resolve once Babel
  rewrites them to `require`.
* JUnit output is written to `./junit-testresults.xml` via
  `--reporter-option output=`; added to `.gitignore`.
* test/tools/clean.js: the per-test artifact archive no longer copies
  `node_modules`. It is large and slow, and on Windows pnpm's nested junctions
  under `.pnpm` make a later recursive walk of `test/artifacts` (Azure's
  PublishTestResults glob) fail with `EPERM: operation not permitted, stat`.
* babel.config.js: `@babel/preset-env` target bumped from node 10 to node 20.

CI
--
jobs/build.yml:
* Node matrix is now 20, 22, 24 and 26 (Node 18 removed); `maxParallel` 3 -> 4.
* Node install task `NodeTool@0` -> `UseNode@1`.
* pnpm bumped 9 -> 12.
* PublishTestResults@2 now points at the exact `junit-testresults.xml` under
  `$(System.DefaultWorkingDirectory)` instead of the recursive `**/junit-*.xml`
  glob, so it never walks the artifacts tree (which is what triggered the
  Windows `EPERM: operation not permitted, stat` failure on pnpm's `.pnpm`
  junctions).
* PublishCodeCoverageResults@1 -> @2; drop the removed `codeCoverageTool`
  input and point `summaryFileLocation` at the single
  `$(System.DefaultWorkingDirectory)/coverage/cobertura-coverage.xml` rather
  than a recursive glob.

Housekeeping
------------
* .gitignore: add `.idea`, `.omc`, `junit-testresults.xml`.
* package-lock.json regenerated.

BREAKING CHANGE: Node.js 18 is no longer supported. The minimum supported
runtime is now Node.js 20 (`engines.node` is `>= 20`), and npm 9 or newer is
recommended. Installing or running commitizen on Node.js 18 or earlier will
fail the engine check. This is required because upgraded tooling and
transitive dependencies (mocha 12, nyc 18, glob 13's `minimatch`/`lru-cache`,
and the pnpm 10+ CLI used in CI) have themselves dropped Node.js 18.

Fixes commitizen#1020, fixes commitizen#1006

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@yinzara
yinzara force-pushed the build/node-20-toolchain branch from 7d7cdde to 39dc10d Compare September 8, 2026 16:23
@yinzara yinzara changed the title build!: drop Node.js 18 support and modernize the dev toolchain Drop Node.js 18 support (now Node 20+ only), modernize the dev toolchain, and increase test coverage to 100% Sep 8, 2026
@yinzara
yinzara force-pushed the build/node-20-toolchain branch from b2f34b5 to af84ab8 Compare September 8, 2026 17:23
`src/cli` was excluded from coverage entirely and several modules elsewhere
were only exercised incidentally, leaving overall coverage around 75%. This
adds focused unit tests (proxyquire + sinon) for the previously untested
code and tightens the gate to 100%.

New / expanded tests
--------------------
* test/tests/cli.js: exercises the whole CLI entry layer.
  - cli/commitizen.js `bootstrap`: the `init` sub-command (calls `init()`
    with cwd/name/parsed args, logs instead of throwing on failure, errors
    when no adapter name is given), the usage-help path for every other
    invocation, argv slicing and the `environment`/`argv` parameter defaults.
  - cli/strategies/git.js: spawns `git commit` with the raw args and
    inherited stdio, rethrows a child-process `error`, and only logs (never
    spawns) in debug mode.
  - cli/strategies/git-cz.js: `--amend` delegates to the plain git strategy,
    the normal path wires inquirer/prompter/options into `commit()`,
    `--retry` and `--hook` set the right flags, `-a`/`--all` stages all
    files, a clean staging area throws unless `--allow-empty` is present,
    and errors from the staging check and the commit callback are rethrown;
    a successful commit exits 0.
* test/tests/git.js (new): git/log and git/whatChanged (success + throw on
  git error); git/commit non-hook mode (spawn args, stdio, `error` event,
  exit code 0 / 128-with-config-warning / other non-zero, single-callback
  guard) and `--hook` mode (`w` write, the Windows `r+` fallback, write
  failure, and fallback failure).
* test/tests/commitizen-commit.js (new): cache-dir creation failure is
  logged and aborts, the normal prompter -> commit -> cache flow, an Error
  from the adapter prompter is propagated without committing, and
  `retryLastCommit` replays the cached attempt.
* test/tests/cache.js (new): create / merge / overwrite / read / missing-key
  / missing-file behaviour of the commitizen cache helpers.
* test/tests/adapter-unit.js (new): `getInstallStringMappings` for
  npm/yarn/pnpm and the unknown-manager fallback (including the `exact` vs
  `saveExact` distinction), `generateInstallAdapterCommand`, and
  `addPathToAdapterConfig` indentation detection / two-space fallback / the
  "already configured" guard branch.
* test/tests/init-unit.js (new): required-argument errors, an install
  failure is logged rather than thrown, and `includeCommitizen` also runs
  the commitizen install.
* test/tests/util.js: adds `getParsedJsonFromFile` /
  `getParsedPackageJsonFromPath` (parse success, missing file, invalid
  JSON) and `isInTest`.
* test/tests/configLoader.js: adds `loader` (explicit path, discovery,
  nothing-found, default cwd), `findup` (walks up parent directories,
  returns undefined at the root, honours the filter callback), and the
  empty/missing config-path branches of `getContent` / `getNormalizedConfig`.
* test/tests/index.js: registers the new suites.

Source / config
---------------
* package.json: drop `src/cli` from `nyc.exclude` so the CLI layer is
  measured, and raise `check-coverage` thresholds from 80 to 100 for
  statements, branches, functions and lines.
* src/cli/parsers.js, src/cli/strategies.js: mark as
  `/* istanbul ignore file */` -- they are pure re-export barrels, matching
  src/git.js, src/commitizen.js and src/configLoader.js.
* src/configLoader/getNormalizedConfig.js: `/* istanbul ignore if */` on the
  czConfig deprecation warning, which is deliberately suppressed while the
  test globals are present and so cannot be reached from the suite.

Result: coverage goes from ~75% to 100% across statements, branches,
functions and lines; `npm test` and `npm run check-coverage` both pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

Dependency security scan results + actionable upgrade path (OWASP project)

1 participant