Skip to content

feat(cli)!: envelope the version and publish commands, unwrap on the pipe - #616

Merged
goosewobbler merged 25 commits into
mainfrom
feat/544-envelope-pipe-commands
Oct 23, 2026
Merged

goosewobbler merged 25 commits into
mainfrom
feat/544-envelope-pipe-commands

Conversation

@goosewobbler

@goosewobbler goosewobbler commented Aug 1, 2026 •

Copy link
Copy Markdown
Owner

Description

version --json and publish --json now print the same JSON envelope as every other command, and both accept --output <path>. notes and publish unwrap an envelope on input, so the stages still chain. This finishes the envelope work in #544: notes has no JSON output, so every --json command now prints an envelope.

--output resolves against the directory releasekit runs in, not --project-dir, the same as release --output.

Chaining the stages

  • Unwrapping. unwrapEnvelope (core) runs inside notes' parseVersionOutput and publish's parseInput, so string, file and stdin input are all covered.

  • Bare input still works. A plain VersionOutput, such as a hand-written file or output from an older releasekit, passes through unchanged.

  • Failed upstream stages. Feeding a failed stage's envelope forward now fails as an input error (exit 3) that names that stage's code and message, instead of a schema error about missing fields:

    code: INPUT_PARSE_ERROR
    message: Failed to parse version output: Upstream releasekit stage failed (GENERAL_ERROR): …
    
  • Chain through a file. notes reads the VersionOutput but doesn't print it, so it can't sit in a pipe. The three-stage stdin pipe the docs showed never worked; version --json | publish does. publish commits only the files version changed plus whatever is staged, so stage the changelog first:

    releasekit version --output version.json
    releasekit notes --input version.json
    git add CHANGELOG.md
    releasekit publish --input version.json

Other changes

  • errorEnvelope takes optional data and changed.
    • Without them, publish's failure envelope would drop partialOutput (what was published before the failure), and would report changed: false after a partial publish.
    • The envelope has no field for the failed stage, so the error message names it.
    • schemaVersion stays 1, because readers that ignore data on an error are unaffected.
  • publishDidChange moved to @releasekit/publish, so the publish CLI and standing-pr publish decide changed the same way.
    • Only a registry publish or a release commit made by this run counts. Tags, pushes and GitHub releases that already existed all report success without changing anything.
    • standing-pr publish never commits, so for it only registry publishes count.
  • One envelope writer. writeEnvelope and writeErrorEnvelope in core's cli.ts are shared by version, publish and release.

Breaking changes

  • --json output shape. Scripts that read version --json or publish --json must now read .data, e.g. jq '.data.updates[]'. An older standalone @releasekit/notes or @releasekit/publish rejects the envelope, so keep all the packages on one version. Worth a callout in the release notes.
  • Exit codes follow the documented table (via exitCodeForError).
    • Before: version exited 1 for every failure, and publish exited 9 for any publish-package error and 1 for anything else.
    • Now: a publish config error exits 2, bad or failed-upstream input exits 3, and pipeline failures still exit 9.
  • release and standing-pr exit codes. These already report a stage's own error code (fix(core): recognise ReleaseKitError across bundled copies of core #651). With the expanded table their exit codes follow its family too; for example, a publish-stage PIPELINE_STAGE_ERROR now exits 9. docs/cli.md says so.

The repo's workflows and the Action only check for a non-zero exit. On 0.x a ! bumps the minor version, so this ships as 0.42.0.

Not in this PR

Related Issue(s)

Part of #544.

Type of Change

  • feat — New feature
  • fix — Bug fix
  • docs — Documentation change
  • refactor — Code refactoring
  • test — Adding or updating tests
  • chore — Maintenance, dependencies, tooling
  • Breaking change — Requires major version bump

Scope

  • @releasekit/version
  • @releasekit/notes
  • @releasekit/publish
  • @releasekit/release
  • @releasekit/config
  • @releasekit/core
  • CI / workflows
  • Root / monorepo

