Skip to content

moon run collapses a failing task's exit code to 1 instead of propagating it #2615

Description

@k0pernikus

Drafted with the help of Claude Fable.

Use case: we run moon tasks in CI, where the pipeline must treat certain exit codes as allowed
failures and all others as job failures (e.g. GitLab's allow_failure: exit_codes). That needs the
task's real exit code; moon run collapses every failure to 1, so exit-code-based job status
cannot be driven through moon.

Steps to reproduce

Reproducer: https://github.com/k0pernikus/moon-run-swallows-exit-codes — a workspace with one task
wrapping a script that exits a random code from 70 / 80 / 90 (codes a pipeline would
allow-list), moon pinned to 2.4.2 via mise, and a GitHub workflow that runs the task bare.

fail.sh:

#!/usr/bin/env bash
set -euo pipefail
codes=(70 80 90)
code="${codes[$((RANDOM % ${#codes[@]}))]}"
echo "fail.sh exiting with code ${code}" >&2
exit "${code}"

moon.yml:

language: bash

tasks:
  fail-with-random-allowed-to-fail-codes:
    command: './fail.sh'
    toolchain: system
    options:
      cache: false
      runFromWorkspaceRoot: true

Run the task and inspect the exit code:

$ moon run demo:fail-with-random-allowed-to-fail-codes; echo "exit: $?"
  × Task demo:fail-with-random-allowed-to-fail-codes failed to run.
  ╰─▶ Process ./fail.sh failed: exit code 80
exit: 1

Run the script directly for contrast:

$ ./fail.sh; echo "exit: $?"
fail.sh exiting with code 80
exit: 80

CI run showing the same:
https://github.com/k0pernikus/moon-run-swallows-exit-codes/actions/runs/29156794001/job/86555120128

Run moon run demo:fail-with-random-allowed-to-fail-codes
▮▮▮▮ demo:fail-with-random-allowed-to-fail-codes (8d5f7a1c)
fail.sh exiting with code 80
▮▮▮▮ demo:fail-with-random-allowed-to-fail-codes (4ms, 8d5f7a1c)
Error: task_runner::run_failed

  × Task demo:fail-with-random-allowed-to-fail-codes failed to run.
  ╰─▶ Process ./fail.sh failed: exit code 80

Error: Process completed with exit code 1.

Expected vs actual

  • Expected: moon run exits with the wrapped task's exit code — 80 here — as the script does when
    run directly, and as task runners like make, npm run, and just propagate a wrapped command's
    code. The pipeline then allow-lists 80 and the job is a tolerated failure.
  • Actual: moon run exits 1, whichever of 70 / 80 / 90 the script exits. moon's own
    diagnostic reports the real code — Process ./fail.sh failed: exit code 80 — yet the process
    returns 1. The allow-list never matches, so a tolerated code is indistinguishable from a
    genuine failure and the job goes red.

Root cause

moon run delegates to exec:

pub async fn run(session: MoonSession, args: RunArgs) -> SessionResult {
exec(session, {
let mut exec = args.to_exec_args();
args.apply_affected_to_exec_args(&mut exec);
exec.targets = args.targets;
exec.on_failure = OnFailure::Bail;
exec.query = args.query;
exec
})
.await
}

exec returns ExecWorkflow::execute's result as the session's exit code:

let executor = ExecWorkflow::new(session, args, plan)?;
let exit_code = executor.execute().await?;
Ok(exit_code)

ExecWorkflow::execute returns a hardcoded Some(1) for any failed action that is not
allowFailure, never the task's own code:

let failed = results.into_iter().any(|result| {
if result.has_failed() {
!result.allow_failure
} else {
false
}
});
if failed {
return Ok(Some(1));
}

The returned Option<u8> becomes the process exit code: None → 0, Some(1) → 1. No branch returns
the wrapped task's exit code.

Possible fixes

  • Propagate the wrapped command's exit code as moon run's exit code — at least when a single task
    target runs, or when all failed targets share one code.
  • Or an opt-in (task option or CLI flag) to surface the underlying exit code.

allowFailure does not cover this use case: it maps a failing task to exit 0, so it can tolerate
a code only by tolerating every failure — the split into allowed codes and job-failing codes is
exactly what it cannot express.

Environment

  • moon 2.4.2 (latest release at time of writing), installed via mise (aqua:moonrepo/moon)
  • Linux x86_64 locally; ubuntu-latest in the CI run above
  • Invocation: moon run <project>:<task>

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions