Summary
ExecuteProgramCommand() in internal/scheduler/shell.go type-asserts the accumulated error instead of the error just returned by the command when extracting the process exit code:
out, e := Cmd.CombinedOutput(ctx, command, params...)
if e != nil {
exitCode = -1
err = errors.Join(err, e) // err becomes *errors.joinError (Join always wraps non-nil input)
if exitError, ok := err.(*exec.ExitError); ok { // asserts on the JOINED error, never succeeds
exitCode = exitError.ExitCode()
}
}
Because errors.Join(nil, e) wraps even a single non-nil error in its own *joinError type, the assertion err.(*exec.ExitError) can never succeed. Every failing program task is therefore logged with exit_code = -1, and the real exit status of the process (exitError.ExitCode()) is discarded.
Static-analysis finding against current master; not executed here.
Location
- File: internal/scheduler/shell.go
- Function: ExecuteProgramCommand() (~lines 55-62 of the loop body), consumed by sch.pgengine.LogTaskExecution(context.Background(), task, exitCode, ...) a few lines below.
Problem
Go's errors.Join returns a non-nil *joinError whenever at least one input error is non-nil - it does not pass through a lone wrapped error unchanged. After the first failing parameter set, err is a *joinError (or a join of several errors on later iterations), so:
- err.(*exec.ExitError) fails on every iteration;
- exitCode remains the pre-set -1;
- LogTaskExecution() records -1 in timetable.execution_log for every failure, whether the underlying process exited with 1, 3, 127 or was killed.
The assertion was clearly meant to inspect e (the immediate error from this command run).
Trigger / Reproduction
Based on static analysis; no runtime run performed:
- Create a PROGRAM task whose command exits with a distinctive status, e.g. /bin/false (exit 1) or a script doing exit 3.
- Run it via any chain (single parameter value is enough).
- Observe timetable.execution_log: exit_code is recorded as -1 rather than 1/3.
Expected Behavior
The recorded exit code should reflect the actual process exit status when the failure is an *exec.ExitError, keeping -1 only for cases like signal termination where no exit code exists (and ideally distinguishing context-cancellation kills as well).
Actual Behavior
All failures are flattened to -1; real exit codes are lost for every execution.
Impact
Monitoring built on execution_log.exit_code cannot distinguish failure modes (e.g. "script validation failed with 3" vs "binary missing"), breaking alerting logic that matches specific codes. The accumulation intent across multiple parameter sets is preserved only for the final returned error, not for per-run logging.
Suggested Direction
Assert on e instead of err:
if e != nil {
exitCode = -1
var exitError *exec.ExitError
if errors.As(e, &exitError) {
exitCode = exitError.ExitCode()
}
err = errors.Join(err, e)
}
(errors.As also future-proofs against wrapped variants.)
Evidence
Summary
ExecuteProgramCommand() in internal/scheduler/shell.go type-asserts the accumulated error instead of the error just returned by the command when extracting the process exit code:
Because errors.Join(nil, e) wraps even a single non-nil error in its own *joinError type, the assertion err.(*exec.ExitError) can never succeed. Every failing program task is therefore logged with exit_code = -1, and the real exit status of the process (exitError.ExitCode()) is discarded.
Static-analysis finding against current master; not executed here.
Location
Problem
Go's errors.Join returns a non-nil *joinError whenever at least one input error is non-nil - it does not pass through a lone wrapped error unchanged. After the first failing parameter set, err is a *joinError (or a join of several errors on later iterations), so:
The assertion was clearly meant to inspect e (the immediate error from this command run).
Trigger / Reproduction
Based on static analysis; no runtime run performed:
Expected Behavior
The recorded exit code should reflect the actual process exit status when the failure is an *exec.ExitError, keeping -1 only for cases like signal termination where no exit code exists (and ideally distinguishing context-cancellation kills as well).
Actual Behavior
All failures are flattened to -1; real exit codes are lost for every execution.
Impact
Monitoring built on execution_log.exit_code cannot distinguish failure modes (e.g. "script validation failed with 3" vs "binary missing"), breaking alerting logic that matches specific codes. The accumulation intent across multiple parameter sets is preserved only for the final returned error, not for per-run logging.
Suggested Direction
Assert on e instead of err:
(errors.As also future-proofs against wrapped variants.)
Evidence