Skip to content

Show dependency failure tree when command chain fails#296

Merged
kindermax merged 8 commits into
masterfrom
feature/dependency-failure-tree
Mar 14, 2026
Merged

Show dependency failure tree when command chain fails#296
kindermax merged 8 commits into
masterfrom
feature/dependency-failure-tree

Conversation

@kindermax
Copy link
Copy Markdown
Collaborator

@kindermax kindermax commented Mar 14, 2026

Summary

  • Adds a DependencyError type that carries the full depends chain when a command fails
  • When execution fails, prints an indented tree to stderr showing the chain with the failing command annotated in red (respects NO_COLOR)
  • Single-command failures (no depends) also show a one-node tree for consistency

Example output for deploy → build → lint (lint fails):
```
deploy
build
lint <-- failed here
ERRO[0000] failed to run command 'lint': exit status 1
```

Test Plan

  • Unit tests: go test ./internal/executor/ — covers chain construction, exit code propagation, tree rendering
  • Bats integration test: lets test-bats dependency_failure_tree — verifies 3-level chain and single-node cases with NO_COLOR=1
  • Manual: run a command with a failing depends chain and confirm tree appears above the error line

Summary by Sourcery

Add a dependency-aware error type and reporting so that failing commands display their full depends chain as an indented tree before the error message.

New Features:

  • Introduce a DependencyError type that carries the full command dependency chain for failed executions.
  • Render an indented dependency tree to stderr on command failure, highlighting the failing command and supporting single-command failures.

Enhancements:

  • Wrap executor errors with chain-building logic in both sequential and parallel execution paths to preserve dependency context.
  • Propagate exit codes from underlying execution errors through DependencyError to maintain correct process exit codes.

Documentation:

  • Document the new dependency failure tree behavior in the changelog and add a detailed design and implementation plan document.

Tests:

  • Add unit tests for dependency chain construction, exit code propagation, and tree rendering for DependencyError.
  • Add bats integration tests that verify the dependency failure tree output for multi-level and single-node failure scenarios.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Mar 14, 2026

Reviewer's Guide

Implements a new DependencyError abstraction to track command dependency chains on failure, wires executor paths to build this chain, and updates the CLI to render an indented dependency tree (with tests and docs) whenever a command or its depends chain fails.

Sequence diagram for dependency failure tree printing on command failure

sequenceDiagram
  actor User
  participant MainCLI
  participant RootCmd
  participant Executor
  participant DependencyHelpers

  User->>MainCLI: invoke lets deploy
  MainCLI->>RootCmd: ExecuteContext(ctx)
  RootCmd->>Executor: execute(ctx)

  Executor->>Executor: initCmd(ctx)
  alt initCmd fails
    Executor->>DependencyHelpers: prependToChain(deploy, err)
    DependencyHelpers-->>Executor: *DependencyError
    Executor-->>RootCmd: *DependencyError
  else initCmd ok
    Executor->>Executor: executeDepends(ctx)
    Executor->>Executor: runCmd(lint)
    Executor-->>Executor: *ExecuteError
    Executor->>DependencyHelpers: prependToChain(lint, err)
    DependencyHelpers-->>Executor: *DependencyError
    Executor-->>RootCmd: *DependencyError (chain deploy→build→lint)
  end

  RootCmd-->>MainCLI: *DependencyError

  MainCLI->>MainCLI: errors.As(err, *DependencyError)
  MainCLI->>DependencyHelpers: PrintDependencyTree(depErr, os.Stderr)
  DependencyHelpers-->>MainCLI: dependency_tree_rendered

  MainCLI->>MainCLI: log.Error(err.Error())
  MainCLI->>MainCLI: os.Exit(getExitCode(err, 1))
Loading

Class diagram for DependencyError and executor integration

classDiagram
class Executor {
  execute(ctx Context) error
  executeParallel(ctx Context) error
}

class DependencyError {
  []string Chain
  error Err
  Error() string
  ExitCode() int
}

class ExecuteError {
  ExitCode() int
}

class DependencyHelpers {
  prependToChain(name string, err error) error
  PrintDependencyTree(e *DependencyError, w io.Writer)
}

class MainCLI {
  main()
}

Executor ..> DependencyHelpers : uses
DependencyHelpers ..> DependencyError : constructs
DependencyError ..> ExecuteError : unwraps
MainCLI ..> DependencyHelpers : prints_tree_on_error
MainCLI ..> DependencyError : type_detection
Loading

File-Level Changes

