fj run watch / view have no --exit-status, so a failed run exits 0 and cannot gate CI #125

Open
opened 2026-06-11 00:51:51 +00:00 by stephen · 2 comments
Owner

What

fj run watch and fj run view always exit 0 once a run reaches a terminal
state, whether the run succeeded or failed. There is no --exit-status
(gh's gh run watch --exit-status / gh run view --exit-status) to make the
process exit code reflect the run conclusion. In CI this forces a fragile
"stream, then re-parse output to decide if it passed" dance instead of letting
the shell branch on $?.

Evidence

watch declares the run finished and returns Ok(()) no matter the status
(src/cli/workflow_run.rs:410):

if view.state.run.done {
    println!(
        "\n{} {}",
        output::bold("run finished:"),
        output::action_status(&view.state.run.status),
    );
    return Ok(());            // <- always success, even on failure/cancelled
}

view is the same: it prints the summary and returns Ok(())
(src/cli/workflow_run.rs:261-278). The process exit path returns 1 only on
an Err, 0 otherwise (src/main.rs:62-63), so a red run is indistinguishable
from a green one to a script. docs/gh-to-fj.md:109 maps gh run watch to
fj run watch but drops gh's --exit-status, the flag CI relies on.

Why it matters for CI/automation buyers

The canonical "kick a workflow and gate the pipeline on it" pattern:

fj run watch "$RUN" --exit-status || exit 1     # block the deploy on green

Today the || exit 1 is dead code: fj run watch exits 0 on a failed run, so
the deploy proceeds. The only workaround is fj run view N --json plus grepping
.status / .jobs[].status, exactly the brittle scripting the --json work
set out to remove.

Proposed shape

  • Add --exit-status to fj run watch and fj run view: when set, exit
    non-zero if the run's terminal conclusion is anything but success /
    skipped. Default stays 0 so existing piping is unaffected.
  • watch follows a single --job (default 0) but keys completion off
    whole-run done; the exit status should reflect the whole run's
    conclusion (view.state.run.status), not just job 0, so a failure in job 1
    isn't reported as success. This mirrors the existing --log-failed
    "don't hide a failure behind a green job 0" reasoning at
    src/cli/workflow_run.rs:160.

Scope

Exit-code plumbing over data already fetched; no new endpoints. Distinct from
the HTTP-error exit-code concern in rasterstate/fj#123 (that is about failed API
calls; this is about a successful command observing a failed run). Pairs
with rasterstate/fj#129 to make "trigger + gate" work end to end.

## What `fj run watch` and `fj run view` always exit `0` once a run reaches a terminal state, whether the run **succeeded or failed**. There is no `--exit-status` (gh's `gh run watch --exit-status` / `gh run view --exit-status`) to make the process exit code reflect the run conclusion. In CI this forces a fragile "stream, then re-parse output to decide if it passed" dance instead of letting the shell branch on `$?`. ## Evidence `watch` declares the run finished and returns `Ok(())` no matter the status (`src/cli/workflow_run.rs:410`): ```rust if view.state.run.done { println!( "\n{} {}", output::bold("run finished:"), output::action_status(&view.state.run.status), ); return Ok(()); // <- always success, even on failure/cancelled } ``` `view` is the same: it prints the summary and returns `Ok(())` (`src/cli/workflow_run.rs:261-278`). The process exit path returns `1` only on an `Err`, `0` otherwise (`src/main.rs:62-63`), so a red run is indistinguishable from a green one to a script. `docs/gh-to-fj.md:109` maps `gh run watch` to `fj run watch` but drops gh's `--exit-status`, the flag CI relies on. ## Why it matters for CI/automation buyers The canonical "kick a workflow and gate the pipeline on it" pattern: ```sh fj run watch "$RUN" --exit-status || exit 1 # block the deploy on green ``` Today the `|| exit 1` is dead code: `fj run watch` exits `0` on a failed run, so the deploy proceeds. The only workaround is `fj run view N --json` plus grepping `.status` / `.jobs[].status`, exactly the brittle scripting the `--json` work set out to remove. ## Proposed shape - Add `--exit-status` to `fj run watch` and `fj run view`: when set, exit non-zero if the run's terminal conclusion is anything but `success` / `skipped`. Default stays `0` so existing piping is unaffected. - `watch` follows a single `--job` (default `0`) but keys completion off whole-run `done`; the exit status should reflect the **whole run's** conclusion (`view.state.run.status`), not just job 0, so a failure in job 1 isn't reported as success. This mirrors the existing `--log-failed` "don't hide a failure behind a green job 0" reasoning at `src/cli/workflow_run.rs:160`. ## Scope Exit-code plumbing over data already fetched; no new endpoints. Distinct from the HTTP-error exit-code concern in rasterstate/fj#123 (that is about failed API calls; this is about a *successful* command observing a *failed run*). Pairs with rasterstate/fj#129 to make "trigger + gate" work end to end.
Author
Owner

Converted to backlog item rasterstate/fj#135 (p1, size S).

Converted to backlog item rasterstate/fj#135 (p1, size S).
Author
Owner

This is already implemented on main, so this can be closed as resolved.

fj run watch and fj run view both take --exit-status:

  • ensure_exit_status() (src/cli/workflow_run.rs) exits non-zero unless the run's conclusion is success or skipped. Without the flag the exit code is unchanged, so existing piping stays at 0.
  • watch keys the exit code off the whole-run status (view.state.run.status), not job 0, so a failure in a later job is not masked by a green job 0 (the nuance this issue raised).
  • Documented in docs/gh-to-fj.md (the fj run view N --exit-status and fj run watch N --exit-status rows).
  • Covered by tests: ensure_exit_status_ignores_failed_runs_without_flag, ensure_exit_status_accepts_success_and_skipped, ensure_exit_status_rejects_failed_and_cancelled_runs, run_view_exit_status_flag_parses, run_watch_exit_status_flag_parses.

Landed in #141 and #151. The canonical fj run watch "$RUN" --exit-status || exit 1 gate works now.

This is already implemented on `main`, so this can be closed as resolved. `fj run watch` and `fj run view` both take `--exit-status`: - `ensure_exit_status()` (src/cli/workflow_run.rs) exits non-zero unless the run's conclusion is `success` or `skipped`. Without the flag the exit code is unchanged, so existing piping stays at 0. - `watch` keys the exit code off the whole-run status (`view.state.run.status`), not job 0, so a failure in a later job is not masked by a green job 0 (the nuance this issue raised). - Documented in `docs/gh-to-fj.md` (the `fj run view N --exit-status` and `fj run watch N --exit-status` rows). - Covered by tests: `ensure_exit_status_ignores_failed_runs_without_flag`, `ensure_exit_status_accepts_success_and_skipped`, `ensure_exit_status_rejects_failed_and_cancelled_runs`, `run_view_exit_status_flag_parses`, `run_watch_exit_status_flag_parses`. Landed in #141 and #151. The canonical `fj run watch "$RUN" --exit-status || exit 1` gate works now.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
rasterstate/fj#125
No description provided.