Skip to main content
Settings
Color Mode
Theme Skin
Background

Appearance preferences are saved in this browser only.

Environment
Current Environment Production

Built with JEKYLL_ENV=production. Changes require deployment.

Theme & Build
Jekyll v3.10.0
Last Build Jul 12, 10:26
Page Location
Page Info
Layout article
Collection hacks
Path _hacks/gh-run-log-failed-and-watch-exit-status.md
URL /hacks/gh-run-log-failed-and-watch-exit-status/
Date 2026-07-12

Read a failing CI run from your terminal: gh run --log-failed (and the watch trap that ships red builds)

A CI run goes red. You open the Actions tab, click the run, click the failed job, expand the step, and scroll past a few hundred lines of “Installing dependencies…” to find the one line that actually matters: expected 200 but got 500. Five clicks and a scroll to read a single sentence.

The gh CLI reads that sentence for you. gh run view --log-failed prints only the log from steps that failed — nothing green, no scrolling. And once you can see failures from the terminal, the temptation is to gate a deploy script on them. That is where the trap is: gh run watch reports a failed run as a success unless you ask it not to. This hack is both halves — the command that saves you the clicks, and the flag that saves you from shipping a red build.

Every block below is real captured output (gh version 2.96.0). To get an honest failed run to read, I pushed a throwaway repo with a workflow whose middle step exits 1, captured the output, and deleted the repo. This is the CLI-side companion to IT-Journey’s GH-600 Evaluation Signals Table — that note catalogs the signals that tell you pass from fail; this is how you read the pass/fail signal without leaving the shell (and the one place gh reads it wrong).

The run went red — find it without the web UI

gh run list is your Actions tab as a table. The failed run is the one with failure in the second column:

$ gh run list --limit 5
completed  failure  ci: a workflow with one failing step  ci  main  push  29187887484  7s  2026-07-12T09:41:10Z

Grab that run ID (29187887484). gh run view <id> gives you the job-and-step tree, with an X on exactly what broke:

$ gh run view 29187887484

X main ci · 29187887484
Triggered via push less than a minute ago

JOBS
X test in 2s (ID 86637188627)
  ✓ Set up job
  ✓ Run echo "installing deps..." && echo "ok"
  X unit tests
  - Run echo "this step never runs"
  ✓ Complete job

ANNOTATIONS
X Process completed with exit code 1.
test: .github#9

To see what failed, try: gh run view 29187887484 --log-failed
View this run on GitHub: https://github.com/…/actions/runs/29187887484

Notice the tree already tells the story: unit tests failed (X), and the step after it shows - — it never ran, because the job stopped. You’ll know you’re reading it right when the X marks the first failing step and everything below it is - (skipped). gh even prints the next command to run for you.

The payoff: –log-failed prints only the red

Take gh’s suggestion. --log-failed dumps the log for the failed steps and nothing else — no “Set up job”, no successful step, no scrolling:

$ gh run view 29187887484 --log-failed
test  unit tests  Run echo "running unit tests"
test  unit tests  echo "running unit tests"
test  unit tests  echo "FAIL: expected 200 but got 500" >&2
test  unit tests  exit 1
test  unit tests  shell: /usr/bin/bash -e {0}
test  unit tests  running unit tests
test  unit tests  FAIL: expected 200 but got 500
test  unit tests  ##[error]Process completed with exit code 1.

(Real capture, trimmed: each line is prefixed job⇥step⇥ and carries an ISO timestamp and ANSI color codes I stripped for readability. Pipe it through sed 's/\x1b\[[0-9;]*m//g' if the escape codes clutter your terminal.)

There it is — FAIL: expected 200 but got 500 and ##[error]Process completed with exit code 1 — the two lines you clicked five times for, printed the moment you asked. If a run has several jobs and you want only one, scope it with --job:

$ gh run view --job 86637188627 --log-failed
test  unit tests  running unit tests
test  unit tests  FAIL: expected 200 but got 500
test  unit tests  ##[error]Process completed with exit code 1.

The trap: gh run watch says a failed build succeeded

gh run watch follows a run live and blocks until it finishes — the natural thing to reach for in a script that waits for CI before doing the next step. So you’d expect it to exit non-zero when the run fails, like every other well-behaved command. It does not. Watch the exit code:

$ gh run watch 29187887484; echo "EXIT: $?"
Run ci (29187887484) has already completed with 'failure'
EXIT: 0

The run failed. gh run watch printed the word failure to your screen — and then exited 0. By the only signal a script can read, this red build is green. Add --exit-status and the exit code finally matches reality:

$ gh run watch 29187887484 --exit-status; echo "EXIT: $?"
Run ci (29187887484) has already completed with 'failure'
EXIT: 1

Why this actually bites: the deploy that ships red

This is not a trivia-question footgun. The whole reason to watch in a script is to gate the next step on it. Here is that gate, with and without the flag — same failed run both times:

$ if gh run watch 29187887484 >/dev/null 2>&1; then echo "shipping"; else echo "aborting"; fi
shipping

$ if gh run watch 29187887484 --exit-status >/dev/null 2>&1; then echo "shipping"; else echo "aborting"; fi
aborting

The first line shipped a build that failed its tests, silently, because gh run watch told the if the build passed. The flag is the entire difference between a deploy that respects CI and one that ignores it. If you take one thing from this hack: any script that waits on gh run watch needs --exit-status, the same way any pipeline you trust needs set -o pipefail.

If you don’t need to wait — the run is already done — skip watch entirely and read the conclusion straight out of the API, which never lies about its exit signal:

$ gh run view 29187887484 --json conclusion -q .conclusion
failure

That’s the one to reach for in a script gating on an already-finished run: [ "$(gh run view "$id" --json conclusion -q .conclusion)" = success ].

When this goes wrong

  • --log-failed prints nothing — the run didn’t fail (check gh run view for an X), or it failed setting up rather than in a step (a bad runs-on, a missing secret). Fall back to the full gh run view <id> --log and read from the top.
  • gh run watch returns instantly with “has already completed” — that’s expected on a finished run; it only streams live while a run is in progress. The exit-code behavior (0 without --exit-status, non-zero with it) is identical either way, which is exactly why the trap is easy to miss in testing: you test against a completed run, it “works”, and the silent-green only bites in production against a live one.
  • could not find any workflows / wrong repogh uses the repo of your current directory. Add -R owner/name to point it somewhere else.
  • The log is a wall of ANSI escape codes — you piped it somewhere non-interactive. Strip them: gh run view <id> --log-failed | sed 's/\x1b\[[0-9;]*m//g'.
  • gh: command not found or an auth error — install gh and run gh auth login; in Actions, gh needs GH_TOKEN in the env — a present secret isn’t enough.

The reflex when a run goes red is to open the browser and start clicking. You don’t have to. gh run view --log-failed prints the failure straight to your terminal — and when you graduate to scripting around it, remember that gh run watch will call that same failure a success until you make it prove otherwise.