Change Details Files
Introduce DependencyError type and helpers to accumulate command dependency chains and render a failure tree
  • Add DependencyError struct that wraps the original error and stores an outermost-first command chain plus ExitCode propagation
  • Implement prependToChain helper to either create a new DependencyError or prepend a command name to an existing chain
  • Add PrintDependencyTree to render the chain as an indented tree with the failing node annotated and respecting NO_COLOR
  • Add unit tests for chain construction, exit code propagation, and tree rendering behavior
internal/executor/dependency_error.go
internal/executor/dependency_error_test.go
Wire executor error paths to build dependency chains using DependencyError
  • Wrap execute() error returns from initCmd, executeDepends, runCmd loop, and persistChecksum using prependToChain
  • Wrap executeParallel() error returns from initCmd, executeDepends, errgroup.Wait, and persistChecksum using prependToChain
internal/executor/executor.go
Print dependency failure tree from CLI on command failure and ensure behavior is covered by integration tests and docs
  • Update main() to detect DependencyError with errors.As, print the dependency tree to stderr before logging the error, and keep exit code resolution unchanged
  • Add bats tests and a lets.yaml fixture to verify 3-level and single-node trees, with NO_COLOR to avoid ANSI codes in assertions
  • Document the feature in changelog and add a detailed design and implementation plan documents
cmd/lets/main.go
tests/dependency_failure_tree.bats
tests/dependency_failure_tree/lets.yaml
docs/docs/changelog.md
docs/specs/2026-03-13-dependency_failure_tree-design.md
docs/plans/2026-03-14-dependency-failure-tree.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 3 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="tests/dependency_failure_tree.bats" line_range="9-15" />
<code_context>
+    cd ./tests/dependency_failure_tree
+}
+
+@test "dependency_failure_tree: shows full 3-level chain on failure" {
+    run env NO_COLOR=1 lets deploy
+    assert_failure
+    assert_line --index 0 "  deploy"
+    assert_line --index 1 "    build"
+    assert_line --index 2 --partial "      lint"
+    assert_line --index 2 --partial "failed here"
+}
+
</code_context>
<issue_to_address>
**issue (testing):** These Bats assertions likely only inspect stdout, but the dependency tree is written to stderr

In `main.go` the tree is printed to stderr (`PrintDependencyTree(depErr, os.Stderr)`), but plain bats-core + bats-assert `run` puts stdout in `$output` and stderr in `$error`, and `assert_line` checks `$output` by default. Unless your helpers redirect stderr to stdout, these assertions won’t actually see the tree. You could instead assert on stderr (e.g. `assert_line --stderr ...` with bats-assert ≥ 2, or an equivalent helper), or redirect stderr in the test (e.g. `run env NO_COLOR=1 lets deploy 2>&1`) so the assertions target the correct stream.
</issue_to_address>