Checklist

  • Lint passes (pnpm lint)
  • Tests pass (pnpm test)
  • Typecheck passes (pnpm typecheck)
  • Documentation updated (if applicable)
  • Breaking changes noted in description

Testing Notes

  • Built CLIs, run by hand:
    • publish reading an enveloped file, a bare file, a failed upstream envelope, and a schemaVersion: 2 envelope.
    • notes reading an enveloped payload and a failed one.
    • version --json | publish, and an unwritable --output.
    • The documented file-based chain, end to end. The release commit holds the bumped manifest and the staged changelog. Re-running on the same tree stops at the git-commit stage, as the docs now say.
  • Unit tests drive the real commands:
    • A version --json failure exits by its code's family; for example, --stable --prerelease gives an INPUT_ERROR envelope.
    • publish --output on a pipeline failure writes the partial output to the file.
    • publish --input holding a failed upstream envelope exits 3.
    • changed is checked for publish.
    • version --output works on its own and with --project-dir.
    • Each package checks that every one of its error codes maps to an exit-code family.
  • Suites: typecheck and lint are clean. The core, publish, version, notes and release unit suites pass, and so do version's integration suite (in CI since test(version): run the integration suite in CI and fix its stale sync-tags fixture #685) and the root integration suite.
  • Merge with main: it merges cleanly with main after fix(version)!: sync every committed Cargo.lock that records a bumped crate #674. The merged exit-code table keeps fix(version)!: sync every committed Cargo.lock that records a bumped crate #674's CARGO_LOCK_SYNC_ERROR in the version-error family.

🤖 Generated with Claude Code

Completes the envelope scope of #544. `version --json` and `publish --json` were
the two commands still printing a bare payload; both now emit the uniform envelope
and accept `--output <path>` for parity with the orchestration commands. `notes`
has no JSON output, so the surface is now uniform.

The coupling this was deferred for is the pipe: `version | notes | publish` carries
JSON between processes, so enveloping the producer means teaching the consumers to
unwrap on input. Both do, via `unwrapEnvelope` in core — called once inside
`parseVersionOutput` and `parseInput` so the string, file, and stdin entries are all
covered. A bare `VersionOutput` still passes through, so a hand-assembled file or an
older releasekit keeps working.

Piping a failed stage forward now fails with that stage's message rather than a
schema complaint about missing fields, which pointed at the wrong thing.

Two things fell out of doing this honestly:

`errorEnvelope` takes optional `data` and `changed`. Enveloping publish's failure
path would otherwise have dropped `partialOutput` — what already landed before the
failure — leaving the enveloped output less informative than the prose it replaced,
and reporting `changed: false` for a partial publish that did change state. Publishes
are idempotent and retried, so that is exactly what the retry reads. `failedStage`
has no envelope field, so it is named in the error message rather than dropped.
Readers that ignore `data` when `status` is `error` are unaffected, so schemaVersion
is unchanged.

The publish-changed predicate moved to `@releasekit/publish` as `publishDidChange`.
The publish CLI and `standing-pr publish` must answer `changed` the same way, and
the logic is subtle enough (tags, `git.pushed`, and already-existing GitHub releases
all report success without anything having published) that two copies would drift.
It is also now tolerant of absent registry arrays: it runs on the partial output of a
failed pipeline, where throwing would replace the publish error the caller needs.

Note `version` now exits with the mapped code from the documented table rather than
always 1 — in practice only `GIT_ERROR`, which becomes 7.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FADRmu9uhtCk3StMoqwV27
@github-actions

github-actions Bot commented Aug 1, 2026 •

Copy link
Copy Markdown
Contributor

Standing release PR: #614 · v0.42.0 queued (4 packages) · open 82d 19h 53m · ✅ ready to merge

Release Preview — v0.41.2 → v0.42.0

