Skip to content

feat(cli): uniform JSON envelope with structured errors and changed reporting - #608

Merged
goosewobbler merged 5 commits into
mainfrom
feat/544-cli-envelope
Aug 1, 2026
Merged

goosewobbler merged 5 commits into
mainfrom
feat/544-cli-envelope

Conversation

@goosewobbler

@goosewobbler goosewobbler commented Jul 21, 2026 •

Copy link
Copy Markdown
Owner

First PR for the #544 epic (agent-grade CLI contract). Establishes the uniform --json envelope on the orchestration commands so humans, CI, and agents share one output surface.

The envelope

Every --json / --output result from release, gate, and standing-pr is now wrapped:

{
  "schemaVersion": 1,   // contract version; stable across minor releases
  "status": "success",  // "success" | "error"
  "changed": true,      // did work, vs. already in the desired state
  "data": { /* … */ },  // command payload verbatim (VersionOutput, gate result); null on error
  "warnings": [{ "code": "…", "message": "…" }],
  "errors": [{ "code": "…", "category": "…", "retryable": false, "message": "…" }]
}

What's in this PR (mapped to the epic's scope)

  • Uniform envelope — new @releasekit/core module (successEnvelope / errorEnvelope), applied via emitResult / emitError in the release, gate, and standing-pr commands. data wraps the payload, never replaces it — release --json still exposes VersionOutput at data.versionOutput, so the manifest-compat invariant holds.
  • Structured error codes — toEnvelopeError maps thrown errors to { code, category, retryable, message }. retryable is conservative (only true for known-transient failures, e.g. an LLMError that classified a timeout/429/5xx), so an agent never retries an unknown failure. exitCodeForError maps the code to the process exit code (config → 2, llm → 5, …).
  • changed reporting — a dry run is never changed; standing-pr publish reports changed: false on skip-if-published; gate is read-only (changed: false).
  • Stream discipline — only the envelope goes to stdout; diagnostics stay on stderr (the logger already uses console.error), so parsing stdout as JSON is always safe.
  • Action integration — run-action.mjs's parseReleaseOutput unwraps the envelope's data, so every existing Action output (tags, has-changes, version-output, should-release, …) is derived unchanged. Legacy un-enveloped output is tolerated.
  • Docs — new "JSON output contract" section in docs/cli.md with the shape, the error-code/exit-code table, and the stability guarantee.

Deliberately deferred (follow-up PRs within #544)

  • Extend the envelope to the version | notes | publish pipe commands. These pipe JSON between processes, so enveloping their output requires the consuming side to unwrap on input — a self-contained change kept separate to avoid touching the inter-process contract here. This PR leaves that pipe untouched (those packages don't import release's emitResult).
  • Dry-run as a structured plan diff — exposing the version→notes→publish plan as a machine-readable data plan.

Testing

  • New envelope.spec.ts (core) — builders, error mapping, exit-code mapping.
  • Updated emitResult.spec.ts + release-command.spec.ts for the envelope shape; new emitError coverage.
  • New parseReleaseOutput tests for envelope unwrap (success + error envelopes) alongside the legacy path.
  • Full suites green: core (195), release (832), root integration (185); repo-wide typecheck + biome check + eslint clean.

Part of #544.

Greptile Summary

This PR establishes a uniform --json output envelope for the release, gate, and standing-pr orchestration commands, replacing bare JSON payloads with a versioned { schemaVersion, status, changed, data, warnings, errors } wrapper shared by humans, CI, and agents.

  • New @releasekit/core envelope module — successEnvelope/errorEnvelope builders, toEnvelopeError for structured error mapping, and exitCodeForError for process exit routing; all wired into emitResult/emitError/failInput in the release, gate, and standing-pr commands.
  • Input-validation paths made contract-safe — bare console.error + process.exit guards in all three commands are replaced by failInput, so --json/--output callers always receive a structured INPUT_ERROR envelope even on pre-flight validation failures.
  • changed semantics per command — dry runs always false; standing-pr publish inspects registry results via publishDidChange; gate is hardwired to false; runStandingPRPublish null-return paths converted to throws so they surface as status:\"error\" rather than silent no-ops.
  • Action and E2E compatibility — run-action.mjs's parseReleaseOutput unwraps .data transparently; the workflow YAML and helpers.sh add the same jq unwrap at their respective layers; legacy un-enveloped output is tolerated at every layer.

Confidence Score: 5/5

Safe to merge — the envelope contract is additive, all three command layers consistently emit the uniform wrapper, and the input-validation fast-exit paths that previously bypassed the JSON channel are now correctly routed through failInput.

The two previously flagged validation bypass issues are both resolved: bare process.exit guards in release-command and standing-pr-command now go through failInput, which emits an error envelope before exiting. The publishFromManifest and runStandingPRPublish null-return paths are converted to throws, eliminating the silent success no-op risk. The categoryForCode derivation, publishDidChange predicate, and action-layer unwrapping are all correct and covered by the new test suite.

Files Needing Attention: No files require special attention.

Important Files Changed

Filename Overview
packages/core/src/envelope.ts New file: defines the uniform JSON envelope types and builders, error mapping, and exit-code routing. Logic is correct and well-tested.
packages/core/src/errors.ts Adds InputError (code INPUT_ERROR) — a new concrete ReleaseKitError subclass for CLI input-validation failures.
packages/release/src/commands/emitResult.ts Refactored to emit the uniform envelope; introduces emitError and failInput. All validation paths now honour the JSON contract before exiting.
packages/release/src/commands/release-command.ts Input-validation branches converted from bare console.error + process.exit to failInput; changed derived from version-update count with dry-run guard.
packages/release/src/commands/standing-pr-command.ts All three subcommands now wrap results in the envelope; invalid --pr paths use failInput; changed derived per subcommand semantics.
packages/release/src/commands/changed.ts New publishDidChange helper correctly reads registry results (not manifest data) to detect whether any package actually reached a registry this run.
packages/release/src/standing-pr/standing-pr.ts Error-return paths converted to throws so the envelope command layer reports status:error instead of a silent success no-op.
scripts/run-action.mjs parseReleaseOutput unwraps the envelope data payload; legacy un-enveloped output passes through unchanged. Type declaration updated to include null.
.github/workflows/_release.reusable.yml Shell-level unwrap added to both release jobs; guard fires only after a successful exit.
test/e2e/lib/helpers.sh run_cli_json now extracts .data from the envelope so E2E callers see payload fields directly; non-envelope output passes through unchanged.

Reviews (5): Last reviewed commit: "fix(cli): derive standing-pr publish `ch..." | Re-trigger Greptile

…eporting

Wrap the --json / --output of the orchestration commands (release, gate,
standing-pr) in one uniform envelope so humans, CI, and agents share a single
output contract:

  { schemaVersion, status, changed, data, warnings, errors }

- data carries each command's payload verbatim (VersionOutput etc.) — the
  envelope wraps, never replaces, so the manifest-compat invariant holds.
- structured error codes replace prose-only failures in JSON mode, each with a
  category and a conservative retryable flag; the process exit code maps to the
  error code (config -> 2, llm -> 5, ...).
- changed distinguishes real work from a no-op: dry runs, skip-if-published,
  and the read-only gate all report changed: false.
- stream discipline: only the envelope is written to stdout, all diagnostics go
  to stderr, so parsing stdout as JSON is always safe.
- the Action unwraps the envelope in parseReleaseOutput, so every existing
  Action output is derived unchanged.

First PR for the #544 epic. Deliberately scoped to the orchestration commands;
follow-ups extend the envelope to the version|notes|publish pipe commands (with
input unwrapping to preserve the pipe) and expose dry-run as a structured plan.

Claude-Session: https://claude.ai/code/session_01K6CmYRXTZQYs4MNo6AETxT

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

Standing release PR: #614 · v0.41.3 queued (4 packages) · open 9m · ✅ 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

  • uniform JSON envelope with structured errors and changed reporting (cli)

Changed

  • bump anthropics/claude-code-action from 1.0.179 to 1.0.183 (deps) (PR #610)
  • bump the development-dependencies group with 3 updates (deps-dev) (PR #612)

Fixed

  • derive standing-pr publish changed from publish effects, error on unresolved PR (cli)
  • envelope pre-flight input-validation errors (cli)
  • unwrap the CLI envelope in the release workflow output parsing (ci)
  • unwrap the CLI envelope in e2e and package-test output consumers (test)

All packages → 0.42.0 (sync versioning)

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

After merge — predicted release

Approximate. The standing PR rebuilds against main at merge time; if other commits land first, the prediction may shift.

Package Standing PR This PR After merge
monorepo 0.41.3 0.42.0 0.42.0 ⚠ escalated from 0.41.3

Updated automatically by ReleaseKit

github-actions Bot and others added 3 commits July 21, 2026 03:55
…sumers

The uniform --json envelope nests each result under `data`, so the e2e helper
run_cli_json and the package-test release-output validator — both of which read
`.versionOutput` at the top level — saw it move and failed. Unwrap the envelope
to its data payload in both (tolerating non-enveloped/legacy output), so the
existing `.versionOutput.*` reads keep working.

Claude-Session: https://claude.ai/code/session_01K6CmYRXTZQYs4MNo6AETxT

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_release.reusable.yml runs the release and standing-pr publish CLI directly and
reads `.versionOutput` from its stdout — which the uniform envelope now nests
under `data`. Without unwrapping, has_changes would read false and no tags would
push. Unwrap RELEASE_OUTPUT to its data payload at both capture points (tolerating
non-enveloped output), so the downstream reads and release-output.json are
unchanged.

Claude-Session: https://claude.ai/code/session_01K6CmYRXTZQYs4MNo6AETxT

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pre-flight CLI arg checks (--stable with --prerelease, --draft with --from-draft,
invalid --from-draft, invalid --pr) exited via console.error + process.exit before
the enveloped path, so in --json / --output mode they produced no envelope (empty
stdout, no output file) — breaking the uniform error contract for ordinary input
errors. The --pr check also exited with GENERAL_ERROR instead of INPUT_ERROR.

Add a typed InputError (code INPUT_ERROR) in core and a failInput() helper that
emits a structured INPUT_ERROR envelope, logs to stderr, and exits with the
input-error code; route all pre-flight validations through it.

Claude-Session: https://claude.ai/code/session_01K6CmYRXTZQYs4MNo6AETxT

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@goosewobbler

Copy link
Copy Markdown
Owner Author

Addressed both Greptile P1 findings in 2bc0a4b:

  • Validation Skips Error Envelope (release-command.ts) and Invalid PR Bypasses JSON Contract (standing-pr-command.ts): pre-flight arg checks (--stable+--prerelease, --draft+--from-draft, invalid --from-draft, invalid --pr) exited before the enveloped path, so --json/--output produced no envelope. The --pr check also used GENERAL_ERROR instead of INPUT_ERROR.

Fix: a typed InputError (code INPUT_ERROR) in core + a failInput() helper that emits a structured INPUT_ERROR envelope, logs to stderr, and exits 3. All pre-flight validations route through it. Added tests asserting the enveloped error + exit code in both commands.

…error on unresolved PR

Three follow-ups to the envelope contract, from review of #608.

`standing-pr publish` derived `changed` from `versionOutput.updates`, which comes
from the merged PR's manifest and is populated on every run — the versions were
already computed and landed on `main` by the merge. A re-run where every package
was skipped as already-published still reported `changed: true`, the exact case
the docs advertised it for. `publishDidChange` reads `publishOutput` instead.
Release tags are created before the pipeline, `git.pushed` is set for any push
attempt, and the GitHub-release stage reports success for an already-existing
release, so registry results are the only honest signal — same
`success && !skipped && !alreadyPublished` predicate the verify stage uses.

`runStandingPRPublish` logged and returned null when it could not resolve a PR to
publish (no GitHub context, no merged standing PR, unreadable or numberless event
payload). Those now throw. Returning null made the envelope assert
`status: "success"` on a misconfiguration — the one field an agent branches on.
Null stays the answer for the genuine no-ops: pull_request:closed fires for every
closed PR, so a non-release head ref or an unmerged PR must remain a quiet skip.
`publishFromManifest` has no no-op outcome at all once a PR is named, so it now
returns `ReleaseOutput` rather than `ReleaseOutput | null`.

The docs claimed every `--json` command emits an envelope; `version`, `notes`, and
`publish` do not, deliberately — they pipe JSON between processes and enveloping
them means teaching the consuming side to unwrap. Scoped the claim to the
orchestration commands and called out the exception.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FADRmu9uhtCk3StMoqwV27
@goosewobbler
goosewobbler merged commit 656dd0d into main Aug 1, 2026
31 checks passed
@goosewobbler
goosewobbler deleted the feat/544-cli-envelope branch August 1, 2026 18:25
@github-actions github-actions Bot mentioned this pull request Aug 1, 2026
goosewobbler added a commit that referenced this pull request Oct 23, 2026
…747)

* fix(notes): drop an entry's own refs from LLM-rewritten descriptions

The model sometimes copies an entry's reference into its rewrite, and
templates then append it again: "(#608) (#608)". The enhance tasks now
remove the entry's own refs from the description they return.

Closes #719

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

* test(notes): assert enhance-and-categorize strips duplicate refs

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:tooling Updates to tooling type:enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant