Skip to content

fix(ci): stop bash -e aborting the eval action's binary and summary steps - #46

Merged
perfectra1n merged 1 commit into
mainfrom
fix/action-errexit-contract-41
Jul 31, 2026
Merged

fix(ci): stop bash -e aborting the eval action's binary and summary steps#46
perfectra1n merged 1 commit into
mainfrom
fix/action-errexit-contract-41

Conversation

@perfectra1n

Copy link
Copy Markdown
Member

Overview

Closes #41.

The half of #41 that was reported — the eval step aborting before it wrote
exit-code / results-path, so the gate annotated every failure as
infrastructure error (exit 3) — landed in #43. This closes out the rest.

Errexit is a property of the shell, not of a step, and the same defect was
still live twice more in the same file:

1. The latest release lookup (bin step). An assignment takes the status
of the pipeline it assigns from, so under -e -o pipefail a 404 or a DNS
failure killed the step at that line. The ::error::could not resolve the latest domarinn release tag and exit 3 written to handle exactly that case
were unreachable code that reads as live. Reproduced under GitHub's interpreter
with a curl that exits 22:

before after
step exit 22 3
job log (empty) ::error::could not resolve the latest domarinn release tag

22 is not one of the four codes the gate step knows how to render, and an
empty log is the worst possible answer to "why is this red".

The step's other two dead ends were fine, which is why this survived review —
they are shaped if curl …; then and if ! command -v cargo, and a command in
a condition is exempt from -e. Same file, same shell, three call sites, one
shape wrong.

2. The Summarize step. It branched on ci-summary's exit code, but the
thing it needs is the file. A CLI that exits 0 without writing one — an
--out it could not create, a run with nothing in it — took the success branch
and then died on the trailing cat, so a reporter whose own comment says "the
gate step below owns the verdict" failed the job instead. The condition is now
"there is no usable summary", which is the actual precondition.

Both fixes are one line each; the rest of the diff is the guard.

Additional context

The guard from #43 executed two of the action's four run: steps. That is how
the two above survived a fix aimed at exactly this bug — opting steps in one at
a time will keep missing a whole-shell property.

crates/domarinn-cli/tests/action_contract.rs now works the other way round: a
registry names every run: step with the environment a caller who set no
optional inputs would produce, and every_shell_step_is_exercised_here fails if
action.yml grows a step the registry does not name. Verified by injecting an
unregistered step and watching it fail, then reverting. Steps are opted out,
not in.

Nine tests, all executing the real run: text lifted out of action.yml under
bash --noprofile --norc -e -o pipefail — reproduced rather than simplified,
because under a plain bash script.sh none of these bugs happen at all and the
tests would pass against the broken action. Both new failing cases were watched
red first:

an_unresolvable_latest_release_is_an_infrastructure_error ... FAILED
  a failed release lookup must be annotated, not left to the shell;
  the step logged:
                                  <- nothing
the_summary_step_never_owns_the_verdict ... FAILED
  ci-summary exited 0 but wrote nothing: the reporter must not fail the job;
  it logged: cat: summary.md: No such file or directory
  left: Some(1)  right: Some(0)

No behaviour change for a healthy run; this only affects what happens when the
network or the CLI misbehaves.

Checklist

  • The PR title is a Conventional Commit
  • mise run ci passes locally — every gate except musl-build, which
    fails on my machine for an unrelated reason (no x86_64-linux-musl-gcc
    cross-compiler installed). clippy, fmt-check, test, schema-check,
    gen-types-check, web-lint, web-build, web-test, docs and
    workflow-lint all pass.
  • Tests were added or updated for this change
  • Generated files were regenerated if their sources changed
    (mise run schema / mise run gen-types) — n/a, no sources changed
  • I have read the contributing guidelines

AI usage disclosure: YES — investigated, fixed, and tested with Claude Code.
Root cause was reproduced by extracting each step's run: text and executing it
under GitHub's exact bash invocation before any fix was written, and both new
tests were confirmed failing against the unfixed action first.

… steps

The eval step's abort-before-writing-outputs was fixed already. The same
defect was still live twice more in the same file, because errexit is a
property of the shell and the guard opted steps in one at a time.

- The `latest` release lookup assigns from a pipeline, so an assignment
  takes its status and a 404 or DNS failure killed the step *at that
  line*. The `::error::could not resolve the latest domarinn release
  tag` and `exit 3` written to handle exactly that were unreachable; an
  operator got a red step with an empty log and curl's raw exit code,
  which is not one of the four the gate step knows how to render.

- The summary step branched on `ci-summary`'s exit code, but the thing
  it needs is the file. A CLI that exits 0 without writing one took the
  success branch and then died on the `cat`, letting a reporter fail the
  job the gate step is supposed to own.

The guard now runs every `run:` step in the action under GitHub's exact
interpreter, and `every_shell_step_is_exercised_here` fails if the
action grows a step the registry does not name — opting steps out
rather than in, so the next one cannot slip through the same way.
@perfectra1n
perfectra1n merged commit 046f7d7 into main Jul 31, 2026
16 checks passed
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.

domarinn-eval action: eval step aborts before writing its outputs, so the gate reports exit 3 for every failure

1 participant