Repository navigation
feat(cli)!: envelope the version and publish commands, unwrap on the pipe - #616
Merged
Merged
Conversation
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
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
These changes will be added to the release PR (#614) when merged: Changelogmonorepo 0.41.2 → 0.42.0
After merge — predicted release
Updated automatically by ReleaseKit |
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
…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
8 of 19 tasks
… 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
This was referenced Oct 22, 2026
- 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
4 of 19 tasks
11 of 19 tasks
…e-commands # Conflicts: # packages/version/README.md
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
7 of 20 tasks
…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
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e
This was referenced Oct 22, 2026
…e-commands # Conflicts: # packages/version/test/integration/sync-tags.spec.ts
This was referenced Oct 22, 2026
…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>
This was referenced Oct 23, 2026
Merged
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.
Description
version --jsonandpublish --jsonnow print the same JSON envelope as every other command, and both accept--output <path>.notesandpublishunwrap an envelope on input, so the stages still chain. This finishes the envelope work in #544:noteshas no JSON output, so every--jsoncommand now prints an envelope.--outputresolves against the directory releasekit runs in, not--project-dir, the same asrelease --output.Chaining the stages
Unwrapping.
unwrapEnvelope(core) runs inside notes'parseVersionOutputand publish'sparseInput, 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:
Chain through a file.
notesreads theVersionOutputbut doesn't print it, so it can't sit in a pipe. The three-stage stdin pipe the docs showed never worked;version --json | publishdoes.publishcommits only the filesversionchanged plus whatever is staged, so stage the changelog first:Other changes
errorEnvelopetakes optionaldataandchanged.partialOutput(what was published before the failure), and would reportchanged: falseafter a partial publish.schemaVersionstays 1, because readers that ignoredataon an error are unaffected.publishDidChangemoved to@releasekit/publish, so the publish CLI andstanding-pr publishdecidechangedthe same way.standing-pr publishnever commits, so for it only registry publishes count.writeEnvelopeandwriteErrorEnvelopein core'scli.tsare shared byversion,publishandrelease.--jsonneeds a one-linenoConsolesuppression (chore(biome): flag console in package source #668's rule).Breaking changes
--jsonoutput shape. Scripts that readversion --jsonorpublish --jsonmust now read.data, e.g.jq '.data.updates[]'. An older standalone@releasekit/notesor@releasekit/publishrejects the envelope, so keep all the packages on one version. Worth a callout in the release notes.exitCodeForError).versionexited 1 for every failure, andpublishexited 9 for any publish-package error and 1 for anything else.releaseandstanding-prexit 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-stagePIPELINE_STAGE_ERRORnow exits 9.docs/cli.mdsays 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
--jsonenvelope wording and the new--outputrow. Unrelated fixes to its flag table moved to docs(version): match the README flag table to the CLI #677.release --jsondrops a publish failure's partial output and failed stage thatpublish --jsonreports #700:release --jsondrops a publish failure's partial output and stage.errors[].categoryisn't the coarse category the docs promise #699: the envelope'scategoryisn't coarse enough.version --project-dir <relative path>loads the config from the wrong directory and silently uses defaults #698: a relativeversion --project-dirloads the wrong config.publishstops at the commit, so it can't resume;docs/cli.mdnow says so.publish --inputdroppingupdates[].tagandbaselineTags(publish --input drops per-package tags and baseline tags from the version output #675) was fixed onmainby fix(publish): keep per-package and baseline tags in --input #684.Related Issue(s)
Part of #544.
Type of Change
feat— New featurefix— Bug fixdocs— Documentation changerefactor— Code refactoringtest— Adding or updating testschore— Maintenance, dependencies, toolingScope
@releasekit/version@releasekit/notes@releasekit/publish@releasekit/release@releasekit/config@releasekit/coreChecklist
pnpm lint)pnpm test)pnpm typecheck)Testing Notes
publishreading an enveloped file, a bare file, a failed upstream envelope, and aschemaVersion: 2envelope.notesreading an enveloped payload and a failed one.version --json | publish, and an unwritable--output.version --jsonfailure exits by its code's family; for example,--stable --prereleasegives anINPUT_ERRORenvelope.publish --outputon a pipeline failure writes the partial output to the file.publish --inputholding a failed upstream envelope exits 3.changedis checked forpublish.version --outputworks on its own and with--project-dir.main: it merges cleanly withmainafter 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'sCARGO_LOCK_SYNC_ERRORin the version-error family.🤖 Generated with Claude Code