Note: Labels on this PR are advisory in standing-pr mode. Bumps come from conventional commits in the standing PR; override by editing labels on the standing PR itself. Add release:immediate to bypass the standing PR and release this PR directly.

These changes will be added to the release PR (#614) when merged:

Changelog

monorepo 0.41.2 → 0.42.0

Added

  • envelope the version and publish commands, unwrap on the pipe (cli)

Changed

  • link to the chaining section from architecture, simplify envelope comments
  • tighten the envelope docs and comments
  • trim the envelope doc comments; drop a stale e2e helper note (core)
  • write --output through the real JSON writer (version)
  • add braces to every single-statement body
  • point --json stability at schemaVersion; keep the version README to this PR's flags
  • cover the version and publish failure paths through the commands (cli)
  • share one error-envelope writer and trim review leftovers (cli)
  • release and standing-pr now report a stage's own error code (cli)
  • drop the duplicate publish exit test and the notes exit clause
  • drop low-value review additions
  • describe notes' parse check precisely; name a lost --output result (cli)
  • scope the exit-code contract to the commands it holds for (cli)

Fixed

  • commit a hybrid package's Cargo.toml in a direct release (release) (PR #691 · closes #678)
  • suppress per line instead of exempting cli.ts; test --output alone (cli)
  • don't crash the error path on an unwritable --output; scope the notes exit claim (cli)
  • correct the chaining, retry and exit-code docs; move the --output test into CI (cli)
  • report publish commits as changes and exit publish by code family (cli)
  • complete the exit-code table and validate envelopes on unwrap (cli)

All packages → 0.42.0 (sync versioning)

  • @releasekit/version
  • @releasekit/notes
  • @releasekit/publish
  • @releasekit/release

After merge — predicted release

No version escalation — this PR's changes will be included in the queued release without affecting the projected versions.

Package Standing PR This PR After merge
monorepo 0.42.0 0.42.0 0.42.0

Updated automatically by ReleaseKit

Comment thread packages/version/src/command.ts
Comment thread packages/core/src/envelope.ts Outdated
Both from Greptile review of #616; both were real.

**Exit classification.** The code → exit-code map only held the nine family names, but
packages throw specific codes — NOT_GIT_REPO, TAG_ALREADY_EXISTS, PACKAGES_NOT_FOUND,
NPM_AUTH_ERROR and the rest — so almost every real failure fell through to exit 1 while
docs/cli.md advertised a mapping. Automation could not tell a git failure from a config
one. Every code the version, notes, and publish packages throw is now mapped to its
family, with a test that fails when a new code isn't, since the degradation is silent.

The family assignment is a judgement call in two places worth a second opinion:
workspace/package-discovery failures (PACKAGES_NOT_FOUND, WORKSPACE_ERROR) are treated
as config errors on the reasoning that the `packages` globs matched nothing, and
manifest-parse failures (CARGO_TOML_ERROR, PUBSPEC_YAML_ERROR) likewise.

**Envelope validation.** `unwrapEnvelope` accepted anything with the three keys, so an
envelope from a newer producer, an unrecognised status, or a success envelope with a null
payload all flowed through — the last surfacing in `notes` as a null-property error about
`changelogs` rather than an input error. It now rejects each with a message naming the
actual cause. This is what schemaVersion was for; nothing had been reading it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FADRmu9uhtCk3StMoqwV27
Comment thread packages/notes/src/input/version-output.ts
claude added 3 commits October 9, 2026 15:48
…family

Review follow-ups for the envelope work:

- `publish --json` reported `changed: false` after creating the release
  commit and tags whenever no registry published (GitHub-release-only,
  `--skip-publish`). `publishDidChange` now also counts `git.committed`,
  which is only set for a commit this run created; `standing-pr publish`
  never commits (`skipGitCommit`), so its `changed` is unaffected.
- `publish` exited 9/1 regardless of the error code, contradicting the
  code-to-exit table. It now exits via `exitCodeForError`, like
  `version`: config failures exit 2, input failures 3, pipeline
  failures still 9.
- `version --output` with a relative path landed inside `--project-dir`
  (the command chdirs first). Resolve it against the invocation
  directory up front, as `release --output` does.
- docs: the `version | notes | publish` pipe never worked (`notes` does
  not pass the VersionOutput on); document chaining through a file.
  Correct the `changed`/retry wording and the exit-code example. Update
  the version and publish READMEs for the envelope and `--output`.
- release: reuse core's `writeEnvelope` instead of a private copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e
…output test into CI

Second-review follow-ups:

- docs: the file-based `version` -> `notes` -> `publish` chain left the
  changelog out of the release commit (publish commits only the version
  files plus what is staged). Document staging it; verified end to end.
- docs: a re-run of the standalone `publish` in the same tree stops at
  the git-commit stage, so drop the blanket "a retry is safe".
- docs: the exit-code examples named codes `version` never throws
  (TAG_ALREADY_EXISTS / GIT_PROCESS_ERROR are unreachable). Use codes
  `publish` actually emits: input -> 3, config -> 2, pipeline -> 9.
- test: the `--output` integration test lived in version's integration
  suite, which CI never runs. Replace it with a unit test in
  test/unit/cli.spec.ts (fails against the pre-fix command).
- publishDidChange docstring: `committed` is also set in a dry run; the
  dry-run guard is what keeps that out of `changed`.
- version README: the flag table listed --strict-reachable, --verbose and
  --quiet, which the command doesn't have, and omitted several it does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e
Third-review follow-ups:

- The specific-code / family-exit contract holds for version, notes and
  publish, but `release` and `standing-pr` report errors raised inside
  their bundled version/notes/publish stages as GENERAL_ERROR (exit 1):
  the release bundle carries several copies of core's ReleaseKitError,
  so the instanceof check misses errors from the other copies. Say so
  rather than claim otherwise; the cause predates this PR and is fixed
  separately.
- `data` on error: describe the actual exception (a publish pipeline
  failure carries partial output even when nothing landed).
- core README: VersionOutput is now the envelope's `data`.
- Restore unit tests for releaseDidPublish, the standing-pr `changed`
  wiring, dropped when the predicate moved to @releasekit/publish.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e
claude added 2 commits October 9, 2026 16:39
… the notes exit claim

Fourth-review follow-ups:

- docs/cli.md claimed the family exit-code contract for `notes`, but
  notes exits 1 for config errors and most non-parse failures; only its
  input-parse failures exit 3. Say exactly that.
- An unwritable `--output` path made the result write throw, then the
  error path threw again writing its envelope to the same file, crashing
  with a stack and no envelope. writeEnvelope still throws for a result
  write (the command must fail), but notes a failed error-envelope write
  on stderr instead of throwing out of the error handler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e
…t result

Fifth-review nits:

- notes only checks that its input has a `changelogs` array, so "a
  schema-invalid object" overstated the exit-3 case; say what it checks.
- When the result write to --output fails, throw "Could not write the
  result to <path>: ..." instead of the bare ENOENT, so stderr says the
  command's work happened but its result never reached the file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e
Remove the releaseDidPublish unit tests (a one-line wrapper over
publishDidChange, which publish's own tests cover) and rethrow the
original error when a result can't be written to --output instead of
re-wrapping it — the ENOENT already names the path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e
INPUT→3 runs the same exit-code path as CONFIG→2, which stays as the guard
against hard-coded exits. The cli.md notes clause is trimmed to the commands
the table holds for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e
#651 is merged, so an error raised inside their version, notes or publish
stage keeps its code instead of degrading to GENERAL_ERROR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e
- core: add writeErrorEnvelope next to writeEnvelope; version, publish and
  release's emitError now call it instead of each building the same
  single-error envelope.
- core: isEnvelope is no longer exported — only unwrapEnvelope needs it, and
  the version test helper now reads output through unwrapEnvelope, the same
  boundary notes and publish cross.
- publishDidChange: drop the `?.` / `?? []` guards. Every PublishOutput it
  sees comes from runPipeline's ctx.output (the only PipelineError is built
  from it), whose registry arrays are always initialised.
- Drop comments that repeat the unwrapEnvelope JSDoc or another comment,
  the old-behaviour note on publish's exit, and the "pipe's contract is
  unchanged" claim (older consumers reject the envelope).
- Braces on the one-line ifs and loop this PR added; give publishDidChange
  its own export section so runPipeline keeps its header.
- Drop the "older producer" unwrap test: it used the current
  schemaVersion, so it duplicated the success-envelope case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e
…mmands

- version --json: an engine error reaches stdout as an error envelope with
  its own code and exits by its family (INVALID_CONFIG -> 2); --stable with
  --prerelease prints an INPUT_ERROR envelope and exits 3.
- publish --output: a PipelineError writes its envelope, partial output in
  `data`, to the file and nothing to stdout.
- publish --input holding a failed upstream envelope runs the real
  parseInput and exits 3 with INPUT_PARSE_ERROR. It replaces the "registry
  failure" test, which rejected runPipeline with a bare NPM_PUBLISH_ERROR
  that runPipeline never throws (it wraps every stage error in a
  PipelineError).
- Exit-code coverage moves from a hardcoded list in core to each package's
  own error enums (PublishErrorCode; VersionErrorCode, GitErrorCode and
  StrictReachableError), so a new unmapped code now fails a test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e
…E to this PR's flags

- troubleshooting: "the JSON schema is stable across minor versions" no
  longer holds — this release changes the version/publish --json shape. Say
  what is versioned instead: the envelope's schemaVersion.
- version README: revert the flag-table rewrite that rode along (short
  aliases, --stable, --allow-first-bump, --sync, --config,
  --include-prerequisites, and dropping --strict-reachable / --verbose /
  --quiet). Keep only the --json description and the new --output row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e
@goosewobbler goosewobbler changed the title feat(cli): envelope the version and publish commands, unwrap on the pipe feat(cli)!: envelope the version and publish commands, unwrap on the pipe Oct 22, 2026
…e-commands

# Conflicts:
#	packages/version/README.md
@goosewobbler goosewobbler mentioned this pull request Oct 22, 2026
10 of 19 tasks
Same sweep as #679, run on this branch so merging main resolves cleanly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e
…e-commands

# Conflicts:
#	packages/publish/src/command.ts
#	packages/release/src/commands/changed.ts
#	packages/release/src/commands/emitResult.ts
…e-commands

#668's noConsole rule meets this branch's core/src/cli.ts, the shared JSON
writer behind every command's --json/--output, so it joins the exempt CLI
files. printJsonOutput keeps this branch's writeEnvelope version, which no
longer needs #668's suppression.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e
…t alone

The --json print takes a one-line noConsole suppression and the unwritable
error-envelope note goes through the logger, so core/src/cli.ts no longer
needs a file-wide exemption. A version --output test now fails if --output
alone stops collecting the result. Inline releaseDidPublish, drop a guard
writeEnvelope already applies, and trim two comments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e
…e-commands

# Conflicts:
#	packages/version/test/integration/sync-tags.spec.ts
claude and others added 3 commits October 23, 2026 00:58
…e-commands

# Conflicts:
#	packages/publish/test/unit/stages/input.spec.ts
…e-commands

Keeps #674's CARGO_LOCK_SYNC_ERROR in the version group of the expanded
exit-code table. With version exiting by its code's family, docs/rust.md
no longer says the standalone command exits 1 for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e
Fix the failed-upstream wording in docs/cli.md (it's an input error that names
the upstream code), say plainly that a partial publish can't be resumed yet,
and cut or shorten the comments that restated code or docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pe comments

Co-Authored-By: Claude Opus 5.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.

2 participants