Skip to content

fix: surface failure output instead of discarding it - #3

Merged
jahvon merged 1 commit into
mainfrom
fix/log-output-visibility
Aug 27, 2026
Merged

fix: surface failure output instead of discarding it#3
jahvon merged 1 commit into
mainfrom
fix/log-output-visibility

Conversation

@jahvon

@jahvon jahvon commented Aug 27, 2026

Copy link
Copy Markdown
Member

Companion to flowexec/tuikit#113 and flowexec/flow#448, which fix the log grouping this works around.

Summary

When a flow executable failed, the reason was hard or impossible to find from the run page. Three changes:

1. stderr was thrown away. execute.sh captured it to a temp file, mined it for a JSON error code, and rm -f'd it without ever printing it. Anything flow wrote to stderr was lost outright — most importantly the structured error envelope under --output json, whose message never reached the log. It is now echoed through.

2. The annotation reported only an exit code. It now carries the error code and message when flow emits a structured envelope:

::error::flow test unit failed with exit code 1 (EXECUTION_FAILED): unable to parse file - 14:1

Newlines are folded to spaces, since a literal newline ends a workflow command early.

3. A step summary is written for both outcomes.

flow test unit

Exit code 1 · EXECUTION_FAILED

unable to parse file - 14:1

Why this matters

flow wraps a serial/parallel run's output in a GitHub log group, and groups are always collapsed — GitHub offers no way to default one open. So the actual output sits one click away, and on failure there was nothing outside the group saying what went wrong. Annotations and the step summary render on the run page itself, where a collapsed group does not.

flowexec/tuikit#113 and flowexec/flow#448 stop burying single-task output in a group in the first place. These changes are complementary: they make failures visible regardless of how the output is grouped.

Testing

The action has no CI workflows to add cases to, so I exercised execute.sh directly against a stand-in flow binary, checking GITHUB_OUTPUT and GITHUB_STEP_SUMMARY:

Case Result
Success Clean ✅ summary, no annotation, exit 0
Failure with JSON envelope stderr printed; annotation carries code + message; summary shows both; error-code output still set
Failure with plain multi-line stderr stderr printed (previously lost entirely); annotation stays single-line; summary shows exit code

bash -n scripts/execute.sh is clean. The capture/upload path and the continue-on-error path are unchanged.

🤖 Generated with Claude Code

https://claude.ai/code/session_01R328pa3FUUfga4gYah1iQi

Three changes, all aimed at the same problem: when a flow executable failed,
the reason was hard or impossible to find from the run page.

stderr was captured to a temp file, mined for a JSON error code, and deleted
without ever being printed. Anything flow wrote there was lost outright — most
importantly the structured error envelope under --output json, whose message
never reached the log. It is now echoed through.

The failure annotation reported only an exit code. It now carries the error
code and message when flow emits a structured envelope, with newlines folded to
spaces so the workflow command cannot be terminated early.

A step summary is written for both outcomes. flow collapses a multi-task run's
output into a GitHub log group, so without this a reader had to expand the log
just to learn whether the step passed; annotations and the summary render on the
run page, where a collapsed group does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R328pa3FUUfga4gYah1iQi
@jahvon
jahvon merged commit 5c82287 into main Aug 27, 2026
@jahvon
jahvon deleted the fix/log-output-visibility branch August 27, 2026 05:05
jahvon added a commit to flowexec/tuikit that referenced this pull request Aug 27, 2026
Part of making flow's CI logs readable — see flowexec/flow#448 and flowexec/action#3.

# Summary

Three problems with CI log grouping, all of which put output where a reader cannot see it.

**1. Workflow commands were emitted on every CI, not just GitHub.** `isCI()` is true when `CI=true`, so GitLab, CircleCI, Buildkite and friends printed literal `::group::build app` lines instead of a header. `::group::` is GitHub syntax, not a CI convention — it is now gated on `GITHUB_ACTIONS`, and every other provider gets the styled header that local runs already got. `isCI()` had no other callers, so it is removed rather than left dead.

**2. Nested groups produced invalid output.** GitHub does not support nesting: a second `::group::` before the first closes swallows the rest of the log. Depth is now tracked so only the outermost pair is emitted, and a nested caller degrades to no group rather than a broken one.

**3. An unmatched `EndGroup` emitted a stray `::endgroup::`**, which closes whatever GitHub had open around it. It is now a no-op.

Group names are also escaped per GitHub's spec — an unescaped newline previously ended the command early and leaked the remainder as ordinary log lines.
jahvon added a commit to flowexec/flow that referenced this pull request Aug 27, 2026
Companion to flowexec/tuikit#113 and flowexec/action#3.

# Summary

A serial or parallel executable wrapped its whole run in a GitHub log
group named after the ref. GitHub log groups are **always collapsed**,
and there is no way to default one open — so a single-task run hid the
only output worth reading behind a click, gaining no structure in
exchange.

From a real CI run of `test unit`:

```
##[endgroup]
Executing: flow test unit --param CI=true
##[group]test flow/unit          ← everything below is collapsed
ok  github.com/flowexec/flow/v2/cmd/internal  1.271s
...
```

`test unit` is a serial executable with one step, so the group had
exactly one child.

# Change

Emit the group only when there is more than one task:

```go
groupOutput := parentTask == nil && len(execs) > 1
```

`EndGroup` is guarded by the same condition rather than relying on the
logger to ignore an unmatched call, so this is correct against the
currently pinned tuikit (v0.4.1) as well as the version that hardens it.

# Verified

Built and run with `CI=true GITHUB_ACTIONS=true`:

| Case | Before | After |
|---|---|---|
| `flow test unit` (1 step) | `::group::test flow/unit` — all output
collapsed | no group, output visible |
| serial, 2 steps | grouped | `::group::run flow/many` … `::endgroup::`
— unchanged |
| parallel, 1 step | grouped | no group |
| parallel, 2+ steps | grouped | unchanged |

`flow validate` passes.

# Related

- flowexec/tuikit#113 — stops emitting GitHub-only `::group::` syntax on
other CI providers, prevents invalid nested groups, and makes an
unmatched `EndGroup` a no-op. Independent of this PR, but it is what
makes the guard above belt-and-braces rather than load-bearing.
- flowexec/action#3 — prints the stderr the action was discarding, and
adds a step summary plus a real error message in the failure annotation,
so a failure is visible on the run page without expanding any log at
all.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01R328pa3FUUfga4gYah1iQi

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant