Skip to content

feat: publish @pact-foundation/pact-cli to npm - #171

Merged
JP-Ellis merged 2 commits into
mainfrom
feat/npm-publish
Oct 21, 2026
Merged

JP-Ellis merged 2 commits into
mainfrom
feat/npm-publish

Conversation

@JP-Ellis

@JP-Ellis JP-Ellis commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Goal

Publish @pact-foundation/pact-cli to npm from this repo on every stable release, the same way publish-pypi.yml publishes pact-python-cli. Today pact-foundation/pact-js-cli repackages these binaries in a separate release cycle, so npm trails: 18.1.2 ships crate 0.10.8 while this repo is at 0.10.9. pact-js-cli will be archived separately once a release from here is on npm.

What ships

  • npm/pact-cli/: plain CommonJS wrappers for the seven existing bins (pact, pact-broker, pact-mock-server, pact-stub-server, pact-plugin, pact-verifier, pactflow).
    • No dependencies, no build step, Node ≥ 16.
    • The binary comes from an optional dependency on @pact-foundation/pact-cli-<os>-<arch>, the same layout and package names as today.
    • The wrapper spawns the binary without a shell, forwards SIGINT/SIGTERM/SIGHUP to it, and exits the way the binary exited.
  • .github/scripts/build-npm.js: a dependency-free Node script. It extracts with tar and unzip, since Node has no xz or zip support. It copies the cargo-dist binaries byte for byte into six platform packages. Linux uses the static musl binary. Windows arm64 carries the x64 binary under emulation; the published pact-cli-windows-arm64@18.1.2 has no binary at all.
  • .github/workflows/publish-npm.yml: a cargo-dist post-announce job (wired through Cargo.toml; release.yml regenerated with dist generate) with four stages:
    1. build: downloads and checksums the archives, then builds and packs the packages;
    2. smoke: a six-runner matrix, including windows-11-arm and macos-15-intel, that npm installs the main tarball with the host's platform tarball and runs every bin;
    3. publish: npm trusted publishing with provenance; platform packages first, main package last, already-published versions skipped;
    4. verify: installs the version from the registry and runs pact --help.
  • CI: a new npm job in test.yml runs the wrapper tests on Linux, macOS and Windows, and checks that the wrapper loads on Node 16.

Decisions

  • Versioning: crate X.Y.Z maps to npm (18+X).Y.Z, so v0.10.9 becomes 18.10.9. npm has no epoch and rejects four-part versions. A prerelease or build-metadata suffix would sort wrongly or drop out of ^18. The offset keeps ^18 users on the line above 18.1.2, and git-cliff makes crate minor bumps additive.

  • The latest guard: npm moves latest to the most recent publish, not the highest version. The publish job reads the current latest and publishes an older version (a retry or backfill) under backfill. Any registry error other than E404 fails the job, so an outage can't pass for "not published".

  • Why no postinstall download: cargo-dist's own npm installer downloads the binary at install time. That approach breaks under --ignore-scripts and in offline CI, so the binaries ship in the packages.

  • Signal forwarding: the first version used spawnSync, the same as pact-js-cli. Review showed that a SIGTERM sent to the wrapper, as test harnesses, concurrently and timeout all send, left pact mock running and holding its port. The wrapper now uses async spawn and forwards signals.

  • Hyphenated commands are deprecated. pact <command> is the supported form, matching the PyPI wheel, which installs only pact. The six hyphenated commands keep working, but each prints warning: pact-broker is deprecated; use 'pact broker' instead (or its equivalent) on stderr. Removing them breaks ^18, so they can go no earlier than crate 1.0.0 (npm 19.0.0).

  • Why a JS shim at all: a wheel is selected per platform and drops pact straight onto PATH. npm commands come from the bin of the installed package, which can't point into an optional dependency. Bins on the platform packages only work through npm's hoisting, not under pnpm, Yarn 2+ or npx. So bin/pact.js is the minimum.

Before the first release

  • On npmjs.com, set up trusted publishing on all seven packages for release.yml and publish-npm.yml, with environment npm.
  • Create the npm environment in this repo.

Testing

  • The wrapper tests pass locally, and the wrapper loads on Node 16.
  • I ran the build script against the real v0.10.9 archives. Its output matches, file for file and mode for mode, that of the earlier Python version. The binary is byte-identical, and it stays executable after npm install. All seven bins run from the installed package.
  • actionlint, shellcheck, ruff, prek and dist plan are clean.
  • The publish workflow itself first runs on the next stable release, or on a manual run with a tag.

`npm/pact-cli` is the `@pact-foundation/pact-cli` npm package: plain
CommonJS with no dependencies and no build step, for Node 16 and later.
It keeps the seven bin commands that pact-js-cli users already have.

The binary comes from an optional dependency on
`@pact-foundation/pact-cli-<os>-<arch>`, the platform package names
already on npm. The wrapper spawns it without a shell, forwards SIGINT,
SIGTERM and SIGHUP to it, and exits the way the binary exited. A missing
platform package names the package it expected, with no stack trace.

npm cannot point a bin at a file in another package. Platform-package
bins only appear through npm's hoisting, not under pnpm, Yarn 2+ or
`npx`, so `bin/pact.js` is the minimum shim.

`pact <command>` is the supported form, matching the PyPI wheel, which
installs only `pact`. The hyphenated commands (`pact-broker`,
`pact-mock-server`, `pact-stub-server`, `pact-plugin`, `pact-verifier`,
`pactflow`) still run the matching subcommand, after a one-line
deprecation notice on stderr. Removing them breaks `^18`, so they can go
no earlier than crate 1.0.0.

The tests cover argument and exit-status passthrough, signal forwarding
in both directions and the missing-package error, all from a path with a
space. The `npm` job in test.yml runs them on Linux and Windows, and
checks that the wrapper loads on Node 16.

Assisted-by: Claude Code:claude-opus-5-5
Each stable release now publishes the `pact` binary to npm as
`@pact-foundation/pact-cli` and six platform packages. The package was
built in pact-foundation/pact-js-cli, which trailed every release here by
a separate update-and-release cycle. Publishing moves here so npm ships
with each release.

npm has no epoch and accepts only three-part versions, so crate `X.Y.Z`
maps to npm `(18+X).Y.Z`: `v0.10.9` becomes `18.10.9`. That continues the
pact-js-cli 18.x line above its last release, 18.1.2, so users on `^18`
receive new releases without changing their range. git-cliff makes a
crate minor bump additive, which suits a `^18` range.

The new `publish-npm` post-announce job downloads the cargo-dist archives,
verifies their checksums and repackages each binary with
`.github/scripts/build-npm.js`, a dependency-free Node script. Node has no
xz or zip support, so the script extracts with `tar` and `unzip`. The
binaries are copied byte for byte; nothing is recompiled. Linux packages
carry the static musl binary. The Windows arm64 package carries the
x86_64 binary, which runs under Windows' x64 emulation, until the native
target is re-enabled; the published 18.1.2 package for that platform has
no binary.

A smoke matrix on six runners, including windows-11-arm, installs the main
tarball with the host's platform tarball and runs every bin. The publish
job then uploads through npm trusted publishing with provenance, platform
packages first and the main package last, and skips versions that already
exist. npm moves `latest` to whatever is published last, so the job reads
the current `latest` and publishes an older version under `backfill`. A
registry error other than E404 fails the job, so an outage cannot pass for
"not published". A final job installs the version from the registry and
runs `pact --help`.

Prereleases are skipped. `workflow_dispatch` takes a tag, for retries and
backfills of tags that have cargo-dist archives.

Maintainer setup: configure npm trusted publishing on all seven packages
for release.yml and publish-npm.yml with environment `npm`, and create
that environment in this repo.

Assisted-by: Claude Code:claude-opus-5-5
@JP-Ellis
JP-Ellis enabled auto-merge (rebase) October 21, 2026 04:47
@JP-Ellis
JP-Ellis merged commit 0b330fd into main Oct 21, 2026
22 checks passed
@JP-Ellis
JP-Ellis deleted the feat/npm-publish branch October 21, 2026 04:48
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.

1 participant