Skip to content

fix(core): support skipped batch tasks end-to-end and fix TUI double logs#35617

Merged
FrozenPandaz merged 17 commits into
masterfrom
fix-tui
May 8, 2026
Merged

fix(core): support skipped batch tasks end-to-end and fix TUI double logs#35617
FrozenPandaz merged 17 commits into
masterfrom
fix-tui

Conversation

@FrozenPandaz
Copy link
Copy Markdown
Contributor

@FrozenPandaz FrozenPandaz commented May 8, 2026

Current Behavior

When running a batch executor (Gradle or Maven) under the TUI:

  1. Double logs. Each task's captured terminalOutput is written into the per-task PTY twice — printTaskTerminalOutput lazily creates the PTY with the terminalOutput, then appendTaskOutput immediately writes the same content again, producing repeated text in the pane (e.g. > Task :foo:bar UP-TO-DATE> Task :foo:bar UP-TO-DATE).
  2. Generic failure on dependents. When one task in the batch fails, Gradle aborts and the JVM exits non-zero. Every peer that never got to run is yielded as success: false with a generic Gradlew batch failed (or Maven batch runner exited with code N) message — both in the streaming output and in every dependent task's TUI pane. The actual root failure is hidden in noise. Same applies for Maven.
  3. Run reported as "Cancelled". Skipped peers were never marked as completed in the lifecycle, so the in-progress set stayed non-empty and the run summary printed Cancelled even though the failure was real.
  4. Misleading > nx run X headers in run-one. Tasks that never actually ran still got a header printed in the streaming output, suggesting they were executed.

Expected Behavior

  1. The captured terminalOutput for a batch task is written to its PTY exactly once.
  2. Peers that never ran because a sibling failed are reported with status: 'skipped' and empty terminal output. The actual failed task keeps its full error in its own pane (✖). Dependents show as ⏭ in the task list with empty panes — the user navigates to the failed task to see why.
  3. The run summary correctly shows Ran target build … N/M failed rather than Cancelled.
  4. Skipped tasks are no longer printed in the run-one streaming summary.

Implementation

This PR changes the batch executor protocol so the Kotlin batch runners are the source of truth for per-task outcomes — the TS executors are a thin relay instead of inferring missing results.

Wire protocol

TaskResult gains an optional status?: 'success' | 'failure' | 'skipped' field. When set, the orchestrator and lifecycle honor it instead of inferring from success: boolean. Existing batch executors that don't emit status are unaffected — the orchestrator falls back to the boolean.

NX_RESULT:{json} lines now carry status alongside success for back-compat with older Nx versions.

Kotlin runners (Gradle and Maven)

  • Track requested vs reported task IDs.
  • At end-of-batch, walk the requested set and emit an explicit skipped NX_RESULT for any task without a finish event (e.g. a peer compilation failed and the build aborted before this task could be scheduled).
  • For Gradle: applied to both runBuildLauncher and runTestLauncher.
  • For Maven: the work-stealing scheduler already tracked TaskState.SKIPPED for tasks removed due to a failed dependency — it now emits an NX_RESULT for each instead of silently dropping them.

TS executors (gradle-batch.impl.ts, maven-batch.impl.ts)

Become thin relays:

  • Parse NX_RESULT, pass status through.
  • Only fallback path: if the runner crashes (non-zero exit) before reporting on every task, backfill the missing ones with a generic failure so Nx doesn't hang.
  • The previous sawFailure-based inference is gone — it was fragile (a regression in this PR's CI revealed that Maven's stderr can interleave concurrent task output and stash one task's NX_RESULT inside another's terminalOutput string, defeating the inference).

Nx core (orchestrator + TUI lifecycle)

  • runBatch's onTaskResults honors result.status ?? (success ? 'success' : 'failure').
  • The double-logs fix is a one-line ordering swap so appendTaskOutput runs before printTaskTerminalOutput — the latter then no-ops in the TUI because the PTY is already populated.
  • TUI summary lifecycle clears skipped tasks from inProgressTasks on setTaskStatus(Skipped) (so the run summary doesn't say "Cancelled"), and skips them in printRunOneSummary (so we don't print a misleading > nx run X header for tasks that never ran).

Tests

  • tui-summary-life-cycle.spec.ts: snapshot test that a Skipped task does not print a > nx run header and the run summary reports as a real failure (not cancelled).
  • gradle-batch.impl.spec.ts and maven-batch.impl.spec.ts: spawn-mocked tests verify the executor relays status: 'skipped' from the runner unchanged, and backfills as failure only when the runner crashes before reporting.

Related Issue(s)

Fixes NXC-4439 (Linear) — Show root Gradle failure for dependent tasks in TUI.

Fixes NXC-4449 (Linear) — TUI shows duplicate terminal output for batch tasks.

- Swap appendTaskOutput / printTaskTerminalOutput order in the batch
  result handler so the second call no-ops in the TUI instead of
  writing the same terminalOutput twice into the per-task PTY.
- Add optional `status` to TaskResult so batch executors can mark
  unyielded peers as `skipped` rather than `failure` when they never
  got to run because a sibling failed.
- Clear skipped tasks from the in-progress set in tui-summary so a
  failed batch isn't reported as `Cancelled`.
- Skip skipped tasks in the run-one summary so we don't print a
  misleading `> nx run <task>` header for tasks that never ran.
When a Gradle task fails (or the batch runner exits non-zero), the
peers that never got to run are now reported with `status: 'skipped'`
and an empty terminalOutput. Previously they all surfaced with a
generic `Gradlew batch failed` message and a `failure` status, which
hid the actual root cause and noisily duplicated the same line in
every dependent task's pane.

Fixes NXC-4439.
Mirrors the Gradle batch executor: when a Maven task fails or the
batch runner exits non-zero, peers that never got to run are now
reported with `status: 'skipped'` and an empty terminalOutput
instead of `failure` with a generic error message.
@FrozenPandaz FrozenPandaz requested a review from a team as a code owner May 8, 2026 02:20
@FrozenPandaz FrozenPandaz requested a review from leosvelperez May 8, 2026 02:20
@netlify
Copy link
Copy Markdown

netlify Bot commented May 8, 2026

Deploy Preview for nx-dev ready!

Name Link
🔨 Latest commit f629f37
🔍 Latest deploy log https://app.netlify.com/projects/nx-dev/deploys/69fe2b3e7476fa00070909cb
😎 Deploy Preview https://deploy-preview-35617--nx-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link
Copy Markdown

netlify Bot commented May 8, 2026

Deploy Preview for nx-docs ready!

Name Link
🔨 Latest commit f629f37
🔍 Latest deploy log https://app.netlify.com/projects/nx-docs/deploys/69fe2b3e75268a00086d7bc3
😎 Deploy Preview https://deploy-preview-35617--nx-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@nx-cloud
Copy link
Copy Markdown
Contributor

nx-cloud Bot commented May 8, 2026

View your CI Pipeline Execution ↗ for commit d418f5a

Command Status Duration Result
nx affected --targets=lint,test,build,e2e,e2e-c... ✅ Succeeded 15m 14s View ↗
nx run-many -t check-imports check-lock-files c... ✅ Succeeded 3s View ↗
nx-cloud record -- pnpm nx-cloud conformance:check ✅ Succeeded 17s View ↗
nx build workspace-plugin ✅ Succeeded <1s View ↗
nx-cloud record -- nx sync:check ✅ Succeeded 24s View ↗
nx-cloud record -- nx format:check ✅ Succeeded 7s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-08 18:47:01 UTC

Adds tests that mock the batch-runner JAR's spawn and verify:
- When a sibling reports failure and the runner exits non-zero,
  unyielded peers are emitted with `status: 'skipped'` and an
  empty terminalOutput.
- When the runner exits non-zero with no per-task results, all
  peers fall back to `failure` (no skipped status), since there
  is no observed sibling failure to attribute the skip to.
- Successful results pass through unchanged.

Equivalent tests added for the Maven batch executor.
ResultEmitter is the protocol boundary between the Kotlin batch
runner and the TypeScript executor — these tests pin the on-the-wire
shape that the TS-side spec depends on:

- emits a single NX_RESULT line per call with the expected JSON
- dedupes repeated emissions for the same task id
- handles distinct task ids independently
- terminates each emission with a newline so readline can split
nx-cloud[bot]

This comment was marked as outdated.

Reverts 39f7401. These tests pinned the NX_RESULT protocol shape
but didn't exercise any code changed in this PR — keeping them only
as scaffolding wasn't worth the maintenance surface.
CI surfaced a regression in the maven batch e2e test: the previous
post-loop fallback yielded a `failure` result for any unyielded task,
which prevented Nx's task-orchestrator from retrying tasks that the
batch runner failed to report on.

The Maven batch runner's stderr can interleave concurrent task output
such that one task's NX_RESULT line ends up captured inside another
task's terminalOutput string, leaving the inner task unyielded on the
TS side. With a clean exit code, Nx's retry loop is the right
recovery path — yielding a fallback failure here turns a transient
parser miss into a permanent failure.

The catch-block fallback (for non-zero exit codes) is preserved so
real maven failures still get the skipped/failed peer treatment.
nx-cloud[bot]

This comment was marked as outdated.

nx-cloud[bot]

This comment was marked as outdated.

Instead of inferring skipped status on the TS side after a sibling
fails, the Kotlin batch runner now emits an explicit
`status: "skipped"` NX_RESULT for any requested Nx task that Gradle
never reached.

- TaskResult gains a `status` field; ResultEmitter writes it on the
  wire alongside `success` for back-compat.
