Skip to content

fix(runner): do not group a single task's output in CI - #448

Merged
jahvon merged 1 commit into
mainfrom
fix/ci-log-group-single-task
Aug 27, 2026
Merged

fix(runner): do not group a single task's output in CI#448
jahvon merged 1 commit into
mainfrom
fix/ci-log-group-single-task

Conversation

@jahvon

@jahvon jahvon commented Aug 27, 2026

Copy link
Copy Markdown
Member

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:

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

🤖 Generated with Claude Code

https://claude.ai/code/session_01R328pa3FUUfga4gYah1iQi

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 offer no way to
default one open, so a single-task run hid the only output worth reading behind
a click while gaining no structure — `flow test unit` showed a group header and
nothing else until expanded.

The group is now emitted only when there is more than one task. 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 as well
as the version that hardens it (flowexec/tuikit#113).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R328pa3FUUfga4gYah1iQi
@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 50.00000% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/runner/parallel/parallel.go 50.00% 2 Missing ⚠️
internal/runner/serial/serial.go 50.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

jahvon added a commit to flowexec/action that referenced this pull request Aug 27, 2026
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.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 that referenced this pull request Aug 27, 2026
Consolidates the three open dependabot PRs into one, plus a tuikit bump
dependabot cannot open on its own.

# Summary

**Dependency bumps** (taken as-is from dependabot — closes #434, closes
#435, closes #437):

| Dependency | From | To |
|---|---|---|
| `go.etcd.io/bbolt` | 1.4.3 | 1.5.0 |
| `github/codeql-action` | 4.37.3 | 4.37.4 |
| `postcss` (in `/docs`) | 8.5.12 | 8.5.25 |

**`github.com/flowexec/tuikit` 0.4.1 → 0.4.2** —
[released](https://github.com/flowexec/tuikit/releases/tag/v0.4.2) for
this change, so dependabot had nothing to open a PR against. It carries
two logging fixes:

- `::group::` was emitted whenever `CI=true`. It is GitHub syntax, so
every other provider (GitLab, CircleCI, Buildkite) printed literal
`::group::name` lines instead of a header. Now gated on
`GITHUB_ACTIONS`; other providers get the styled header that local runs
already got.
- Nested groups and unmatched `EndGroup` calls could emit invalid output
that swallowed or truncated the surrounding log — GitHub does not
support nested groups, and a stray `::endgroup::` closes whatever it had
open.

# Testing

`flow validate` passes — generate, lint, unit + e2e, generated-diff,
schema validation.

**bbolt backs `pkg/store`**, so the suite was run against it rather than
taking a minor bump on the datastore on trust. `pkg/store` and
`pkg/cache` both pass.

**The tuikit fix was verified through this build**, not just assumed
from the version number:

```
$ CI=true GITHUB_ACTIONS=true flow exec --spec '...serial with 2 steps...'
::group::run flow/many
::endgroup::

$ CI=true flow exec --spec '...same...'
(no literal workflow commands — was leaking them at v0.4.1)
```

# Related

#448 stops flow from wrapping a *single* task's output in a
group at all, which is the other half of making CI logs readable. It is
independent of this PR — this one only moves the dependency.

🤖 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>
@jahvon
jahvon merged commit e72c059 into main Aug 27, 2026
15 checks passed
@jahvon
jahvon deleted the fix/ci-log-group-single-task branch August 27, 2026 05:37
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