Skip to content

[eas-cli] Print a JSON error on stdout when a --json command fails - #4593

Draft
vonovak wants to merge 3 commits into
vonovak/failure-reason-stderrfrom
vonovak/json-error-output
Draft

vonovak wants to merge 3 commits into
vonovak/failure-reason-stderrfrom
vonovak/json-error-output

Conversation

@vonovak

@vonovak vonovak commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Why

When a command run with --json fails, stdout holds no JSON. A program must read the human message. Also, build:list enables JSON output only after getContextAsync, so errors from the context step went to stdout as text.

Stacked on #4592. This is the third of four PRs: #4591 ← #4592 ← #4593 (this PR) ← #4594. Error codes are not in this PR. #4594 adds them, so the eas-cli team can decide on that contract separately.

How

  • EasCommand.run enables JSON output before runAsync when the command declares a json flag and --json is in argv. The check copies oclif's jsonEnabled, so --json after -- does not count. enableJsonOutput does nothing on a second call, so the existing calls in commands stay correct.

  • EasCommand.catch prints this object on stdout:

    { "error": { "message": "...", "requestId": "..." } }

    requestId comes from the first GraphQL error. The human message already shows it. The message has no ANSI codes. GraphQL error codes are not printed, because they belong to the server.

  • The error prints once per process. update:rollback runs update:republish in the same process, and the innermost error is the one that says what went wrong.

  • Errors.exit(1) carries no reason, so its JSON message is <command> command failed. instead of EEXIT: 1. The reason is on stderr.

Known limits:

  • process.exit calls do not reach catch, so they print no JSON error. A follow-up can turn them into throws.
  • metadata:lint --json printed nothing on stdout when it failed for a reason other than validation. It now prints the error object.
  • In a terminal, a link in the message loses its URL when the ANSI codes are removed. Piped, the link is plain text, so the URL stays.

LLM Authorship

  • Vojtech asked for machine-readable errors with --json. Claude Code wrote the change.
  • An AI design review added the early enableJsonOutput call and the once-per-process rule for update:rollback. An AI diff review checked every command with a json flag: each enables JSON output only under that flag, and none writes its result with Log.log. The same review found the EEXIT message, which is now fixed and tested.

Test Plan

  • EasCommand tests: JSON output is on before runAsync; the JSON error for an EasCommandError, for Errors.exit, and for a GraphQL error (request ID, no server code); no JSON error without --json, with --json after --, or for a command without a json flag.

  • json-test.ts: one object on stdout, without ANSI codes, once per process.

  • Real run, not logged in, build:list --json --non-interactive in a project with eas-cli in devDependencies:

    stdout: {"error": {"message": "Either log in with eas login or set the EXPO_TOKEN ..."}}
    stderr: An Expo user account is required to proceed.
            Either log in with eas login or set the EXPO_TOKEN ...
                Error: build:list command failed.
    

    24.12.1 printed the reason as text on stdout, and the dependency notice first on stderr.

  • config --json --profile x: stdout is {"error": {"message": "Missing build profile in eas.json: \"x\". Available profiles: []"}}.

  • eas-cli unit tests: 3253 passed. 2 tests in simulator/__tests__/egressLimits.test.ts fail with ECONNRESET on localhost. They fail the same way on main on this machine.

  • tsc, oxlint and oxfmt are clean.

🤖 Generated with Claude Code

…erminal

The notice is advice for a person. A program that runs eas-cli reads
stderr for the command's error and found this notice first.

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

codecov Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.06%. Comparing base (3cb54f7) to head (1dcfc9a).

Additional details and impacted files
@@                        Coverage Diff                        @@
##           vonovak/failure-reason-stderr    #4593      +/-   ##
=================================================================
+ Coverage                          74.04%   74.06%   +0.02%     
=================================================================
  Files                               1018     1018              
  Lines                              49544    49569      +25     
  Branches                           10684    10690       +6     
=================================================================
+ Hits                               36680    36707      +27     
+ Misses                             11871    11869       -2     
  Partials                             993      993              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@vonovak
vonovak force-pushed the vonovak/json-error-output branch from 3da5c85 to d466904 Compare October 9, 2026 19:42
@vonovak
vonovak added this pull request to stack #4595 October 9, 2026 19:52
EasCommand.catch printed the reason with Log.error, which writes to
stdout. oclif then printed "<command> command failed." to stderr. A
program that reads stderr for the error found only that generic line.

Only the catch changes. Other Log.error and Log.warn calls stay on
stdout, because some of them are not failures and the spinner keeps
non-TTY output off stderr on purpose.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A command run with --json printed nothing on stdout when it failed, so
a program had to read the human message.

- EasCommand.run enables JSON output before the context is resolved,
  so context messages and errors stay off stdout. build:list enabled it
  only after getContextAsync.
- EasCommand.catch prints {"error": {"message", "requestId"?}}. The
  request ID is the one the human message already shows. Once per
  process, because update:rollback runs other commands in-process.
- Errors.exit(1) carries no reason, so its message is
  "<command> command failed." instead of "EEXIT: 1".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vonovak
vonovak force-pushed the vonovak/json-error-output branch from d466904 to 1dcfc9a Compare October 12, 2026 08:04
@github-actions

Copy link
Copy Markdown

✅ Thank you for adding the changelog entry!

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