- runBuildLauncher and runTestLauncher walk the requested task set at
  end-of-batch and emit `skipped` for any task without a TaskFinishEvent
  (e.g. a peer compilation failed and the build aborted before this
  task could be scheduled).
- gradle-batch.impl.ts is now a thin relay: parse NX_RESULT (passing
  through `status` if present), yield. The only TS-side fallback is a
  generic crash recovery path that fires when the runner exits before
  reporting on every task.

This eliminates a class of TS-side inference bugs (the `sawFailure`
heuristic was fragile when the runner emitted partial output) and
mirrors the Maven runner's existing skipped-task tracking.
Mirrors the Gradle runner change. The Maven batch runner already
tracked TaskState.SKIPPED for tasks whose dependency failed, but
silently dropped them — only success/failure tasks were emitted as
NX_RESULT, leaving the TS side to infer skipped peers.

- TaskResult gains a `status` field surfaced on the wire.
- The work-stealing scheduler's skipped-task branch now emits an
  explicit `skipped` NX_RESULT (with empty terminalOutput) for each
  task removed due to a failed dependency.
- maven-batch.impl.ts becomes a thin relay: parses NX_RESULT, passes
  `status` through, no inference. The only TS-side fallback is a
  generic crash recovery for non-zero exits with no per-task results.

The previous TS-side `sawFailure` inference combined with a post-loop
fallback was the source of a CI regression: if the runner's stderr
interleaved concurrent task output and one task's NX_RESULT got
captured inside another's terminalOutput, the missing task got marked
as a permanent failure instead of letting Nx retry. With the runner
now authoritative, that inference path goes away entirely.
@FrozenPandaz FrozenPandaz changed the title fix(core): mark batch peers as skipped, fix double logs in TUI fix(core): support skipped batch tasks end-to-end and fix TUI double logs May 8, 2026
Drop the silent default on `TaskResult.status` and add `success`,
`failure`, `skipped`, `fromBoolean` factory functions. Every call
site is now explicit about the outcome it represents, and the TS
side can drop the `data.result.status ?? …` defensive fallback —
the runner always sends `status` on the wire.

Same shape for the Maven runner.

Also tightens stale comments throughout the diff.
Apply review cleanup:

- Extract `resolveBatchTaskStatus` helper in task-orchestrator instead
  of repeating `result.status ?? (result.success ? 'success' : 'failure')`
  in two spots.
- Skip `printTaskTerminalOutput` for skipped batch tasks with no
  terminal output — otherwise the TUI allocates a per-task PTY just to
  write a cursor-hide escape, which scales linearly with skipped peers
  in large batches.
- Move maven `emitResult` for cascaded skips out of the graph
  `synchronized` block — JSON serialization and stderr I/O don't need
  the graph monitor and would otherwise stall every other worker on
  cascading failures. Skipped-task results are collected locally and
  emitted after the lock is released.
- Drop the redundant "Kotlin runner emits NX_RESULT" preamble comment
  in both TS executor files.
- Drop `= 0` defaults from Maven `TaskResult` factory startTime/endTime
  params — every real call site passes both, and the default would
  silently produce zero-duration tasks.
- Reword the `TaskResult.status` doc-comment to call out that it's the
  contract for batch executors, not an opt-in escape hatch.
nx-cloud[bot]

This comment was marked as outdated.

nx-cloud Bot and others added 4 commits May 8, 2026 14:13
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
…Y state

The TS-side ordering swap that fixed double-logged batch terminal
output also moved batch tasks from the no-PTY branch of
`print_task_terminal_output` (which appended `\x1b[?25l`) to the
PTY-exists branch (which previously returned early). Finished batch
panes ended up showing a blinking virtual cursor at the end of the
captured output.

Make `print_task_terminal_output` emit the cursor-hide escape on
both branches: the no-PTY branch keeps writing `output + cursor-hide`
when it creates the PTY, and the PTY-exists branch now writes only
the cursor-hide. The escape is idempotent so it's safe regardless of
how the PTY's content got there.

Two unit tests pin the contract — verified to fail without the fix:
- batch-style flow (append creates the PTY, print finalizes)
- cache-hit flow (print creates the PTY)
Pins the negative half of the streaming/finalize contract: while
chunks are still arriving via append_task_output, the cursor must
remain visible. Only print_task_terminal_output (called at end of
life) hides the cursor.
nx-cloud[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@nx-cloud nx-cloud Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nx Cloud has identified a flaky task in your failed CI:

🔂 Since the failure was identified as flaky, we triggered a CI rerun by adding an empty commit to this branch.

Nx Cloud View detailed reasoning in Nx Cloud ↗


🎓 Learn more about Self-Healing CI on nx.dev

@FrozenPandaz FrozenPandaz merged commit 9f8a6c2 into master May 8, 2026
18 checks passed
@FrozenPandaz FrozenPandaz deleted the fix-tui branch May 8, 2026 20:28
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.

2 participants