feat: publish the CLI to npm as @voiceflow/cli (COR-13588) - #8
Open
Bradenream wants to merge 4 commits into
Open
feat: publish the CLI to npm as @voiceflow/cli (COR-13588)#8Bradenream wants to merge 4 commits into
Bradenream wants to merge 4 commits into
Conversation
Adds a hand-written (non-generated) npm distribution layer so 'npx @voiceflow/cli' works cold with no postinstall scripts: - npm/cli: wrapper package exposing 'vf' and 'voiceflow' bins via a zero-dependency shim that resolves the platform binary package (@voiceflow/cli-<os>-<cpu>, optionalDependencies with os/cpu fields) and hands over stdio, exit codes, and signals - npm/scripts/prepare.ts: stages all 7 packages from goreleaser's artifacts.json, stamping the release version and restoring the executable bit the Actions artifact zip round-trip drops - npm/scripts/publish.ts: idempotent publish — platform packages first, registry-visibility gate, wrapper last; prereleases go to the 'next' dist-tag so latest never resolves an rc - npm/README.md: pipeline invariants and failure recovery - test/npm-distribution.test.ts: hermetic coverage of staging, ordering, version stamping, exec bits, and the shim's exec/error paths
Appends an npm-publish job to the existing tag-triggered release workflow: downloads the goreleaser dist artifact, stages the seven npm packages, smoke-tests a staged binary, and publishes idempotently with provenance. Requires the NPM_TOKEN repo secret (human step).
There was a problem hiding this comment.
Pull request overview
Adds a hand-written npm distribution layer so the Voiceflow CLI can be executed via npx @voiceflow/cli by publishing a thin wrapper package plus six platform-specific binary packages, and wires this into the tag-triggered release workflow.
Changes:
- Introduces
npm/scripts/prepare.tsto stage the wrapper + platform binary packages fromdist/artifacts.json, stamping versions and restoring executable bits. - Introduces
npm/scripts/publish.tsto publish packages idempotently with ordering/visibility gating and prerelease dist-tag routing. - Adds a Node bin shim (
npm/cli/bin/vf.js), documentation, hermetic-ish tests, and a newnpm-publishjob in the release workflow.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/npm-distribution.test.ts | Adds tests for staging, publish planning, and the wrapper shim behavior. |
| npm/scripts/prepare.ts | Stages all npm packages from goreleaser output and stamps versions/modes. |
| npm/scripts/publish.ts | Publishes staged packages with ordering and prerelease dist-tag logic. |
| npm/README.md | Documents pipeline invariants and failure recovery for npm publishing. |
| npm/cli/README.md | End-user docs for installing/running the npm-distributed CLI. |
| npm/cli/package.json | Wrapper package definition with bins + optionalDependencies for platforms. |
| npm/cli/bin/vf.js | Zero-dependency launcher that resolves and spawns the platform binary. |
| .github/workflows/release.yaml | Adds an npm-publish job after goreleaser on tag pushes. |
Suppressed comments (2)
test/npm-distribution.test.ts:113
- This test depends on side effects from a previous
it(...)(theout-okstaging). If tests are filtered (e.g.vitest -t 'publish.ts') or the earlier test fails, this one will fail for unrelated reasons. Stage a fresh outDir within this test (or via abeforeAllin this describe) to keep it isolated.
it('plans platform packages before the wrapper and never invokes a real publish', async () => {
const outDir = path.join(workDir, 'out-ok'); // staged by the first test
const result = await execa({ reject: false })(TSX, [PUBLISH, '--version', '0.229.0', '--out', outDir, '--dry-run']);
expect(result.exitCode, result.stderr).toBe(0);
test/npm-distribution.test.ts:15
- This hard-codes the tsx binary path to
node_modules/.bin/tsx. On Windows, Yarn will typically createtsx.cmd(andtsx.ps1) instead, and spawning an explicit path without the.cmdextension can fail. Consider selecting the correct filename based on platform so the tests are runnable on Windows too.
const TSX = path.join(REPO_ROOT, 'node_modules', '.bin', 'tsx');
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- prepare now FAILS when the repo has no LICENSE instead of warning. Every
package.json declares Apache-2.0; staging without the license text made
it possible to permanently publish a bare license claim (versions cannot
be unpublished) if a tag landed before the LICENSE PR.
- Both scripts converted from .ts to dependency-free .mjs run by bare node.
The publish step holds NPM_TOKEN, and 'npx tsx' had npm resolve tsx's
transitive tree from the registry at run time with no lockfile — arbitrary
registry code executing in the token-bearing step. Both scripts only ever
used node builtins, so the runner bought nothing.
- latest now only moves forward: publish reads the registry's current latest
and tags a non-newer version 'previous' instead. The documented recovery
('re-run the job') could otherwise demote what npx installs when a newer
release had already shipped.
- Each staged package embeds the LICENSE file.
- Tests: goreleaser fixtures now use cwd-relative dist/-prefixed paths (the
branch CI actually takes, previously untested), npm is stubbed on PATH so
dist-tag decisions are hermetic instead of depending on live registry
state, plus coverage for the license gate, latest-guard, partial-publish
skip, and shim signal propagation.
npx prompts 'Ok to proceed?' when a TTY is present and the package is not already cached, which stalls unattended agent runs. This README is the one npm renders on the package page, so it is the copy most people and agents will paste.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of COR-13588 — shipping the vf CLI as an agent-era distribution channel.
What
Hand-written (non-generated) npm distribution layer so
npx @voiceflow/cliworks cold — no postinstall scripts, no compilation, ~2–4s cold start:npm/cli/— wrapper package exposingvfandvoiceflowbins via a zero-dependency shim that resolves the platform binary package (@voiceflow/cli-<os>-<cpu>, optionalDependencies with os/cpu fields — npm downloads only the matching one, ~5.6MB) and hands over stdio, exit codes, and signalsnpm/scripts/prepare.ts— stages all 7 packages from goreleaser'sartifacts.json, stamping the release version and restoring the executable bit the Actions artifact zip round-trip dropsnpm/scripts/publish.ts— idempotent publish: platform packages first, registry-visibility gate, wrapper last; prereleases go to thenextdist-tag solatestnever resolves an rcnpm/README.md— pipeline invariants + failure recoveryStatic CGO-free binaries → one linux package per arch, no musl split. Base branch: #7 (needed to build locally).
Human steps before first publish
NPM_TOKEN(scope@voiceflow, read+write, may-create-packages) → repo Actions secretnext), verify on macOS/Linux/Windows@voiceflow/clilive; then configure npm trusted publishing and retire the tokenVerification
test/npm-distribution.test.ts): staging layout, version stamping, exec bits, npx bin invariant, publish ordering + dist-tags, shim exec/error pathsnpm pack→ install into a scratch prefix → both bins execute the CLI in 27ms, exit codes propagate, unsupported-platform error teachesrelease.yamlYAML-validated; existing goreleaser job byte-identical