### Comment 2
<location path="tests/dependency_failure_tree.bats" line_range="18-22" />
<code_context>
+    assert_line --index 2 --partial "failed here"
+}
+
+@test "dependency_failure_tree: single node when no depends" {
+    run env NO_COLOR=1 lets lint
+    assert_failure
+    assert_line --index 0 --partial "  lint"
+    assert_line --index 0 --partial "failed here"
+}
+```
</code_context>
<issue_to_address>
**suggestion (testing):** No assertion that NO_COLOR actually suppresses ANSI color codes in the dependency tree output

The tests run with `NO_COLOR=1` but only check for text like `failed here`, so they wouldn’t fail if ANSI escapes were accidentally reintroduced. Add an assertion that the captured output does *not* contain `�[` (or a specific color code), e.g. via `refute_line --regex $'\x1b\['`, so the test explicitly verifies the tree is uncolored when `NO_COLOR` is set.

Suggested implementation:

```shell
@test "dependency_failure_tree: shows full 3-level chain on failure" {
    run env NO_COLOR=1 lets deploy
    assert_failure
    assert_line --index 0 "  deploy"
    assert_line --index 1 "    build"
    assert_line --index 2 --partial "      lint"
    assert_line --index 2 --partial "failed here"
    refute_output --regex $'\x1b\['
}

```

```shell
@test "dependency_failure_tree: single node when no depends" {
    run env NO_COLOR=1 lets lint
    assert_failure
    assert_line --index 0 --partial "  lint"
    assert_line --index 0 --partial "failed here"
    refute_output --regex $'\x1b\['
}

```
</issue_to_address>

### Comment 3
<location path="tests/dependency_failure_tree.bats" line_range="10-15" />
<code_context>
+}
+
+@test "dependency_failure_tree: shows full 3-level chain on failure" {
+    run env NO_COLOR=1 lets deploy
+    assert_failure
+    assert_line --index 0 "  deploy"
+    assert_line --index 1 "    build"
+    assert_line --index 2 --partial "      lint"
+    assert_line --index 2 --partial "failed here"
+}
+
</code_context>
<issue_to_address>
**suggestion (testing):** Consider asserting that the dependency tree appears before the error log line to match the documented behavior

The PR description says the tree should appear *above* the error line (e.g., `ERRO[...] failed to run command ...`), but this test only verifies the tree content, not its position relative to the error. To cover the UX requirement, consider asserting that the initial lines are the tree and that any `ERRO` (or command error) line comes after them.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +9 to +15
@test "dependency_failure_tree: shows full 3-level chain on failure" {
run env NO_COLOR=1 lets deploy
assert_failure
assert_line --index 0 " deploy"
assert_line --index 1 " build"
assert_line --index 2 --partial " lint"
assert_line --index 2 --partial "failed here"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (testing): These Bats assertions likely only inspect stdout, but the dependency tree is written to stderr

In main.go the tree is printed to stderr (PrintDependencyTree(depErr, os.Stderr)), but plain bats-core + bats-assert run puts stdout in $output and stderr in $error, and assert_line checks $output by default. Unless your helpers redirect stderr to stdout, these assertions won’t actually see the tree. You could instead assert on stderr (e.g. assert_line --stderr ... with bats-assert ≥ 2, or an equivalent helper), or redirect stderr in the test (e.g. run env NO_COLOR=1 lets deploy 2>&1) so the assertions target the correct stream.

Comment on lines +18 to +22
@test "dependency_failure_tree: single node when no depends" {
run env NO_COLOR=1 lets lint
assert_failure
assert_line --index 0 --partial " lint"
assert_line --index 0 --partial "failed here"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): No assertion that NO_COLOR actually suppresses ANSI color codes in the dependency tree output

The tests run with NO_COLOR=1 but only check for text like failed here, so they wouldn’t fail if ANSI escapes were accidentally reintroduced. Add an assertion that the captured output does not contain �[ (or a specific color code), e.g. via refute_line --regex $'\x1b\[', so the test explicitly verifies the tree is uncolored when NO_COLOR is set.

Suggested implementation:

@test "dependency_failure_tree: shows full 3-level chain on failure" {
    run env NO_COLOR=1 lets deploy
    assert_failure
    assert_line --index 0 "  deploy"
    assert_line --index 1 "    build"
    assert_line --index 2 --partial "      lint"
    assert_line --index 2 --partial "failed here"
    refute_output --regex $'\x1b\['
}
@test "dependency_failure_tree: single node when no depends" {
    run env NO_COLOR=1 lets lint
    assert_failure
    assert_line --index 0 --partial "  lint"
    assert_line --index 0 --partial "failed here"
    refute_output --regex $'\x1b\['
}

Comment on lines +10 to +15
run env NO_COLOR=1 lets deploy
assert_failure
assert_line --index 0 " deploy"
assert_line --index 1 " build"
assert_line --index 2 --partial " lint"
assert_line --index 2 --partial "failed here"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Consider asserting that the dependency tree appears before the error log line to match the documented behavior

The PR description says the tree should appear above the error line (e.g., ERRO[...] failed to run command ...), but this test only verifies the tree content, not its position relative to the error. To cover the UX requirement, consider asserting that the initial lines are the tree and that any ERRO (or command error) line comes after them.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 14, 2026

Greptile Summary

This PR adds a DependencyError type and a tree-rendering function to internal/executor so that when a lets command (or any of its depends) fails, an indented dependency chain is printed to stderr before the error log line — making it immediately clear where in a multi-level dependency chain execution stopped.

Key changes:

  • internal/executor/dependency_error.go (new): DependencyError struct carrying an outermost-first Chain []string and the root cause Err error; prependToChain helper that builds the chain bottom-up as errors bubble through execute/executeParallel; PrintDependencyTree renderer using fatih/color (already a dependency) for the red <-- failed here annotation and automatic NO_COLOR support.
  • internal/executor/executor.go: Every error return path in both execute() and executeParallel() is now wrapped with prependToChain, replacing the bare return err pattern. This is the right level to intercept — each recursive call adds its command name to the front, building the chain without any global state.
  • cmd/lets/main.go: After rootCmd.ExecuteContext fails, errors.As is used to detect a *DependencyError and call PrintDependencyTree before the existing log.Error and os.Exit calls. getExitCode already works correctly since DependencyError implements ExitCode() int.
  • Tests: Unit tests cover chain construction, exit-code propagation (including a real exec.ExitError with exit code 2), and tree rendering; bats integration tests cover the 3-level chain and single-node case with NO_COLOR=1.

The main code-quality gap is that DependencyError does not implement Unwrap() error, which is the Go convention for error-wrapping types and is needed for errors.Is/errors.As to traverse through it in the future.

Confidence Score: 4/5

  • This PR is safe to merge; the logic is sound and well-tested, with only a minor Go best-practice gap (missing Unwrap) to address.
  • The implementation is clean and mechanically correct: chain construction is bottom-up, rendering matches the spec, exit-code propagation is preserved, and both unit and integration tests provide solid coverage. The one actionable gap — missing Unwrap() on DependencyError — does not break any current code path but violates Go error-wrapping conventions and could silently break future errors.Is/errors.As usage. Everything else, including the bats test pattern, is consistent with the rest of the repository.
  • internal/executor/dependency_error.go — add Unwrap() before merging to keep the error chain transparent.

Important Files Changed

Filename Overview
internal/executor/dependency_error.go New file introducing DependencyError, prependToChain, and PrintDependencyTree. Core logic is correct; missing Unwrap() is a best-practice gap.
internal/executor/dependency_error_test.go Good unit test coverage for chain construction, exit code propagation (including real exec.ExitError), and tree rendering with and without color.
internal/executor/executor.go All error return paths in execute() and executeParallel() now call prependToChain; changes are minimal and mechanical with no regressions introduced.
cmd/lets/main.go Tree is printed to stderr before log.Error, correctly ordered. errors.As and getExitCode work correctly since DependencyError implements ExitCode().
tests/dependency_failure_tree.bats Integration tests cover 3-level chain and single-node cases with NO_COLOR=1. Uses the same cd ./tests/... convention as all other bats tests in the project.
tests/dependency_failure_tree/lets.yaml Simple fixture for the 3-level chain (deploy → build → lint fails). No issues.

Sequence Diagram

sequenceDiagram
    participant main
    participant Executor
    participant execute_deploy as execute(deploy)
    participant execute_build as execute(build)
    participant execute_lint as execute(lint)

    main->>Executor: Execute(deploy)
    Executor->>execute_deploy: execute(deploy)
    execute_deploy->>Executor: executeDepends → Execute(build)
    Executor->>execute_build: execute(build)
    execute_build->>Executor: executeDepends → Execute(lint)
    Executor->>execute_lint: execute(lint)
    execute_lint->>execute_lint: runCmd → ExecuteError
    execute_lint-->>execute_build: prependToChain("lint", ExecuteError)<br/>→ DependencyError{Chain:["lint"]}
    execute_build-->>execute_deploy: prependToChain("build", DependencyError)<br/>→ DependencyError{Chain:["build","lint"]}
    execute_deploy-->>main: prependToChain("deploy", DependencyError)<br/>→ DependencyError{Chain:["deploy","build","lint"]}
    main->>main: errors.As → PrintDependencyTree(stderr)
    Note over main: "  deploy\n    build\n      lint  <-- failed here"
    main->>main: log.Error + os.Exit(exitCode)
Loading

Last reviewed commit: 643262e

Comment thread internal/executor/dependency_error.go
Comment thread internal/executor/executor.go
@kindermax kindermax force-pushed the feature/dependency-failure-tree branch 3 times, most recently from bdd0fb3 to 31003dc Compare March 14, 2026 16:50
Fix spec: clarify chain construction and single-node failure handling

Clarify error path coverage, executeParallel, global init, and NO_COLOR in bats test

Update spec status to ready for implementation

Add dependency failure tree implementation plan
- prependToChain now returns a new DependencyError instead of mutating
  the original, making it safe for concurrent use
- Rename misleading test "wraps ExecuteError exit code" to accurately
  reflect it tests the fallback-to-1 path
- Tighten single-node PrintDependencyTree assertion to use HasPrefix
  on a split line rather than Contains
@kindermax kindermax force-pushed the feature/dependency-failure-tree branch 3 times, most recently from df4f145 to 6d752bd Compare March 14, 2026 17:14
@kindermax kindermax force-pushed the feature/dependency-failure-tree branch from 6d752bd to bb3d7e4 Compare March 14, 2026 17:18
@kindermax kindermax merged commit 072d796 into master Mar 14, 2026
5 checks passed
@kindermax kindermax deleted the feature/dependency-failure-tree branch March 14, 2026 17:20
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