Conversation
📝 WalkthroughWalkthroughStep command model moved from single scalar fields to a Commands []CommandEntry slice across core, spec, runtime, executors, API, CLI, UI, and tests; an executor capabilities registry was added and executors now declare supported features. Changes
Sequence Diagram(s)mermaid Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/core/spec/step.go (1)
683-717: Add "gha" to thesingleCommandOnlyExecutorsrestriction map.The "gha" executor type is registered in
internal/runtime/builtin/gha/executor.go:550alongside "github_action" and "github-action", but it is missing from thesingleCommandOnlyExecutorsmap. The gha implementation (lines 210-217) only extracts the first command (Commands[0]), matching the single-command-only behavior of its github_action counterpart. Without this entry, DAG files usingexecutor: ghawith multiple commands would not be caught by the validation check.Registered executors verified:
- Single-command only (in map): jq, http, archive, github_action, github-action, mail, dag, parallel
- Single-command only (missing): gha
- Multiple-command capable (not in map): shell, command, docker, container, ssh, "" (empty)
🧹 Nitpick comments (7)
internal/runtime/builtin/gha/executor.go (1)
210-217: Consider extracting duplicated command extraction logic.The command extraction and validation logic is duplicated between
generateWorkflowYAML(lines 210-217) andnewGitHubAction(lines 531-538). Consider extracting this into a helper method to follow the DRY principle.Additionally, since the GitHub Action executor semantically supports only a single action per step, you might validate that
len(step.Commands) == 1to catch configuration errors where users accidentally provide multiple commands.🔎 Proposed refactor to extract command extraction
Add a helper method:
// extractGHACommand extracts and validates the command for GitHub Action execution. // It returns an error if no command is provided or if the command is empty. func extractGHACommand(step core.Step) (string, error) { if len(step.Commands) == 0 { return "", fmt.Errorf("command field is required for GitHub Action executor (e.g., command: actions/checkout@v4)") } command := step.Commands[0].Command if command == "" { return "", fmt.Errorf("command field is required for GitHub Action executor (e.g., command: actions/checkout@v4)") } // Optional: Validate single command if len(step.Commands) > 1 { return "", fmt.Errorf("GitHub Action executor supports only a single command, got %d", len(step.Commands)) } return command, nil }Then update both locations:
func (e *githubAction) generateWorkflowYAML(ctx context.Context) (string, error) { - // Extract command from Commands field - var command string - if len(e.step.Commands) > 0 { - command = e.step.Commands[0].Command - } - if command == "" { - return "", fmt.Errorf("command field is required for GitHub Action executor (e.g., command: actions/checkout@v4)") + command, err := extractGHACommand(e.step) + if err != nil { + return "", err }func newGitHubAction(ctx context.Context, step core.Step) (executor.Executor, error) { - // Extract command from Commands field - var command string - if len(step.Commands) > 0 { - command = step.Commands[0].Command - } - if command == "" { - return nil, fmt.Errorf("command field is required for GitHub Action executor (e.g., command: actions/checkout@v4)") + _, err := extractGHACommand(step) + if err != nil { + return nil, err }Also applies to: 531-538
internal/runtime/builtin/jq/jq.go (1)
43-48: Consider validating non-empty query during executor construction.The query is extracted from
Commands[0].CmdWithArgswith a guard for empty Commands, but there's no validation that the query is non-empty. An empty query will causegojq.Parse(e.query)to fail at runtime in theRun()method.Suggested validation
// Extract query from Commands field var query string if len(step.Commands) > 0 { query = step.Commands[0].CmdWithArgs } + +if query == "" { + return nil, fmt.Errorf("jq query is required") +} return &jq{This provides clearer error messaging during setup rather than a cryptic parsing error during execution.
internal/runtime/builtin/http/http.go (1)
63-71: Consider validating required HTTP method and URL during construction.The HTTP executor extracts
methodandurlfromCommands[0]with proper guards, but doesn't validate they're non-empty:var method string var url string if len(step.Commands) > 0 { method = step.Commands[0].Command if len(step.Commands[0].Args) > 0 { url = step.Commands[0].Args[0] } }Later in
Run()(line 167), these are used directly:e.req.Execute(strings.ToUpper(e.method), e.url). Empty values will likely cause confusing runtime errors.Suggested validation
// Extract method and url from Commands field var method string var url string if len(step.Commands) > 0 { method = step.Commands[0].Command if len(step.Commands[0].Args) > 0 { url = step.Commands[0].Args[0] } } + +if method == "" { + return nil, fmt.Errorf("HTTP method is required") +} +if url == "" { + return nil, fmt.Errorf("URL is required") +}internal/core/step.go (1)
134-166: Clear legacy fields after migration to prevent JSON duplication.The
UnmarshalJSONimplementation successfully migrates legacyCommand/Argsfields toCommands, but leaves the legacy fields populated. Since all fields havejson:"...,omitempty"tags and no customMarshalJSONfilters them, JSON serialization outputs both old and new representations:{ "command": "echo", "args": ["hello"], "cmdWithArgs": "echo hello", "commands": [{"command": "echo", "args": ["hello"], ...}] }Clear legacy fields after migration to produce clean output:
if s.Command != "" { s.Commands = []CommandEntry{...} s.Command = "" s.Args = nil s.CmdWithArgs = "" }This maintains backward compatibility for deserialization while ensuring new JSON output uses only the current format.
internal/runtime/builtin/command/multi_command.go (1)
57-73: Consider thread-safety when updating config writers.
SetStdoutandSetStderriterate overe.configswhile holding the lock, butRuncreatescommandExecutorinstances from these configs without holding the lock. IfSetStdout/SetStderrare called concurrently withRun, there's a potential data race oncfg.Stdout/cfg.Stderr.This may be acceptable if the API contract ensures these are only called before
Run, but consider documenting this requirement.internal/runtime/builtin/docker/executor.go (1)
238-335: Consider documenting the Run vs StartBackground+Exec decision.The method branches on Commands length, using:
Run(nil)for zero commandsRun(cmd)for single commandStartBackground() + Exec(cmd)for multiple commandsWhile functionally correct, the choice of
Runfor single commands vsStartBackground+Execfor multiple could affect container lifecycle differently. Consider adding a comment explaining why single commands useRuninstead of the sameStartBackground+Execapproach.📝 Suggested documentation addition
// For single command, use the simple Run approach + // Run executes the command as the container's main process, which is more + // efficient than StartBackground+Exec for single commands and aligns with + // Docker conventions for simple command execution. if len(e.step.Commands) == 1 {internal/core/spec/step.go (1)
697-717: Consider clarifying the error message for restricted executors.The error message on lines 709-713 is functional but could be more helpful by suggesting alternatives. For example, users trying to use multiple commands with
jqmight benefit from knowing they should use separate steps.💡 Enhanced error message suggestion
return core.NewValidationError( "command", result.Commands, - fmt.Errorf("%w: executor type %q only supports a single command", ErrExecutorDoesNotSupportMultipleCmd, executorType), + fmt.Errorf("%w: executor type %q only supports a single command. Consider using separate steps for sequential execution", ErrExecutorDoesNotSupportMultipleCmd, executorType), )
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (33)
internal/core/spec/builder.gointernal/core/spec/builder_test.gointernal/core/spec/dag_test.gointernal/core/spec/errors.gointernal/core/spec/loader_test.gointernal/core/spec/step.gointernal/core/spec/step_test.gointernal/core/step.gointernal/integration/multi_command_test.gointernal/integration/shell_test.gointernal/runtime/agent/agent_test.gointernal/runtime/agent/reporter.gointernal/runtime/agent/reporter_test.gointernal/runtime/builtin/archive/archive.gointernal/runtime/builtin/archive/archive_test.gointernal/runtime/builtin/command/command.gointernal/runtime/builtin/command/command_script.gointernal/runtime/builtin/command/command_test.gointernal/runtime/builtin/command/multi_command.gointernal/runtime/builtin/docker/client.gointernal/runtime/builtin/docker/executor.gointernal/runtime/builtin/gha/executor.gointernal/runtime/builtin/http/http.gointernal/runtime/builtin/http/http_test.gointernal/runtime/builtin/jq/jq.gointernal/runtime/builtin/jq/jq_test.gointernal/runtime/builtin/ssh/ssh.gointernal/runtime/data.gointernal/runtime/node.gointernal/runtime/node_test.gointernal/runtime/runner_helper_test.gointernal/service/frontend/api/v1/dags.gointernal/service/frontend/api/v2/transformer.go
💤 Files with no reviewable changes (1)
- internal/integration/shell_test.go
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*.go: Backend entrypoint incmd/orchestrates the scheduler and CLI; runtime, persistence, and service layers sit underinternal/*(for exampleinternal/runtime,internal/persistence)
Keep Go filesgofmt/goimportsclean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, andErr...names for package-level errors
Repository linting relies ongolangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers ininternal/common
Files:
internal/runtime/builtin/http/http.gointernal/runtime/builtin/command/command_script.gointernal/service/frontend/api/v1/dags.gointernal/core/spec/errors.gointernal/runtime/builtin/jq/jq.gointernal/runtime/builtin/ssh/ssh.gointernal/runtime/agent/reporter.gointernal/runtime/builtin/archive/archive.gointernal/runtime/builtin/archive/archive_test.gointernal/runtime/builtin/command/multi_command.gointernal/runtime/builtin/docker/executor.gointernal/runtime/builtin/jq/jq_test.gointernal/runtime/node.gointernal/core/spec/loader_test.gointernal/runtime/agent/agent_test.gointernal/core/spec/step_test.gointernal/runtime/builtin/gha/executor.gointernal/core/spec/dag_test.gointernal/core/spec/step.gointernal/core/spec/builder.gointernal/runtime/builtin/command/command.gointernal/runtime/data.gointernal/runtime/node_test.gointernal/runtime/builtin/docker/client.gointernal/integration/multi_command_test.gointernal/runtime/runner_helper_test.gointernal/runtime/builtin/http/http_test.gointernal/runtime/agent/reporter_test.gointernal/service/frontend/api/v2/transformer.gointernal/core/step.gointernal/runtime/builtin/command/command_test.gointernal/core/spec/builder_test.go
**/*_test.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*_test.go: Co-locate Go tests as*_test.go; favour table-driven cases and cover failure paths
Usestretchr/testify/requireand shared fixtures frominternal/testinstead of duplicating mocks
Files:
internal/runtime/builtin/archive/archive_test.gointernal/runtime/builtin/jq/jq_test.gointernal/core/spec/loader_test.gointernal/runtime/agent/agent_test.gointernal/core/spec/step_test.gointernal/core/spec/dag_test.gointernal/runtime/node_test.gointernal/integration/multi_command_test.gointernal/runtime/runner_helper_test.gointernal/runtime/builtin/http/http_test.gointernal/runtime/agent/reporter_test.gointernal/runtime/builtin/command/command_test.gointernal/core/spec/builder_test.go
🧠 Learnings (5)
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Commit summaries follow the Go convention `package: change` (lowercase package or area, present tense summary); keep body paragraphs wrapped at 72 chars when needed
Applied to files:
internal/runtime/agent/reporter.go
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*.go : Backend entrypoint in `cmd/` orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under `internal/*` (for example `internal/runtime`, `internal/persistence`)
Applied to files:
internal/runtime/node.gointernal/runtime/node_test.go
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: `make test` (or `make test-coverage`) executes the Go suite via `gotestsum`; append `TEST_TARGET=./internal/...` to focus packages
Applied to files:
internal/core/spec/step_test.gointernal/integration/multi_command_test.go
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*_test.go : Co-locate Go tests as `*_test.go`; favour table-driven cases and cover failure paths
Applied to files:
internal/core/spec/step_test.gointernal/core/spec/dag_test.gointernal/integration/multi_command_test.go
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*_test.go : Use `stretchr/testify/require` and shared fixtures from `internal/test` instead of duplicating mocks
Applied to files:
internal/core/spec/step_test.gointernal/core/spec/builder_test.go
🧬 Code graph analysis (23)
internal/service/frontend/api/v1/dags.go (2)
internal/core/step.go (1)
Step(13-89)api/v2/api.gen.go (1)
Step(856-919)
internal/runtime/builtin/ssh/ssh.go (1)
internal/runtime/subcmd.go (1)
Run(247-269)
internal/runtime/agent/reporter.go (2)
api/v2/api.gen.go (1)
Step(856-919)api/v1/api.gen.go (1)
Step(541-589)
internal/runtime/builtin/archive/archive_test.go (1)
internal/core/step.go (1)
CommandEntry(120-127)
internal/runtime/builtin/command/multi_command.go (2)
internal/runtime/executor/executor.go (2)
Executor(15-20)ExitCoder(50-52)internal/runtime/env.go (1)
GetEnv(380-386)
internal/runtime/builtin/docker/executor.go (4)
internal/runtime/builtin/docker/client.go (1)
ExecOptions(89-92)internal/runtime/executor/tail.go (1)
TailWriter(17-23)internal/common/logger/context.go (2)
Debug(35-37)Error(50-52)internal/common/logger/tag/tag.go (3)
Command(403-405)Args(408-410)Error(20-22)
internal/runtime/builtin/jq/jq_test.go (1)
internal/core/step.go (1)
CommandEntry(120-127)
internal/runtime/node.go (2)
internal/core/step.go (1)
CommandEntry(120-127)internal/common/cmdutil/eval.go (2)
EvalString(126-190)BuildCommandEscapedString(73-106)
internal/runtime/agent/agent_test.go (1)
internal/core/step.go (1)
CommandEntry(120-127)
internal/core/spec/step_test.go (1)
internal/core/step.go (3)
CommandEntry(120-127)Step(13-89)ExecutorConfig(169-177)
internal/runtime/builtin/gha/executor.go (2)
internal/common/logger/tag/tag.go (1)
Command(403-405)internal/common/logger/context.go (1)
Errorf(75-77)
internal/core/spec/step.go (3)
internal/core/spec/errors.go (3)
ErrStepCommandMustBeArrayOrString(32-32)ErrStepCommandIsEmpty(31-31)ErrExecutorDoesNotSupportMultipleCmd(34-34)internal/core/step.go (3)
Step(13-89)CommandEntry(120-127)ExecutorConfig(169-177)internal/common/cmdutil/cmdutil.go (1)
SplitCommand(142-169)
internal/runtime/builtin/command/command.go (2)
internal/core/step.go (1)
Step(13-89)internal/runtime/executor/executor.go (1)
Executor(15-20)
internal/runtime/data.go (1)
internal/core/step.go (2)
Step(13-89)CommandEntry(120-127)
internal/runtime/node_test.go (5)
internal/runtime/data.go (1)
NodeData(23-26)internal/common/cmdutil/cmdutil.go (1)
SplitCommand(142-169)internal/core/step.go (2)
Step(13-89)CommandEntry(120-127)api/v2/api.gen.go (1)
Step(856-919)api/v1/api.gen.go (1)
Step(541-589)
internal/integration/multi_command_test.go (4)
internal/test/helper.go (1)
Setup(81-225)internal/runtime/subcmd.go (1)
Run(247-269)internal/core/status.go (2)
Succeeded(11-11)Failed(9-9)internal/core/spec/loader.go (1)
Load(150-171)
internal/runtime/runner_helper_test.go (1)
internal/core/step.go (1)
CommandEntry(120-127)
internal/runtime/builtin/http/http_test.go (1)
internal/core/step.go (1)
CommandEntry(120-127)
internal/runtime/agent/reporter_test.go (3)
internal/common/logger/tag/tag.go (2)
Name(271-273)Args(408-410)internal/core/step.go (1)
CommandEntry(120-127)api/v2/api.gen.go (1)
Step(856-919)
internal/service/frontend/api/v2/transformer.go (2)
internal/core/step.go (1)
Step(13-89)api/v2/api.gen.go (1)
Step(856-919)
internal/core/step.go (1)
api/v2/api.gen.go (1)
Step(856-919)
internal/runtime/builtin/command/command_test.go (1)
internal/core/step.go (2)
CommandEntry(120-127)Step(13-89)
internal/core/spec/builder_test.go (3)
internal/test/helper.go (1)
DAG(426-429)internal/core/dag.go (1)
DAG(33-135)internal/core/step.go (1)
SubDAG(113-116)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Test on ubuntu-latest
🔇 Additional comments (62)
internal/runtime/builtin/gha/executor.go (2)
210-217: Command extraction looks safe.The bounds checking before accessing
Commands[0]is appropriate, and the error message clearly guides users on the expected format.
219-219: Good practice trimming the command string.Using the extracted command variable with
strings.TrimSpaceensures clean input for action resolution.internal/runtime/builtin/docker/client.go (2)
471-495: LGTM! Well-implemented background startup method.The
StartBackgroundmethod correctly delegates toCreateContainerKeepAlive, which properly handles all startup modes (keepalive, command, entrypoint), readiness waiting, and log pattern matching. The documentation is clear, error handling is appropriate, and logging follows structured logging patterns.
593-596: Good defensive programming for startup command fallback.This fallback condition makes
startNewContainermore self-contained by allowing it to useStartCmdwhen no explicitcmdparameter is provided in "command" startup mode. The validation checklen(c.cfg.StartCmd) > 0ensures the fallback only applies whenStartCmdis configured, maintaining safe behavior.While current callers already provide the
cmdparameter, this change improves flexibility for future use cases without breaking existing behavior.internal/runtime/builtin/command/command_test.go (1)
452-453: Test migration to Commands-based representation looks good.The tests have been correctly migrated to use the new
Commands: []core.CommandEntry{{Command: "...", Args: [...]}}format. The changes are consistent across all test cases and properly reflect the new data model where each step contains a slice of command entries rather than single Command/Args fields.Also applies to: 461-462, 470-471, 565-566, 574-575, 583-584, 591-592, 811-814, 1114-1114, 1128-1129, 1381-1382, 2240-2241
internal/core/spec/builder.go (1)
199-199: Command detection correctly updated to use Commands slice.The change from
step.Command != ""tolen(step.Commands) > 0properly adapts the step prefix logic to the new multi-command data model.internal/core/spec/errors.go (1)
34-34: Error constant added for multi-command validation.The new error
ErrExecutorDoesNotSupportMultipleCmdprovides clear error messaging for executors that don't support multiple commands.internal/service/frontend/api/v1/dags.go (1)
828-835: API transformation correctly adapted to Commands-based model.The
toStepfunction properly extracts command information fromCommands[0]with appropriate guards for empty Commands. The fallback to empty values is appropriate for the API presentation layer.internal/runtime/data.go (1)
146-154: Remove dead methodSetArgs()or prevent invalid CommandEntry state.
Data.SetArgs()creates aCommandEntrywith emptyCommandfield, which contradicts all other code patterns whereCommandis always populated. Additionally, this method appears to be unused—no callers found in the codebase. Either remove it entirely or add validation to prevent creating invalid state. If keeping the method for future API compatibility, at least document why an emptyCommandis acceptable.Likely an incorrect or invalid review comment.
internal/service/frontend/api/v2/transformer.go (1)
49-64: LGTM! Safe extraction from Commands slice with proper guard.The logic correctly checks
len(obj.Commands) > 0before accessingCommands[0], ensuring no index-out-of-bounds panic. This maintains backward compatibility by exposing only the first command's data through the existing API fields.Note: For steps with multiple commands, only the first command's details will be visible through
Command,Args, andCmdWithArgsAPI fields. If multi-command visibility is needed in the API response in the future, consider adding aCommandsfield toapi.Step.internal/runtime/builtin/archive/archive.go (2)
39-47: LGTM! Proper migration to Commands-based access.The validation correctly checks for commands in the new
Commandsslice and maintains the existing error for missing commands.
72-80: LGTM! Consistent command extraction in newExecutor.The executor creation logic mirrors the validation pattern with proper bounds checking before accessing
Commands[0].Command.internal/runtime/agent/reporter.go (2)
259-266: LGTM! Correct use of CmdWithArgs for HTML rendering.Properly extracts the full command string from
Commands[0].CmdWithArgswith appropriate HTML escaping.
586-593: LGTM! Consistent with the other HTML renderer.Same correct pattern as
renderHTML, usingCmdWithArgsfor the command display.internal/runtime/builtin/command/command_script.go (1)
88-106: LGTM! Clean validation logic migrated to Commands-based model.The validation correctly handles all valid step configurations:
- Commands with Script
- Commands only
- Script only
- SubDAG
The
hasCommandsvariable improves readability and the switch structure is clear.internal/runtime/builtin/http/http_test.go (1)
28-38: LGTM! Tests properly migrated to Commands-based step construction.Test cases correctly use the new
Commands: []core.CommandEntry{{Command: "GET", Args: []string{...}}}pattern, consistent with the updated HTTP executor implementation.internal/runtime/builtin/command/multi_command.go (2)
86-118: LGTM! Sequential execution with proper error handling and cancellation checks.The implementation correctly:
- Executes commands in order
- Captures exit code after each command
- Stops on first failure with descriptive error
- Checks context cancellation between commands
- Uses mutex to protect shared state
30-55: The context handling in this code is correct. Thecfg.Ctxstored in the config does not control execution—commandExecutor.Run(ctx)receives and uses the context parameter passed frommultiCommandExecutor.Run(), which is the expected behavior. The concern about stale context is unfounded.internal/runtime/agent/agent_test.go (2)
243-245: LGTM! Test correctly migrated to Commands-based step modification.The retry test properly updates steps using the new
Commandsslice structure, maintaining bothCommandandCmdWithArgsfor execution and display purposes.
316-318: LGTM! Consistent with the other retry test.Same correct pattern applied to the StepRetry test case.
internal/runtime/builtin/archive/archive_test.go (3)
28-39: LGTM! Test correctly uses Commands-based step construction.The extract test properly constructs the step with
Commands: []core.CommandEntry{{Command: opExtract}}, aligning with the updated archive executor implementation.
87-98: LGTM! Consistent pattern for create operation test.Same correct structure used for the tar.gz creation test.
135-144: LGTM! Consistent pattern for list operation test.All three archive operation tests (extract, create, list) now correctly use the Commands-based step structure.
internal/runtime/runner_helper_test.go (1)
86-91: LGTM!The test helper correctly migrates to the new
Commandsslice structure. Usingcmdutil.SplitCommandensures consistent parsing, and preservingCmdWithArgsmaintains display fidelity.internal/runtime/builtin/command/command.go (2)
293-314: LGTM!The config derivation correctly handles both populated and empty
Commandsslices. WhenCommandsis empty,commandandargsremain zero values, which the downstream logic handles appropriately.
273-286: Multi-command support is properly implemented with correct sequential execution and edge case handling.The branching logic correctly delegates to
newMultiCommandExecutorwhenlen(step.Commands) > 1. The implementation iterates through all commands sequentially, stops on first failure, and handles context cancellation between commands. Edge cases are safe: emptyCommandsslice and single-command path both route toNewCommandConfig, which safely checkslen(step.Commands) > 0before accessingCommands[0]. All Go conventions are followed (tabs, error handling, thread-safe mutex, proper naming).internal/runtime/node.go (2)
428-468: Well-structured multi-command evaluation logic.The implementation correctly:
- Deep-clones the
Commandsslice to prevent race conditions- Evaluates each command entry's
Argsindependently- Sets
ShellCmdArgsonly for the first command when using shell executorOne observation: The
CmdWithArgsfield is preserved from the original entry without re-evaluation (line 446), which is intentional for maintaining the original command string for display purposes.
470-562: Legacy field migration paths look correct.All legacy formats (
CmdArgsSys,CmdWithArgs,Commandwith/withoutArgs) are properly converted to the newCommandsstructure. This ensures backward compatibility with existing JSON serialization.internal/core/spec/loader_test.go (5)
116-117: LGTM!Tests correctly updated to validate the new
Commandsstructure. The pattern of checkingLen(Commands, 1)before accessingCommands[0].Commandis safe and consistent.
258-259: LGTM!Consistent with the updated data model.
280-281: LGTM!Test assertions properly adapted.
348-349: LGTM!Multi-DAG test correctly validates Commands structure.
358-359: LGTM!Consistent pattern maintained across test cases.
internal/core/spec/step_test.go (6)
841-895: LGTM!Test structure correctly updated to validate
Commandsslice andScriptfields. The assertion that legacy fields (Command,Args,CmdWithArgs) remain empty ensures clean separation from the new data model.
897-1010: Comprehensive multi-command test coverage.Excellent coverage of edge cases including:
- Single command in array
- Multiple commands with various argument patterns
- Quoted arguments and pipes
- Empty arrays and mixed empty/valid entries
- Non-string element conversion
The test at lines 973-980 validates that non-string types (int, bool, float) are converted to string commands, which is a useful flexibility for YAML parsing.
1012-1090: LGTM!
buildSingleCommandtests properly validate both command parsing and script fallback behavior for multiline commands.
1092-1147: LGTM!
buildMultipleCommandstests correctly validate array processing and empty-value filtering.
1149-1195: LGTM!
HasMultipleCommandstests cover all boundary conditions (0, 1, 2+ commands).
1859-2044: Thorough executor validation testing.The test suite comprehensively validates which executor types support multiple commands:
- Supported: shell, command, docker, container, ssh (empty string executor type)
- Unsupported: jq, http, archive, github_action, mail, dag, parallel
This aligns with the semantic expectations for each executor type.
internal/runtime/node_test.go (2)
15-15: LGTM!Import added for
cmdutilto support command splitting in test helpers.
1042-1057: LGTM!Test helpers correctly updated to populate
Commandsslice. ThewithNodeCmdArgshelper properly usescmdutil.SplitCommandfor consistent parsing, whilewithNodeCommandcreates a minimal entry for simple command-only tests.internal/runtime/builtin/ssh/ssh.go (1)
84-122: Multi-command SSH execution implemented correctly with one caveat.The implementation properly handles SSH's constraint that each session can only call
Runonce by creating a new session per command. Context cancellation checks between commands and indexed error messages are good additions.Potential issue with
Kill()during execution: Thee.sessionfield is overwritten on each iteration (line 104), so callingKill()during multi-command execution will only close the current session. However, since SSH commands run synchronously andKill()would typically be called from a signal handler, this should work correctly in practice—the current running session is the one that needs to be killed.internal/runtime/builtin/jq/jq_test.go (6)
169-170: LGTM!Test correctly updated to use
Commandsslice withCmdWithArgsfor the JQ query. TheScriptfield remains at the Step level as it represents input data, not a command.
246-247: LGTM!Consistent pattern maintained.
277-278: LGTM!
302-303: LGTM!
323-324: LGTM!
348-349: LGTM!internal/runtime/agent/reporter_test.go (2)
56-58: LGTM! Test data correctly migrated to Commands structure.The test data now uses the new
Commands: []core.CommandEntrystructure with proper field population (Command, Args). This aligns with the PR's multi-command support while maintaining backward compatibility for single-command tests.
231-232: Verify Commands slice is non-empty before accessing index 0.The test asserts
Len(Commands, 1)on line 231 before accessingCommands[0].Args[0]on line 232, which is correct defensive programming. However, consider consolidating these assertions for clarity.internal/integration/multi_command_test.go (2)
17-43: LGTM! Comprehensive multi-command shell test coverage.The test suite properly exercises multiple commands with output concatenation, environment variable expansion, failure scenarios, and sequential execution. The Windows skip and parallel execution are appropriately configured.
382-404: The test is well-written and error messages are sufficiently descriptive for production use. The validation already provides context including the executor type ("executor type "jq" only supports a single command"), field name, and command values. Temporary file cleanup viat.TempDir()is handled automatically. No changes needed.internal/core/spec/dag_test.go (2)
1639-1654: LGTM! Handler tests correctly migrated to Commands structure.All handler fields (Init, Exit, Success, Failure, Cancel) are now properly accessed via
Commands[0].CmdWithArgs. The assertions first verify the Commands slice length before accessing elements, which is defensive and correct.
1698-1708: Error path tests validate empty command handling.The test correctly validates that an empty command (after trim) triggers an error. This ensures the new Commands-based validation maintains the same strictness as the legacy code.
internal/runtime/builtin/docker/executor.go (2)
186-236: LGTM! Existing container execution properly handles multiple commands.The method correctly:
- Handles zero commands case with nil command execution
- Iterates Commands sequentially with proper error reporting
- Checks context cancellation between commands (lines 228-232)
- Tracks exit codes with mutex protection
- Augments errors with stderr tail when available
281-294: Update the comment to include all three startup modes.The comment at lines 286-288 mentions only "startup:command" and "keepalive" modes, but the implementation supports a third mode: "entrypoint". Update the comment to reflect that
StartBackgrounddelegates toCreateContainerKeepAlive, which properly handles all three configured startup modes: "keepalive" (default), "command" (uses configured StartCmd), and "entrypoint" (respects image entrypoint/cmd).internal/core/spec/builder_test.go (3)
495-511: Breaking change: Array commands now create multiple Commands entries.The test now expects
Len(Commands, 2)where each array element becomes a separate command. Previously,command: [echo, 1]was treated as a single command with args. This is a breaking behavioral change.Verify this breaking change is documented in migration notes. The new behavior treats:
command: [echo, 1] # Creates 2 commands: "echo" and "1"differently from:
command: "echo 1" # Creates 1 command: "echo" with arg "1"Users might expect the array form to split command and args, not create multiple commands.
531-554: LGTM! Excellent test coverage for sequential multi-command execution.This new test validates the core multi-command feature with proper assertions on Commands length and individual command parsing (Command, Args). The test data
npm install,npm run build,npm testrepresents a realistic use case.
610-628: SubDAG migration correctly uses SubDAG.Name and SubDAG.Params.The tests properly validate that both
calland legacyrunfields populateSubDAG.NameandSubDAG.Paramsinstead of the deprecated Command/Args/CmdWithArgs fields. The deprecation warning check on line 628 is appropriate.internal/core/spec/step.go (3)
582-602: LGTM! Clean type switch for command parsing.The refactored
buildStepCommandproperly delegates to specialized helpers based on command type (string vs []any). Error handling returns a descriptive validation error for unsupported types.
604-634: Single command parsing correctly handles scripts and splits command/args.The logic:
- Trims whitespace
- Detects multi-line as script (line 612)
- Splits command using
cmdutil.SplitCommand(line 618)- Populates Commands[0] with parsed data
This is correct and maintains backward compatibility with legacy single-command syntax.
636-681: Multiple command parsing handles array elements sequentially.The implementation:
- Iterates array elements, converting non-strings via
fmt.Sprintf(lines 645-650)- Skips empty commands after trim (lines 653-655)
- Parses each command string into Command/Args/CmdWithArgs (lines 657-671)
- Provides per-command error context with
command[%d]format (line 661)This is well-designed for user-friendly error reporting.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
internal/core/spec/step.go (1)
645-650: Consider stricter type validation for command array elements.The silent conversion of non-string values via
fmt.Sprintf("%v", v)could mask configuration errors. For example, a nested map would stringify tomap[key:value], which is unlikely to be a valid command.Consider validating that array elements are primitive types (string, int, float, bool) and rejecting complex types like maps or nested arrays with a clear error message.
🔎 Proposed fix
for i, v := range val { - strVal, ok := v.(string) - if !ok { - // If the value is not a string, convert it to a string. - strVal = fmt.Sprintf("%v", v) + var strVal string + switch tv := v.(type) { + case string: + strVal = tv + case int, int64, uint64, float64, bool: + strVal = fmt.Sprintf("%v", tv) + default: + return core.NewValidationError( + fmt.Sprintf("command[%d]", i), + v, + fmt.Errorf("command array elements must be strings or primitive types, got %T", v), + ) }internal/cmd/exec_spec.go (2)
58-60: Clarify the comment to better reflect the actual behavior.The comment states "ensures the command is treated as a single command, not multiple commands," which could be misleading.
shellquote.Joinproperly escapes and quotes the arguments for safe shell execution, but it doesn't prevent multiple commands if the original args contained shell metacharacters like;or|.Consider revising the comment to:
- // Join command args into a single properly quoted string - // This ensures the command is treated as a single command, not multiple commands + // Join command args into a single shell-safe string with proper quoting/escaping + // This preserves the original args as separate parameters for shell execution
14-14: Consider the long-term maintenance and support of thego-shellquotelibrary.The library is appropriate for this use case—converting CLI arguments to a shell-quoted string for DAG serialization requires proper escaping. However, the library appears minimally maintained (no recent releases; last version dated 2018). While no known vulnerabilities exist, consider monitoring this dependency for future updates or exploring well-maintained alternatives for shell quoting if long-term support becomes a concern.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
internal/cmd/exec_spec.gointernal/core/spec/step.go
🧰 Additional context used
📓 Path-based instructions (1)
**/*.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*.go: Backend entrypoint incmd/orchestrates the scheduler and CLI; runtime, persistence, and service layers sit underinternal/*(for exampleinternal/runtime,internal/persistence)
Keep Go filesgofmt/goimportsclean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, andErr...names for package-level errors
Repository linting relies ongolangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers ininternal/common
Files:
internal/cmd/exec_spec.gointernal/core/spec/step.go
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Test on ubuntu-latest
🔇 Additional comments (6)
internal/core/spec/step.go (4)
208-210: LGTM!The validation is correctly placed after
buildStepCommandandbuildStepExecutor, ensuring that bothresult.Commandsandresult.ExecutorConfig.Typeare populated before validation.
590-602: LGTM!Clean type switch pattern that delegates to appropriate handler functions. The error case properly validates against unexpected types.
604-634: LGTM!The function correctly handles edge cases: empty strings are rejected, multi-line content is treated as a script, and single-line commands are properly parsed. The
CmdWithArgsfield preserves the original trimmed command string for display/logging purposes.
698-718: LGTM!The validation logic correctly handles edge cases:
- Early return for single or no commands avoids unnecessary checks.
- Empty executor type (default behavior) correctly bypasses validation since shell-like executors support multiple commands.
- Error message is informative with the executor type included.
internal/cmd/exec_spec.go (2)
40-42: LGTM!The type change from
[]stringtostringfor theCommandfield aligns with the new approach of storing a single shell-quoted command string. The YAML tag is appropriate.
72-72: LGTM!The use of
commandStris consistent with the type change and properly utilizes the shell-quoted command string.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
internal/integration/gha_test.go (1)
31-35: Use testify/require for consistency.The error handling uses
t.Fatalfinstead oftestify/require, which is inconsistent with the project's coding guidelines.As per coding guidelines: "Use
stretchr/testify/requireand shared fixtures frominternal/testinstead of duplicating mocks"🔎 Suggested refactor using testify/require
+ require := require.New(t) + // Initialize git repo in the temp dir to satisfy act requirements cmd := exec.Command("git", "init", dag.WorkingDir) - if err := cmd.Run(); err != nil { - t.Fatalf("failed to init git repo: %v", err) - } + require.NoError(cmd.Run(), "failed to init git repo")Additionally, consider verifying git availability before running the command to provide clearer error messages:
_, err := exec.LookPath("git") require.NoError(err, "git is required for this test but not found in PATH")
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
internal/integration/gha_test.go
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*.go: Backend entrypoint incmd/orchestrates the scheduler and CLI; runtime, persistence, and service layers sit underinternal/*(for exampleinternal/runtime,internal/persistence)
Keep Go filesgofmt/goimportsclean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, andErr...names for package-level errors
Repository linting relies ongolangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers ininternal/common
Files:
internal/integration/gha_test.go
**/*_test.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*_test.go: Co-locate Go tests as*_test.go; favour table-driven cases and cover failure paths
Usestretchr/testify/requireand shared fixtures frominternal/testinstead of duplicating mocks
Files:
internal/integration/gha_test.go
🧠 Learnings (1)
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*_test.go : Co-locate Go tests as `*_test.go`; favour table-driven cases and cover failure paths
Applied to files:
internal/integration/gha_test.go
🧬 Code graph analysis (1)
internal/integration/gha_test.go (3)
internal/runtime/subcmd.go (1)
Run(247-269)internal/test/helper.go (1)
Setup(81-225)internal/core/dag.go (1)
DAG(33-135)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Test on ubuntu-latest
🔇 Additional comments (2)
internal/integration/gha_test.go (2)
4-4: LGTM: Import updated correctly.The import change from
ostoos/execis appropriate for the new git initialization logic.
17-29: Thenode:25-bookwormDocker image exists on Docker Hub and is available for use.Likely an incorrect or invalid review comment.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (8)
internal/integration/container_test.go (1)
521-521: Good addition of outer-level parallelism.This change correctly aligns TestStepLevelContainer with the parallelism strategy used by other test functions in this file (lines 260, 294, 319, 342, 374). Each subtest creates isolated resources, making parallel execution safe.
However, for consistency, TestDAGLevelContainer (line 92) should also add outer-level
t.Parallel()at its function start. Currently it only has inner parallelism (line 240 inside the loop), creating an inconsistency in the test suite's parallelism strategy.🔎 Suggested consistency improvement for TestDAGLevelContainer
Add outer-level parallelism to TestDAGLevelContainer:
func TestDAGLevelContainer(t *testing.T) { + t.Parallel() + tests := []containerTest{This would enable both TestDAGLevelContainer and TestStepLevelContainer to run in parallel with other test functions while maintaining their inner subtest parallelism.
internal/runtime/env_test.go (1)
283-289: Consider makingCmdWithArgsconsistent withArgsfor realismThis fixture is valid, but
Args: []{"hello","arg1","arg2"}withCmdWithArgs: "echo hello"is a bit inconsistent with how the loader would typically populate it (full original command string). For more realistic coverage you could either:
- Set
CmdWithArgs: "echo hello arg1 arg2", or- Drop the extra args so both reflect the same shape.
Not functionally required for this test, just a minor realism/maintainability tweak.
internal/runtime/node_test.go (1)
1048-1057: Consider handling the error fromSplitCommand.The error return from
cmdutil.SplitCommandis silently discarded. While current usages pass valid command strings, ignoring parse errors could lead to confusing test failures if malformed commands are accidentally used in the future.🔎 Proposed fix
func withNodeCmdArgs(cmdWithArgs string) nodeOption { return func(data *runtime.NodeData) { - cmd, args, _ := cmdutil.SplitCommand(cmdWithArgs) + cmd, args, err := cmdutil.SplitCommand(cmdWithArgs) + if err != nil { + panic(fmt.Sprintf("withNodeCmdArgs: invalid command %q: %v", cmdWithArgs, err)) + } data.Step.Commands = []core.CommandEntry{{ Command: cmd, Args: args, CmdWithArgs: cmdWithArgs, }} } }api/v2/api.gen.go (1)
165-172: AlignCommandEntryAPI shape with internal/core or clarify divergence
api/v2.CommandEntryonly exposescommandandargs, whileinternal/core/step.go’sCommandEntryalso hasCmdWithArgsfor the original command string. If API consumers (UI or others) need that original string, consider adding:// CmdWithArgs is the original command string for display purposes. CmdWithArgs string `json:"cmdWithArgs,omitempty"`to this type, or document that clients should reconstruct it from
command+argsand thatcmdWithArgsis intentionally internal-only.internal/runtime/builtin/command/command_test.go (1)
2295-2408: QuoteoutputFilein shell snippets to tolerate spaces in temp pathsThe multi-command tests build shell commands like
echo first >>+outputFilewithout quoting the path. If the temp directory ever contained spaces, these would break. Wrapping the path in double quotes would make the tests more robust without changing intent.For example:
Suggested adjustment
- {Command: "sh", Args: []string{"-c", "echo first >> " + outputFile}}, + {Command: "sh", Args: []string{"-c", "echo first >> \"" + outputFile + "\""}},Apply similarly for the other
echo ... >> outputFilecommands.internal/runtime/runner_helper_test.go (1)
80-92: Handler construction and option wiring look correct; consider de-duplicating command setupThe switch to
step.Commands = []core.CommandEntry{...}inwithCommandandnewHandlerStepmatches the newcore.Stepshape, andwithOnSuccess/withOnFailure/withOnExit/withOnCancelnow correctly wire each handler to the correspondingruntime.Configfield.You do duplicate the
SplitCommand+CommandEntryconstruction betweenwithCommandandnewHandlerStep, which could drift over time. If you want to DRY this up later, you could factor a tiny helper that returns acore.CommandEntryfrom a raw command string and reuse it in both helpers.Also applies to: 136-173
ui/src/features/dags/components/dag-details/NodeStatusTableRow.tsx (1)
606-615: Multi-command rendering is consistent; optional: add legacy fallback for older API responsesThe new usage of
CommandDisplayon desktop and the explicitnode.step.commands.map(...)rendering on mobile correctly handle 0/1/many commands and look aligned with the newcommandsAPI.If you expect the UI to talk to older servers for a while, you might want a lightweight fallback like:
const commands = node.step.commands ?? ( node.step.command ? [{ command: node.step.command, args: node.step.args ?? [] }] : undefined );and then render from
commandsin both places. That would keep command display working whencommandsisn’t yet populated by the backend. Otherwise, the current code is fine for a strictly version-locked frontend/backend.Also applies to: 952-985
ui/src/components/ui/command-display.tsx (1)
16-45: Clamp truncation length to avoid negative cutoffs for tinymaxLength
truncateTextassumesmaxLengthis reasonably larger than 3; whenmaxLengthis very small (e.g. 0–4),maxLength - 3ormaxLength - 5can go negative, and you end up relying on JSsubstring’s odd negative-index behavior.It’s harmless with your current defaults, but you can make it more robust with a simple clamp:
Suggested tweak
- const cutoff = maxLength - 3; + const cutoff = Math.max(maxLength - 3, 1);and in the multi-command case:
- const { truncated } = truncateText(fullCmd, maxLength - 5); + const { truncated } = truncateText(fullCmd, Math.max(maxLength - 5, 4));This keeps behavior the same for normal values while avoiding edge-case surprises.
Also applies to: 89-132
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (20)
api/v2/api.gen.goapi/v2/api.yamlinternal/core/step.gointernal/integration/container_test.gointernal/runtime/builtin/command/command.gointernal/runtime/builtin/command/command_test.gointernal/runtime/env_test.gointernal/runtime/executor/dag_runner_test.gointernal/runtime/node.gointernal/runtime/node_test.gointernal/runtime/output_test.gointernal/runtime/plan_test.gointernal/runtime/runner_helper_test.gointernal/runtime/runner_test.gointernal/runtime/transform/status_test.gointernal/service/frontend/api/v2/transformer.goui/src/api/v2/schema.tsui/src/components/ui/command-display.tsxui/src/features/dags/components/dag-details/DAGStepTableRow.tsxui/src/features/dags/components/dag-details/NodeStatusTableRow.tsx
✅ Files skipped from review due to trivial changes (1)
- api/v2/api.yaml
🚧 Files skipped from review as they are similar to previous changes (1)
- internal/service/frontend/api/v2/transformer.go
🧰 Additional context used
📓 Path-based instructions (4)
**/*.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*.go: Backend entrypoint incmd/orchestrates the scheduler and CLI; runtime, persistence, and service layers sit underinternal/*(for exampleinternal/runtime,internal/persistence)
Keep Go filesgofmt/goimportsclean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, andErr...names for package-level errors
Repository linting relies ongolangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers ininternal/common
Files:
internal/integration/container_test.gointernal/runtime/node.gointernal/runtime/executor/dag_runner_test.gointernal/runtime/transform/status_test.gointernal/runtime/node_test.gointernal/runtime/env_test.gointernal/core/step.gointernal/runtime/plan_test.gointernal/runtime/builtin/command/command.goapi/v2/api.gen.gointernal/runtime/runner_test.gointernal/runtime/builtin/command/command_test.gointernal/runtime/output_test.gointernal/runtime/runner_helper_test.go
**/*_test.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*_test.go: Co-locate Go tests as*_test.go; favour table-driven cases and cover failure paths
Usestretchr/testify/requireand shared fixtures frominternal/testinstead of duplicating mocks
Files:
internal/integration/container_test.gointernal/runtime/executor/dag_runner_test.gointernal/runtime/transform/status_test.gointernal/runtime/node_test.gointernal/runtime/env_test.gointernal/runtime/plan_test.gointernal/runtime/runner_test.gointernal/runtime/builtin/command/command_test.gointernal/runtime/output_test.gointernal/runtime/runner_helper_test.go
ui/**/*.{ts,tsx,jsx,js}
📄 CodeRabbit inference engine (ui/CLAUDE.md)
ui/**/*.{ts,tsx,jsx,js}: Use developer-centric UI design with high information density, minimal whitespace, compact components, and no unnecessary decorations
Support both light and dark modes for all UI components using Tailwind CSS class pairs likedark:bg-slate-700
NEVER use full-page loading overlays or LoadingIndicator components that hide content - show stale data while fetching updates instead
Use compact modal design with small headers, minimal padding (p-2 or p-3), tight spacing, and support keyboard navigation (arrows, enter, escape)
Use small heights for form elements: select boxes h-7 or smaller, buttons h-7 or h-8, inputs with compact padding (py-0.5 or py-1)
Minimize row heights in tables and lists while maintaining readability, merge related columns, and always handle long text withwhitespace-normal break-words
Use consistent metadata styling withbg-slate-200 dark:bg-slate-700backgrounds and maintain text hierarchy with primary/secondary/muted text colors
Use flexbox-first layouts withmin-h-0andoverflow-hiddento prevent layout breaks, account for fixed elements when setting heights
Maintain keyboard navigation support in all interactive components with appropriate focus indicators and ARIA labels
Files:
ui/src/api/v2/schema.tsui/src/components/ui/command-display.tsxui/src/features/dags/components/dag-details/NodeStatusTableRow.tsxui/src/features/dags/components/dag-details/DAGStepTableRow.tsx
ui/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
ui/**/*.{ts,tsx}: The React + TypeScript frontend resides inui/, with production bundles copied tointernal/service/frontend/assetsbymake ui
UI code follows ESLint + Prettier (2-space indent) and Tailwind utilities; name React components in PascalCase (JobList.tsx) and hooks withuse*(useJobs.ts)
Files:
ui/src/api/v2/schema.tsui/src/components/ui/command-display.tsxui/src/features/dags/components/dag-details/NodeStatusTableRow.tsxui/src/features/dags/components/dag-details/DAGStepTableRow.tsx
🧠 Learnings (5)
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*_test.go : Co-locate Go tests as `*_test.go`; favour table-driven cases and cover failure paths
Applied to files:
internal/integration/container_test.gointernal/runtime/node_test.gointernal/runtime/output_test.go
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: `make test` (or `make test-coverage`) executes the Go suite via `gotestsum`; append `TEST_TARGET=./internal/...` to focus packages
Applied to files:
internal/runtime/node_test.go
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*_test.go : Use `stretchr/testify/require` and shared fixtures from `internal/test` instead of duplicating mocks
Applied to files:
internal/runtime/node_test.go
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*.go : Backend entrypoint in `cmd/` orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under `internal/*` (for example `internal/runtime`, `internal/persistence`)
Applied to files:
internal/runtime/node_test.gointernal/runtime/runner_test.go
📚 Learning: 2025-12-04T10:34:01.045Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: ui/CLAUDE.md:0-0
Timestamp: 2025-12-04T10:34:01.045Z
Learning: Applies to ui/**/*.{ts,tsx,jsx,js} : Minimize row heights in tables and lists while maintaining readability, merge related columns, and always handle long text with `whitespace-normal break-words`
Applied to files:
ui/src/features/dags/components/dag-details/NodeStatusTableRow.tsx
🧬 Code graph analysis (16)
internal/integration/container_test.go (1)
internal/runtime/data.go (1)
Parallel(69-73)
internal/runtime/node.go (3)
internal/core/step.go (2)
CommandEntry(120-127)ExecutorConfig(167-175)api/v2/api.gen.go (1)
CommandEntry(166-172)internal/runtime/eval.go (1)
EvalString(14-16)
internal/runtime/executor/dag_runner_test.go (3)
internal/common/logger/tag/tag.go (3)
Name(271-273)Command(403-405)Args(408-410)internal/core/step.go (1)
CommandEntry(120-127)api/v2/api.gen.go (1)
CommandEntry(166-172)
internal/runtime/transform/status_test.go (2)
internal/core/step.go (1)
CommandEntry(120-127)api/v2/api.gen.go (1)
CommandEntry(166-172)
internal/runtime/node_test.go (3)
internal/core/step.go (2)
CommandEntry(120-127)Step(13-89)internal/runtime/data.go (1)
NodeData(23-26)internal/common/cmdutil/cmdutil.go (1)
SplitCommand(142-169)
internal/runtime/env_test.go (1)
internal/core/step.go (1)
CommandEntry(120-127)
internal/core/step.go (1)
api/v2/api.gen.go (2)
CommandEntry(166-172)Step(865-922)
internal/runtime/plan_test.go (2)
internal/common/logger/tag/tag.go (2)
Name(271-273)Args(408-410)internal/core/step.go (1)
CommandEntry(120-127)
internal/runtime/builtin/command/command.go (4)
internal/core/step.go (1)
Step(13-89)api/v2/api.gen.go (1)
Step(865-922)internal/runtime/executor/executor.go (1)
Executor(15-20)internal/runtime/env.go (1)
GetEnv(380-386)
ui/src/features/dags/components/dag-details/NodeStatusTableRow.tsx (1)
ui/src/components/ui/command-display.tsx (1)
CommandDisplay(47-137)
api/v2/api.gen.go (2)
internal/common/logger/tag/tag.go (1)
Args(408-410)internal/core/step.go (1)
CommandEntry(120-127)
internal/runtime/runner_test.go (1)
internal/core/step.go (1)
CommandEntry(120-127)
internal/runtime/builtin/command/command_test.go (3)
internal/core/step.go (2)
CommandEntry(120-127)Step(13-89)internal/runtime/builtin/command/command.go (1)
NewCommand(274-286)internal/runtime/executor/executor.go (1)
ExitCoder(50-52)
internal/runtime/output_test.go (1)
internal/core/step.go (1)
CommandEntry(120-127)
ui/src/features/dags/components/dag-details/DAGStepTableRow.tsx (1)
ui/src/components/ui/command-display.tsx (1)
CommandDisplay(47-137)
internal/runtime/runner_helper_test.go (3)
internal/core/step.go (2)
CommandEntry(120-127)Step(13-89)internal/common/cmdutil/cmdutil.go (1)
SplitCommand(142-169)internal/runtime/runner.go (1)
Config(83-95)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Test on ubuntu-latest
- GitHub Check: Build
🔇 Additional comments (23)
internal/runtime/executor/dag_runner_test.go (1)
29-31: Step fixture migration toCommandslooks correctUsing
Commands: []core.CommandEntry{{Command: "echo", Args: []string{"hello"}}}aligns this test with the new multi-command model without changing behavior; no issues spotted.internal/runtime/transform/status_test.go (1)
24-31: Status/Node fixtures now aligned withStep.CommandsAll updated step fixtures correctly use
Commands: []core.CommandEntry{...}while keeping existing assertions focused on names/status/preconditions, so behavior stays the same and exercises the new shape.Also applies to: 60-62, 305-315
ui/src/features/dags/components/dag-details/DAGStepTableRow.tsx (1)
81-91: Command rendering correctly switched to multi-command APIConditioning on
step.commands?.length > 0and passingcommands={step.commands}matches the updated schema and the newCommandDisplayAPI; no additional handling needed here.internal/runtime/plan_test.go (1)
59-62: Plan dependency tests now exerciseCommandswithout altering semanticsAdding trivial
Commandsentries to the “basic” test steps keeps dependency expectations intact while validating the new command representation; looks good.internal/runtime/node.go (1)
413-467: Multi-command evaluation logic looks sound and preserves executor semanticsThe new
evaluateCommandArgscorrectly:
- Guards with
cmdEvaluated.- Applies
cmdutil.WithoutExpandEnv()only for command executors with a shell.- Evaluates each arg and (when set)
CmdWithArgsper entry, then writes back a fresh[]core.CommandEntryviaSetStep.This keeps behavior consistent while supporting multiple commands; no obvious edge-case regressions found.
ui/src/api/v2/schema.ts (1)
1219-1230: API schema correctly modelscommandsandCommandEntryExposing
Step.commands?: CommandEntry[]plus the newCommandEntry { command: string; args?: string[] }type aligns the UI schema with the backend’s multi-command model. Given this file is generated, the changes look consistent and sufficient for the new UI consumers.Also applies to: 1328-1334
internal/runtime/output_test.go (1)
55-62: Output-focused node tests correctly migrated toCommandsAll affected tests now construct steps via
Commandswithsh -c <python script>while preservingOutputand assertions about size/behavior. This keeps the intent of the tests intact and ensures the new multi-command path is exercised.Also applies to: 123-132, 174-183, 209-217, 247-257
internal/runtime/node_test.go (9)
15-15: LGTM!The
cmdutilimport is correctly added and used by thewithNodeCmdArgshelper function on line 1050.
1059-1066: LGTM!The helper correctly sets both
CommandandCmdWithArgsto the same value. For simple commands like"true"or"sleep 3", the executor will useCmdWithArgsvia the shell whenArgsis empty, which is the expected behavior.
676-681: LGTM!The test data correctly uses the new
Commands []core.CommandEntrystructure with properly populatedCommandandArgsfields.
736-741: LGTM!Consistent with the new multi-command data model.
884-890: LGTM!The
Outputfield is correctly placed at the step level (not insideCommandEntry), which aligns with thecore.Stepstructure where output capture is a step-level concern.
1171-1177: LGTM!Test data correctly structured for output redirection testing with the new Commands model.
1213-1219: LGTM!Consistent test data construction for relative path testing.
1260-1266: LGTM!Shell command with stderr redirection correctly structured.
1308-1314: LGTM!Nested path test case correctly uses the new Commands structure.
internal/runtime/builtin/command/command.go (2)
296-319: No action needed—backward compatibility is properly handled via UnmarshalJSON migration.The Step struct includes an UnmarshalJSON implementation (
internal/core/step.go:136-156) that automatically migrates legacyCommand/Argsfields to theCommandsslice during JSON unmarshalling. When old-format configurations are deserialized, the migration happens before the code at lines 296-319 executes, so extracting fromstep.Commands[0]and falling back tostep.ShellCmdArgsis correct and sufficient. No additional fallback to deprecated fields is needed in NewCommandConfig.
273-286: LGTM. The branching logic correctly routes tomultiCommandExecutorwhen multiple commands exist. ThenewMultiCommandExecutorimplementation properly iterates overstep.Commandsand constructs configs for each command. The single-command path handles bothlen(step.Commands) == 0andlen(step.Commands) == 1cases viaNewCommandConfig, with proper fallback tostep.ShellCmdArgs. Code follows Go conventions with proper tab indentation, error wrapping, and naming patterns.api/v2/api.gen.go (2)
869-871:Step.Commandsshape looks consistent with multi‑command supportUsing
Commands *[]CommandEntrywithomitemptymatches how other optional lists are modeled in this file and cleanly represents sequential commands for a step; no issues from an API-shape perspective.
8476-8670: Swagger blob appears properly regenerated; just keep it source‑of‑truth drivenThe updated
swaggerSpecbase64/gzip blob is expected after changing the OpenAPI forStep.commands/CommandEntry. As long as this file is entirely generated from the updated spec (e.g., via your normaloapi-codegen/make generateflow) and not hand‑edited, this block looks fine.internal/runtime/builtin/command/command_test.go (1)
450-476: Single-command tests correctly migrated toCommandsAPIAll updated tests now use
core.Step{ Name: "…", Commands: []core.CommandEntry{{Command: "...", Args: ...}} }, which aligns withNewCommand’slen(step.Commands) > 1switch and keeps single-command behavior unchanged. No further changes needed here.Also applies to: 562-595, 810-815, 1112-1132, 1380-1383, 2239-2246
internal/runtime/runner_test.go (2)
2260-2306: Step retry DAG updated correctly to useCommandsThe DAG in
TestRunner_StepRetryExecutionnow usesCommands: []core.CommandEntry{{Command: "echo", Args: []string{"A"}}}etc., which matches the new multi-command representation and keeps the retry-plan semantics intact. The node construction fromdag.Steps[i]lines up with this; no issues spotted.
2408-2606: Handler ID-access tests correctly exercisenewHandlerStepand new handler wiringThe event-handler tests now:
- Build handlers via
newHandlerStep(t, name, id, command)so each handler has a properCommandsentry and optional ID.- Use
withOnSuccess,withOnFailure, andwithOnExitthat now correctly assign toruntime.Config.OnSuccess/OnFailure/OnExit.- Verify that handlers can reference main steps via
${id.stdout}/${id.exit_code}, while cross-handler references remain unresolved.This cleanly validates the new handler construction path and the environment semantics around step IDs. No further changes needed.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (2)
internal/integration/gha_test.go (1)
13-13: Unconditional skip still present.This test remains permanently skipped with only "skip" as the message. As noted in the previous review, this should either be:
- Fixed and enabled
- Conditionally skipped with a clear reason (e.g., check for required dependencies)
- Removed if no longer needed
If there's a blocking issue, use something like
t.Skip("TODO: GitHub Actions executor requires act - see issue #XXX").internal/core/spec/step.go (1)
691-704: Add missing "subworkflow" executor to singleCommandOnlyExecutors map.As identified in a previous review, the "subworkflow" executor type is registered as an alias for "dag" (in
internal/runtime/builtin/dag/dag.go) but is missing from this map. Since it uses the same executor implementation, it should also be restricted to single commands.🔎 Proposed fix
var singleCommandOnlyExecutors = map[string]bool{ "jq": true, "http": true, "archive": true, "github_action": true, "github-action": true, "gha": true, "mail": true, "dag": true, + "subworkflow": true, "parallel": true, }
🧹 Nitpick comments (2)
internal/runtime/runner_helper_test.go (1)
141-147: Unused parameter in newHandlerStep.The
t *testing.Tparameter is not used in this function (it's marked with_). Consider removing it or usingt.Helper()if this helper might be extended later.Suggested cleanup
-func newHandlerStep(_ *testing.T, name, id, command string) core.Step { +func newHandlerStep(name, id, command string) core.Step { return core.Step{ Name: name, ID: id, Commands: []core.CommandEntry{parseCommand(command)}, } }internal/core/spec/step_test.go (1)
1879-2064: Comprehensive validation tests, but missing "subworkflow" executor type.The tests thoroughly cover executor types for multiple command validation. However, per a past review comment, the "subworkflow" executor type (an alias for "dag") is missing from both the
singleCommandOnlyExecutorsmap and these tests.Consider adding a test case for "subworkflow":
{ name: "MultipleCommands_SubworkflowExecutor", executorType: "subworkflow", commands: []core.CommandEntry{ {Command: "dag1"}, {Command: "dag2"}, }, wantErr: true, },
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
internal/cmd/exec_spec.gointernal/core/spec/step.gointernal/core/spec/step_test.gointernal/core/step.gointernal/core/step_test.gointernal/integration/container_test.gointernal/integration/gha_test.gointernal/runtime/agent/reporter.gointernal/runtime/builtin/command/command_test.gointernal/runtime/env_test.gointernal/runtime/node_test.gointernal/runtime/runner_helper_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
- internal/runtime/agent/reporter.go
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*.go: Backend entrypoint incmd/orchestrates the scheduler and CLI; runtime, persistence, and service layers sit underinternal/*(for exampleinternal/runtime,internal/persistence)
Keep Go filesgofmt/goimportsclean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, andErr...names for package-level errors
Repository linting relies ongolangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers ininternal/common
Files:
internal/integration/container_test.gointernal/core/spec/step.gointernal/runtime/env_test.gointernal/runtime/builtin/command/command_test.gointernal/core/step.gointernal/cmd/exec_spec.gointernal/core/spec/step_test.gointernal/runtime/node_test.gointernal/integration/gha_test.gointernal/runtime/runner_helper_test.gointernal/core/step_test.go
**/*_test.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*_test.go: Co-locate Go tests as*_test.go; favour table-driven cases and cover failure paths
Usestretchr/testify/requireand shared fixtures frominternal/testinstead of duplicating mocks
Files:
internal/integration/container_test.gointernal/runtime/env_test.gointernal/runtime/builtin/command/command_test.gointernal/core/spec/step_test.gointernal/runtime/node_test.gointernal/integration/gha_test.gointernal/runtime/runner_helper_test.gointernal/core/step_test.go
🧠 Learnings (5)
📓 Common learnings
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*.go : Backend entrypoint in `cmd/` orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under `internal/*` (for example `internal/runtime`, `internal/persistence`)
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*_test.go : Co-locate Go tests as `*_test.go`; favour table-driven cases and cover failure paths
Applied to files:
internal/integration/container_test.gointernal/runtime/node_test.gointernal/integration/gha_test.gointernal/core/step_test.go
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*_test.go : Use `stretchr/testify/require` and shared fixtures from `internal/test` instead of duplicating mocks
Applied to files:
internal/integration/container_test.gointernal/integration/gha_test.go
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: `make test` (or `make test-coverage`) executes the Go suite via `gotestsum`; append `TEST_TARGET=./internal/...` to focus packages
Applied to files:
internal/core/spec/step_test.gointernal/integration/gha_test.go
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*.go : Backend entrypoint in `cmd/` orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under `internal/*` (for example `internal/runtime`, `internal/persistence`)
Applied to files:
internal/runtime/node_test.go
🧬 Code graph analysis (9)
internal/integration/container_test.go (1)
internal/runtime/data.go (1)
Parallel(69-73)
internal/core/spec/step.go (2)
internal/core/spec/errors.go (3)
ErrStepCommandMustBeArrayOrString(32-32)ErrStepCommandIsEmpty(31-31)ErrExecutorDoesNotSupportMultipleCmd(34-34)internal/common/cmdutil/cmdutil.go (1)
SplitCommand(142-169)
internal/runtime/env_test.go (2)
internal/common/logger/tag/tag.go (3)
Name(271-273)Command(403-405)Args(408-410)internal/core/step.go (1)
CommandEntry(120-127)
internal/runtime/builtin/command/command_test.go (3)
internal/core/step.go (2)
CommandEntry(120-127)Step(13-89)internal/runtime/builtin/command/command.go (1)
NewCommand(274-286)internal/runtime/executor/executor.go (1)
ExitCoder(50-52)
internal/core/step.go (1)
api/v2/api.gen.go (2)
CommandEntry(166-172)Step(865-922)
internal/cmd/exec_spec.go (1)
internal/common/cmdutil/eval.go (1)
BuildCommandEscapedString(73-106)
internal/core/spec/step_test.go (1)
internal/core/step.go (3)
CommandEntry(120-127)Step(13-89)ExecutorConfig(181-189)
internal/runtime/runner_helper_test.go (3)
internal/core/step.go (2)
CommandEntry(120-127)Step(13-89)internal/common/cmdutil/cmdutil.go (1)
SplitCommand(142-169)internal/runtime/runner.go (1)
Config(83-95)
internal/core/step_test.go (1)
internal/core/step.go (1)
CommandEntry(120-127)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Build
- GitHub Check: Test on ubuntu-latest
🔇 Additional comments (25)
internal/core/step_test.go (1)
280-318: LGTM! Well-structured table-driven test for CommandEntry.String().The test covers the key scenarios including empty entry, command-only, command with args, and the priority behavior where
CmdWithArgstakes precedence. This aligns with the coding guidelines for table-driven tests.internal/cmd/exec_spec.go (2)
58-59: LGTM! Command string building using cmdutil.The use of
BuildCommandEscapedStringproperly handles argument escaping and quoting, which is necessary when converting from[]stringargs to a single command string for YAML serialization.
39-43: Aligned with the new command model.The
execStepstruct now uses a singleCommand stringfield which will be parsed by the spec loader into theCommands []CommandEntrystructure during DAG loading.internal/core/step.go (3)
118-141: Well-designed CommandEntry type with proper String() method.The
CommandEntrystruct cleanly encapsulates command data, and theString()method correctly prioritizesCmdWithArgsfor display purposes, falling back to building fromCommandandArgs.
148-178: Migration logic now properly guards against dummy entries.The
UnmarshalJSONimplementation correctly applies the fix from the past review: it only populatesCommandswhen at least one legacy field (Command,Args, orCmdWithArgs) is actually set. This prevents creating dummyCommandEntryentries for non-command steps like SubDAG-only or script-only steps.
143-146: Simple and effective helper method.
HasMultipleCommands()provides a clean way to check for multi-command scenarios, which is used byNewCommandto decide between single and multi-command executors.internal/integration/gha_test.go (1)
32-38: Good defensive checks for test prerequisites.The git availability check and repository initialization are properly implemented. This setup ensures the test environment is correctly configured before running.
internal/runtime/builtin/command/command_test.go (2)
452-476: Test cases properly migrated to Commands-based model.The exit code tests correctly use
Commands: []core.CommandEntry{{...}}to construct steps, aligning with the new data model.
2296-2408: Comprehensive multi-command executor test suite.Excellent coverage of the multi-command functionality:
- Sequential execution: Verifies commands run in order with file-based output verification
- Stop on first failure: Confirms execution halts and subsequent commands don't run
- Executor type selection: Validates single command uses
commandExecutor, multiple usesmultiCommandExecutor- Exit code propagation: Ensures the failing command's exit code is properly captured
The tests follow table-driven patterns and use
require/assertfrom testify as per coding guidelines.internal/runtime/runner_helper_test.go (2)
80-91: Clean helper for parsing command strings into CommandEntry.The
parseCommandhelper correctly usescmdutil.SplitCommandto parse command strings and constructs aCommandEntrywith all fields populated. Usingpanicis appropriate here since a parse failure indicates a test authoring bug that should fail fast.
93-97: withCommand updated to use new Commands model.The
withCommandstep option now correctly builds aCommandsslice with a single entry from the parsed command.internal/runtime/env_test.go (1)
283-289: Test properly migrated to Commands-based step construction.The step construction now correctly uses
Commands: []core.CommandEntry{{...}}with all relevant fields populated, aligning with the new multi-command data model.internal/integration/container_test.go (1)
35-36: Parallelization moved to outer test level.Adding
t.Parallel()at the test function level (rather than inside each subtest) is a valid approach when subtests may share state or when sequential execution within a test function is desired. This allows the test functions themselves to run in parallel with other tests while subtests execute sequentially.Also applies to: 95-96, 523-524
internal/runtime/node_test.go (3)
1048-1061: Well-structured test helper for the new multi-command API.The
withNodeCmdArgshelper correctly usescmdutil.SplitCommandto parse command strings and populates the newCommandsslice with aCommandEntry. The use oft.Fatalffor parse errors is appropriate for test setup code.
1063-1070: LGTM!The updated
withNodeCommandhelper correctly populates theCommandsslice for simple command strings that don't require argument parsing.
109-192: Output tests properly adapted to use the new command parsing helper.The output-related tests correctly use
withNodeCmdArgsfor command strings that contain quotes, escapes, and special characters, ensuring proper parsing throughcmdutil.SplitCommand.internal/core/spec/step_test.go (4)
837-895: Excellent test coverage for the refactored buildStepCommand function.The test properly verifies:
- Single commands populate the
Commandsslice- Multiline commands become scripts
- Legacy fields (
Command,Args,CmdWithArgs) remain empty
939-945: Verify pipe handling behavior is intentional.The test shows that
cat file.txt | grep patternis parsed with|,grep, andpatternas separate args. This is expected behavior fromcmdutil.SplitCommandbut means pipes won't work as shell pipes during direct command execution. Ensure this is the intended behavior, as users may expect shell-like pipe support.
1012-1090: Comprehensive unit tests for buildSingleCommand.Good coverage of command parsing scenarios including multiline scripts, quoted arguments, and error cases for empty/whitespace-only commands.
1169-1215: LGTM!Clear and concise tests for the
HasMultipleCommands()helper method oncore.Step.internal/core/spec/step.go (5)
208-210: LGTM!The validation is correctly placed after
buildStepCommandto validate the parsed commands against the executor type.
582-601: Clean implementation of command type dispatching.The function correctly handles the three cases: nil (no-op), string (single command), and array (multiple commands), with appropriate error handling for invalid types.
604-634: Well-implemented single command parsing.The function correctly:
- Trims whitespace and validates non-empty
- Treats multiline strings as scripts
- Uses
cmdutil.SplitCommandfor proper shell-like parsing- Populates only the new
Commandsfield, leaving legacy fields empty
636-689: Robust multiple command parsing with good edge case handling.The implementation properly:
- Converts primitive types to strings
- Rejects complex types (maps, arrays) with clear error messages
- Skips empty commands silently
- Validates that at least one valid command exists
Minor consideration: The type switch handles
int, int64, uint64, float64, boolbut notuintorint32. YAML parsers typically unmarshal tointorint64, so this should be sufficient.
706-726: LGTM!The validation function is correctly implemented with clear logic and informative error messages. The early return for single/no commands is efficient.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
internal/core/spec/step.go (2)
660-673: Consider stricter type validation for command array elements.Lines 665-666 coerce primitive types (int, float64, bool) to strings. While this provides flexibility, it might mask configuration errors where users accidentally provide non-string values. Consider whether this permissiveness aligns with your validation philosophy.
676-678: Consider logging or warning when skipping empty commands.Line 677 silently skips empty command entries with a
continue. While this is defensive, it could hide configuration mistakes (e.g., a user accidentally including empty strings in the array). Consider whether a warning would be appropriate.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
internal/core/capabilities.gointernal/core/spec/step.gointernal/core/spec/step_test.gointernal/runtime/builtin/archive/archive.gointernal/runtime/builtin/command/command.gointernal/runtime/builtin/dag/dag.gointernal/runtime/builtin/dag/parallel.gointernal/runtime/builtin/docker/executor.gointernal/runtime/builtin/gha/executor.gointernal/runtime/builtin/http/http.gointernal/runtime/builtin/jq/jq.gointernal/runtime/builtin/mail/mail.gointernal/runtime/builtin/ssh/ssh.gointernal/runtime/builtin/ssh/ssh_test.gointernal/runtime/executor/executor.go
💤 Files with no reviewable changes (1)
- internal/runtime/builtin/ssh/ssh_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
- internal/runtime/builtin/archive/archive.go
- internal/runtime/builtin/http/http.go
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*.go: Backend entrypoint incmd/orchestrates the scheduler and CLI; runtime, persistence, and service layers sit underinternal/*(for exampleinternal/runtime,internal/persistence)
Keep Go filesgofmt/goimportsclean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, andErr...names for package-level errors
Repository linting relies ongolangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers ininternal/common
Files:
internal/runtime/builtin/dag/dag.gointernal/runtime/builtin/command/command.gointernal/runtime/builtin/dag/parallel.gointernal/runtime/executor/executor.gointernal/runtime/builtin/gha/executor.gointernal/core/spec/step_test.gointernal/core/capabilities.gointernal/runtime/builtin/docker/executor.gointernal/core/spec/step.gointernal/runtime/builtin/ssh/ssh.gointernal/runtime/builtin/mail/mail.gointernal/runtime/builtin/jq/jq.go
**/*_test.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*_test.go: Co-locate Go tests as*_test.go; favour table-driven cases and cover failure paths
Usestretchr/testify/requireand shared fixtures frominternal/testinstead of duplicating mocks
Files:
internal/core/spec/step_test.go
🧠 Learnings (4)
📓 Common learnings
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*.go : Backend entrypoint in `cmd/` orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under `internal/*` (for example `internal/runtime`, `internal/persistence`)
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: `make test` (or `make test-coverage`) executes the Go suite via `gotestsum`; append `TEST_TARGET=./internal/...` to focus packages
Applied to files:
internal/core/spec/step_test.go
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*_test.go : Co-locate Go tests as `*_test.go`; favour table-driven cases and cover failure paths
Applied to files:
internal/core/spec/step_test.go
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*_test.go : Use `stretchr/testify/require` and shared fixtures from `internal/test` instead of duplicating mocks
Applied to files:
internal/core/spec/step_test.go
🧬 Code graph analysis (11)
internal/runtime/builtin/dag/dag.go (2)
internal/core/capabilities.go (1)
ExecutorCapabilities(4-17)internal/runtime/executor/executor.go (1)
RegisterExecutor(40-46)
internal/runtime/builtin/dag/parallel.go (4)
internal/runtime/executor/executor.go (1)
RegisterExecutor(40-46)internal/core/step.go (2)
ExecutorTypeParallel(313-313)SubDAG(113-116)internal/core/capabilities.go (1)
ExecutorCapabilities(4-17)internal/common/logger/tag/tag.go (1)
SubDAG(35-37)
internal/runtime/executor/executor.go (2)
internal/core/validator.go (2)
StepValidator(32-32)RegisterStepValidator(36-38)internal/core/capabilities.go (2)
ExecutorCapabilities(4-17)RegisterExecutorCapabilities(44-46)
internal/runtime/builtin/gha/executor.go (2)
internal/core/capabilities.go (1)
ExecutorCapabilities(4-17)internal/runtime/executor/executor.go (1)
RegisterExecutor(40-46)
internal/core/spec/step_test.go (2)
internal/core/capabilities.go (2)
RegisterExecutorCapabilities(44-46)ExecutorCapabilities(4-17)internal/core/step.go (4)
SubDAG(113-116)CommandEntry(120-127)Step(13-89)ExecutorConfig(181-189)
internal/core/capabilities.go (1)
internal/runtime/builtin/command/shell.go (1)
Shell(13-20)
internal/runtime/builtin/docker/executor.go (3)
internal/runtime/builtin/docker/client.go (1)
ExecOptions(89-92)internal/core/capabilities.go (1)
ExecutorCapabilities(4-17)internal/runtime/executor/executor.go (1)
RegisterExecutor(40-46)
internal/core/spec/step.go (5)
internal/core/spec/errors.go (3)
ErrStepCommandMustBeArrayOrString(32-32)ErrStepCommandIsEmpty(31-31)ErrExecutorDoesNotSupportMultipleCmd(34-34)internal/core/step.go (3)
Step(13-89)CommandEntry(120-127)SubDAG(113-116)internal/common/cmdutil/cmdutil.go (1)
SplitCommand(142-169)internal/core/capabilities.go (6)
SupportsCommand(49-51)SupportsMultipleCommands(54-56)SupportsScript(59-61)SupportsShell(64-66)SupportsContainer(69-71)SupportsSubDAG(74-76)internal/runtime/builtin/command/shell.go (1)
Shell(13-20)
internal/runtime/builtin/ssh/ssh.go (2)
internal/core/capabilities.go (1)
ExecutorCapabilities(4-17)internal/runtime/executor/executor.go (1)
RegisterExecutor(40-46)
internal/runtime/builtin/mail/mail.go (2)
internal/runtime/executor/executor.go (1)
RegisterExecutor(40-46)internal/core/capabilities.go (1)
ExecutorCapabilities(4-17)
internal/runtime/builtin/jq/jq.go (2)
internal/runtime/executor/executor.go (1)
RegisterExecutor(40-46)internal/core/capabilities.go (1)
ExecutorCapabilities(4-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Test on ubuntu-latest
- GitHub Check: Build
🔇 Additional comments (29)
internal/runtime/builtin/dag/parallel.go (1)
315-315: LGTM!The parallel executor correctly registers with
SubDAG: truecapability, which accurately reflects its purpose of executing sub-DAGs in parallel. This is consistent with the dag executor registration pattern.internal/runtime/executor/executor.go (1)
39-46: LGTM!The updated
RegisterExecutorfunction properly integrates the capabilities system. The unconditional call tocore.RegisterExecutorCapabilitiesis correct since the zero-value ofExecutorCapabilities(all fieldsfalse) is a valid state representing an executor with no special capabilities.internal/runtime/builtin/gha/executor.go (3)
210-219: LGTM!The command extraction properly migrates from the legacy
step.Commandfield to the newstep.Commandsslice. The nil-check before array access (len(step.Commands) > 0) prevents potential panics.
531-538: LGTM!Consistent command extraction pattern with
generateWorkflowYAML. Both functions use the same guard and access pattern for the Commands slice.
547-551: LGTM!The GHA executor correctly registers with
Command: trueonly (notMultipleCommands), which aligns with its design to execute a single GitHub Action per step.internal/core/spec/step_test.go (6)
16-51: LGTM!Excellent test setup using
TestMainto register executor capabilities before tests run. This ensures tests are isolated from runtimeinit()functions and explicitly defines the capabilities needed for validation tests. As per coding guidelines, this follows good Go testing practices.
875-933: LGTM!The test correctly validates that:
- Single-line commands populate
Commandsslice (not legacy fields)- Multi-line commands become
Scriptinstead- Legacy fields (
Command,Args,CmdWithArgs) remain emptyThis ensures backward-compatible parsing while migrating to the new data model.
935-1048: LGTM!Comprehensive table-driven tests for multiple command parsing covering:
- Array of commands with various argument forms
- Quoted arguments preservation
- Empty/whitespace handling
- Non-string type conversion
Good coverage of edge cases. As per coding guidelines, the table-driven approach is preferred for Go tests.
976-983: Consider shell handling for pipe characters.The test expects
cat file.txt | grep patternto parse as separate args["file.txt", "|", "grep", "pattern"]. This is technically correct for the current tokenization, but pipe handling typically requires shell execution. If users expect shell semantics, they should use thescriptfield or ensure the shell executor is invoked.This test documents the current behavior, which is acceptable.
1917-2102: LGTM!Thorough validation tests for
validateMultipleCommandscovering:
- Single command always passes
- Multiple commands pass for multi-command executors (shell, docker, ssh)
- Multiple commands fail for single-command executors (jq, http, archive, gha, mail, dag)
- Empty commands always pass
Test cases align with the capabilities registered in
TestMain.
2104-2198: LGTM!Validation tests for script support cover all executor types appropriately, including the case where SSH does not support script (as per registered capabilities).
internal/core/capabilities.go (2)
1-17: LGTM!Clean capability definition struct with well-documented fields. The boolean flags provide a clear contract for what each executor supports.
43-76: LGTM!Public registration function and convenience accessors provide a clean API for the capabilities system. The one-liner accessors reduce boilerplate across the codebase.
internal/runtime/builtin/jq/jq.go (2)
43-55: LGTM!The jq executor correctly extracts the query from
Commands[0].CmdWithArgs, preserving the original query string which may contain spaces and special characters. UsingCmdWithArgsinstead ofCommandis the right choice here.
147-148: LGTM!Registration with
Command: true, Script: truecorrectly reflects the jq executor's usage pattern: the command field holds the jq query, and the script field holds the JSON input to process.internal/runtime/builtin/mail/mail.go (1)
124-125: LGTM!The mail executor correctly registers with an empty
ExecutorCapabilities{}, indicating it doesn't support command, script, shell, or other step-level features. All mail configuration comes from the executor config block, which is the appropriate pattern for this executor type.internal/runtime/builtin/dag/dag.go (1)
149-153: LGTM!Clean registration of both
subworkflowanddagexecutor types with sharedSubDAG: truecapability. Reusing thecapsvariable is efficient and ensures consistency between the two aliases.internal/runtime/builtin/command/command.go (3)
328-336: LGTM!The executor capabilities registration correctly advertises support for Command, MultipleCommands, Script, and Shell, enabling the multi-command execution path.
296-305: Potential index out of bounds when accessing step.Commands[0].The code accesses
step.Commands[0]without verifying thatstep.Commandsis non-empty. While upstream validation may ensure at least one command exists, defensive programming would prevent a potential panic.🔎 Suggested fix
func NewCommandConfig(ctx context.Context, step core.Step) (*commandConfig, error) { env := runtime.GetEnv(ctx) var command string var args []string var shellCmdArgs string // Extract command and args from the first command entry - if len(step.Commands) > 0 { + if len(step.Commands) > 0 { command = step.Commands[0].Command args = step.Commands[0].Args shellCmdArgs = step.Commands[0].CmdWithArgs }Actually, the current code already has the length check on line 301. Let me reconsider...
Likely an incorrect or invalid review comment.
307-310: The backward compatibility forstep.ShellCmdArgsis already properly implemented.The field is defined in the
Stepstruct with standard JSON tags and will be automatically unmarshalled from DAG files. The fallback logic at lines 308–310 is intentional and tested; it ensures that legacy DAGs usingShellCmdArgsinstead of theCommandsarray continue to work. This pattern mirrors the deprecatedCommandfield, which is explicitly documented as kept "for JSON backward compatibility."internal/runtime/builtin/ssh/ssh.go (2)
84-88: LGTM!Early return for empty commands is a clean guard clause.
125-130: Validation correctly moved to spec layer with capability-based checks.The SSH executor registration with
nilvalidator is appropriate. The spec layer (internal/core/spec/step.go) validates executor capabilities usingcore.SupportsCommand(),core.SupportsMultipleCommands(), andcore.SupportsShell()checks, which query the capabilities registered viaRegisterExecutor. No SSH-specific validation logic was lost—validation has been generalized to a capability-based approach applied uniformly across all executors.internal/runtime/builtin/docker/executor.go (3)
186-200: LGTM!The empty commands case correctly uses
nilto invoke the image's default entrypoint, with proper error handling and stderr tail augmentation.
255-279: LGTM!Single command path uses the straightforward
Runapproach with proper error handling and stderr tail reporting.
486-494: LGTM!Executor capabilities registration correctly advertises support for Command, MultipleCommands, Script, Shell, and Container.
internal/core/spec/step.go (4)
597-617: LGTM!Clean routing logic for single vs. multiple commands with proper type validation.
619-649: LGTM!The single command builder correctly handles multi-line strings as scripts and properly splits command strings into Command/Args.
706-795: LGTM!The validation functions follow a consistent pattern and properly leverage the capability system to enforce executor constraints. Clean separation of concerns between parsing and validation.
208-225: Validation sequence is well-structured.The new validation calls (validateCommand, validateMultipleCommands, validateScript, validateShell, validateContainer, validateSubDAG) provide clear capability checking after command building. This enforces executor constraints early in the build process.
| slog.Any("cmd", cmd), | ||
| ) | ||
|
|
||
| exitCode, err := e.container.Exec(ctx, cmd, e.stdout, e.stderr, ExecOptions{}) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the executor.go file around the mentioned lines
wc -l internal/runtime/builtin/docker/executor.goRepository: dagu-org/dagu
Length of output: 104
🏁 Script executed:
# Read the file to understand the context
cat -n internal/runtime/builtin/docker/executor.go | sed -n '180,195p'Repository: dagu-org/dagu
Length of output: 693
🏁 Script executed:
# Read around line 316
cat -n internal/runtime/builtin/docker/executor.go | sed -n '310,325p'Repository: dagu-org/dagu
Length of output: 620
🏁 Script executed:
# Search for ExecOptions definition and WorkingDir usage
rg -n "type ExecOptions|struct.*ExecOptions" --type=go -A 10Repository: dagu-org/dagu
Length of output: 1522
🏁 Script executed:
# Check how ExecOptions is used throughout the codebase
rg -n "ExecOptions{" --type=goRepository: dagu-org/dagu
Length of output: 144
🏁 Script executed:
# Search for where WorkingDir is actually set/populated
rg -n "WorkingDir\s*=" --type=go internal/runtime/builtin/docker/Repository: dagu-org/dagu
Length of output: 339
🏁 Script executed:
# Check the step object to see if it has working directory info
rg -n "type Step|WorkingDir" --type=go internal/executor/ -A 3Repository: dagu-org/dagu
Length of output: 115
🏁 Script executed:
# Look for the full context of line 188 function to see what happens with execOpts
cat -n internal/runtime/builtin/docker/executor.go | sed -n '186,210p'Repository: dagu-org/dagu
Length of output: 1106
🏁 Script executed:
# Check what happens after execOpts is declared at line 188
cat -n internal/runtime/builtin/docker/executor.go | sed -n '186,250p'Repository: dagu-org/dagu
Length of output: 2527
🏁 Script executed:
# Look at the function containing line 316 to understand context
cat -n internal/runtime/builtin/docker/executor.go | sed -n '290,330p'Repository: dagu-org/dagu
Length of output: 1494
🏁 Script executed:
# Check if e.step has a WorkingDir field
rg -n "type.*Step|WorkingDir" --type=go -B 2 -A 10 internal/runtime/builtin/docker/executor.go | head -60Repository: dagu-org/dagu
Length of output: 1942
🏁 Script executed:
# Check if execOpts at line 188 ever gets its WorkingDir set
cat -n internal/runtime/builtin/docker/executor.go | sed -n '188,235p'Repository: dagu-org/dagu
Length of output: 1626
🏁 Script executed:
# Look at the broader context to see if step.WorkingDir exists
rg -n "e.step\\.WorkingDir|step.*WorkingDir" --type=goRepository: dagu-org/dagu
Length of output: 146
🏁 Script executed:
# Check if e.step has WorkingDir or WorkingDirectory field
rg -n "type.*Step.*struct|WorkingDir" --type=go -A 20 | grep -A 20 "type.*Step"Repository: dagu-org/dagu
Length of output: 30787
🏁 Script executed:
# Look at what the Step type is in the docker executor
grep -n "e.step" internal/runtime/builtin/docker/executor.go | head -20Repository: dagu-org/dagu
Length of output: 785
🏁 Script executed:
# Find Step type definition
fd -e go | xargs grep -l "type Step struct" --type=goRepository: dagu-org/dagu
Length of output: 179
🏁 Script executed:
# Check client.go line 665 to see where WorkingDir is being set
cat -n internal/runtime/builtin/docker/client.go | sed -n '660,675p'Repository: dagu-org/dagu
Length of output: 628
🏁 Script executed:
# Check the full Step type definition
cat -n internal/core/step.go | head -80Repository: dagu-org/dagu
Length of output: 4661
🏁 Script executed:
# Now check if there's any usage of step.Dir in the docker executor
grep -n "step.Dir\|Dir\s*=" internal/runtime/builtin/docker/executor.goRepository: dagu-org/dagu
Length of output: 39
🏁 Script executed:
# Let's examine the complete runInExistingContainer and runInNewContainer functions
cat -n internal/runtime/builtin/docker/executor.go | sed -n '186,236p'Repository: dagu-org/dagu
Length of output: 1839
🏁 Script executed:
# Check if WorkingDir should be derived from e.step.Dir in executor
cat -n internal/runtime/builtin/docker/executor.go | sed -n '1,50p'Repository: dagu-org/dagu
Length of output: 1429
🏁 Script executed:
# Check if there's any special handling of step.Dir in docker executor initialization
grep -n "step\.Dir\|WorkingDir" internal/runtime/builtin/docker/executor.go | head -20Repository: dagu-org/dagu
Length of output: 270
🏁 Script executed:
# Look at the broader context - how is step.Dir expected to be used?
cat -n internal/runtime/builtin/docker/executor.go | sed -n '238,280p'Repository: dagu-org/dagu
Length of output: 1659
🏁 Script executed:
# Check the Container config structure to see if WorkingDir is set from step.Dir
rg -n "type Config|WorkingDir" internal/runtime/builtin/docker/config.go -A 3 | head -60Repository: dagu-org/dagu
Length of output: 737
🏁 Script executed:
# Check the full context around line 188 and see if execOpts is ever modified after initialization
cat -n internal/runtime/builtin/docker/executor.go | sed -n '186,220p'Repository: dagu-org/dagu
Length of output: 1399
🏁 Script executed:
# Check if there's a pattern in other executors for handling step.Dir
find internal/runtime/builtin -name "*.go" -exec grep -l "step.Dir" {} \;Repository: dagu-org/dagu
Length of output: 39
🏁 Script executed:
# Look at the entire runInNewContainer function to see all usages of ExecOptions{}
cat -n internal/runtime/builtin/docker/executor.go | sed -n '238,350p'Repository: dagu-org/dagu
Length of output: 4318
🏁 Script executed:
# Search for where e.step.Dir is actually used in the docker executor
grep -n "\.Dir" internal/runtime/builtin/docker/executor.goRepository: dagu-org/dagu
Length of output: 39
🏁 Script executed:
# Check if step.Dir is supposed to be passed through the Config/Container mechanism
cat -n internal/runtime/builtin/docker/config.go | sed -n '130,180p'Repository: dagu-org/dagu
Length of output: 1956
🏁 Script executed:
# Look at the docker executor initialization to see if step.Dir is handled there
cat -n internal/runtime/builtin/docker/executor.go | sed -n '344,410p'Repository: dagu-org/dagu
Length of output: 2767
🏁 Script executed:
# Check if step.Dir is supposed to be used via the Container config mechanism
# Look at the entire Step structure and how Dir is used
grep -n "step\.Dir\|e\.step\.Dir" internal/runtime/builtin/docker/*.goRepository: dagu-org/dagu
Length of output: 39
🏁 Script executed:
# Check if there are any tests that show the expected behavior
find internal/runtime/builtin/docker -name "*test.go" -exec grep -l "Dir\|WorkingDir" {} \;Repository: dagu-org/dagu
Length of output: 198
🏁 Script executed:
# Look at whether step.Dir is supposed to be merged into Container config
cat -n internal/runtime/builtin/docker/executor.go | sed -n '350,395p'Repository: dagu-org/dagu
Length of output: 2143
🏁 Script executed:
# Check if step.Dir is supposed to be applied to the Container working directory
cat -n internal/runtime/builtin/docker/executor.go | sed -n '344,390p'Repository: dagu-org/dagu
Length of output: 2032
🏁 Script executed:
# Look at tests to understand expected behavior with step.Dir
cat -n internal/runtime/builtin/docker/client_test.go | head -100Repository: dagu-org/dagu
Length of output: 3594
🏁 Script executed:
# Check if there's any handling of step.Dir in the executor or if it's missing
rg -n "step.Dir" internal/runtime/builtin/docker/ -C 3Repository: dagu-org/dagu
Length of output: 39
🏁 Script executed:
# Check client.go to see the full Exec function signature and how WorkingDir is used
cat -n internal/runtime/builtin/docker/client.go | sed -n '650,680p'Repository: dagu-org/dagu
Length of output: 1148
🏁 Script executed:
# Look at the complete Exec function to understand how WorkingDir parameter works
rg -n "func.*Exec.*ExecOptions" internal/runtime/builtin/docker/client.go -A 20Repository: dagu-org/dagu
Length of output: 860
🏁 Script executed:
# Check if step.Dir should be applied anywhere in the executor
cat -n internal/runtime/builtin/docker/executor.go | sed -n '100,150p'Repository: dagu-org/dagu
Length of output: 1759
🏁 Script executed:
# Check if there's code that applies step.Dir to the Container before loading config
grep -n "step.Dir\|step\.Container" internal/runtime/builtin/docker/executor.go | head -30Repository: dagu-org/dagu
Length of output: 153
🏁 Script executed:
# Look for how other executors handle step.Dir
find internal/runtime/builtin -name "*.go" -type f -exec grep -l "step.Dir" {} \;Repository: dagu-org/dagu
Length of output: 39
🏁 Script executed:
# Check the complete initialization to see if step.Dir is applied anywhere
cat -n internal/runtime/builtin/docker/executor.go | sed -n '344,390p'Repository: dagu-org/dagu
Length of output: 2032
🏁 Script executed:
# See if step.Dir should be applied to Container.WorkingDir before loading config
rg -n "Container.WorkingDir|step.Dir" internal/runtime/builtin/docker/ -B 2 -A 2Repository: dagu-org/dagu
Length of output: 1603
🏁 Script executed:
# Final confirmation: check if step.Dir is actually available at line 316 context
cat -n internal/runtime/builtin/docker/executor.go | sed -n '280,320p'Repository: dagu-org/dagu
Length of output: 1723
Add step working directory to ExecOptions for both existing and new container execution paths.
At line 316 (and line 188 in runInExistingContainer), ExecOptions{} is passed with an empty WorkingDir field, ignoring e.step.Dir. The Client.Exec function explicitly handles WorkingDir override (client.go:664-665), and step-level configuration is already being merged for environment variables (line 368). For consistency, both execution paths should populate ExecOptions.WorkingDir from step.Dir when available:
execOpts := ExecOptions{WorkingDir: e.step.Dir}🤖 Prompt for AI Agents
In internal/runtime/builtin/docker/executor.go around lines 188 and 316,
ExecOptions is being passed with an empty WorkingDir which ignores e.step.Dir;
update both execution paths to populate ExecOptions.WorkingDir from the step by
creating execOpts (or equivalent) with WorkingDir: e.step.Dir and pass that to
e.container.Exec so the client-side WorkingDir override is respected and matches
how environment variables are merged.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
internal/core/capabilities.go (1)
35-43: The permissive default fallback has already been flagged.This concern was raised in a previous review: the default capabilities
{Command: true, MultipleCommands: true}for unregistered executor types allows invalid configurations to pass validation, deferring errors to runtime. The suggestion to returnExecutorCapabilities{}(all false) remains valid.
🧹 Nitpick comments (1)
internal/core/capabilities.go (1)
30-33: Consider validating executorType parameter.The
Registermethod doesn't validate thatexecutorTypeis non-empty. While likely not an issue in practice (since registrations are explicit in code), adding a check could prevent subtle bugs.🔎 Optional validation to add
func (r *executorCapabilitiesRegistry) Register(executorType string, caps ExecutorCapabilities) { + if executorType == "" { + panic("executorType cannot be empty") + } r.caps[executorType] = caps }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
internal/core/capabilities.gointernal/core/spec/errors.gointernal/core/spec/step.gointernal/core/spec/step_test.gointernal/runtime/builtin/dag/dag.gointernal/runtime/builtin/dag/parallel.go
🚧 Files skipped from review as they are similar to previous changes (3)
- internal/runtime/builtin/dag/parallel.go
- internal/core/spec/errors.go
- internal/runtime/builtin/dag/dag.go
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*.go: Backend entrypoint incmd/orchestrates the scheduler and CLI; runtime, persistence, and service layers sit underinternal/*(for exampleinternal/runtime,internal/persistence)
Keep Go filesgofmt/goimportsclean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, andErr...names for package-level errors
Repository linting relies ongolangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers ininternal/common
Files:
internal/core/spec/step_test.gointernal/core/capabilities.gointernal/core/spec/step.go
**/*_test.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*_test.go: Co-locate Go tests as*_test.go; favour table-driven cases and cover failure paths
Usestretchr/testify/requireand shared fixtures frominternal/testinstead of duplicating mocks
Files:
internal/core/spec/step_test.go
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*.go : Backend entrypoint in `cmd/` orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under `internal/*` (for example `internal/runtime`, `internal/persistence`)
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: `make test` (or `make test-coverage`) executes the Go suite via `gotestsum`; append `TEST_TARGET=./internal/...` to focus packages
Applied to files:
internal/core/spec/step_test.go
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*_test.go : Use `stretchr/testify/require` and shared fixtures from `internal/test` instead of duplicating mocks
Applied to files:
internal/core/spec/step_test.go
🧬 Code graph analysis (2)
internal/core/capabilities.go (1)
internal/runtime/builtin/command/shell.go (1)
Shell(13-20)
internal/core/spec/step.go (4)
internal/core/spec/errors.go (8)
ErrStepCommandMustBeArrayOrString(32-32)ErrStepCommandIsEmpty(31-31)ErrExecutorDoesNotSupportMultipleCmd(34-34)ErrSubDAGAndExecutorConflict(35-35)ErrSubDAGAndCommandConflict(36-36)ErrSubDAGAndScriptConflict(37-37)ErrContainerAndExecutorConflict(27-27)ErrContainerAndScriptConflict(28-28)internal/core/step.go (4)
Step(13-89)CommandEntry(120-127)ExecutorConfig(181-189)SubDAG(113-116)internal/common/cmdutil/cmdutil.go (1)
SplitCommand(142-169)internal/core/capabilities.go (7)
SupportsCommand(51-53)SupportsMultipleCommands(56-58)SupportsScript(61-63)SupportsShell(66-68)SupportsContainer(71-73)SupportsSubDAG(76-78)SupportsWorkerSelector(81-83)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Test on ubuntu-latest
- GitHub Check: Build
🔇 Additional comments (14)
internal/core/capabilities.go (3)
3-19: Well-designed capability model.The
ExecutorCapabilitiesstruct clearly defines executor features with well-documented boolean fields. The field names are intuitive and the structure supports the multi-command and executor-specific validation requirements.
45-83: Clean and consistent public API.The public API functions (
RegisterExecutorCapabilitiesand theSupports*accessors) provide a clear, consistent interface for capability checks. The naming is intuitive and the implementation is straightforward.
21-28: Thread safety is properly handled by init()-time registration.All
RegisterExecutorCapabilitiescalls occur exclusively ininit()functions across the builtin executors (command,docker,ssh,jq,http,archive,gha,dag,parallel). Since Go serializes allinit()functions beforemain()executes, all registrations complete before any concurrent access. After initialization, the map is only read via the publicGet()andSupports*()functions, never written to. This pattern is safe and idiomatic Go.internal/core/spec/step_test.go (5)
877-935: LGTM - Correct validation of new command model.The test correctly validates that:
- Commands slice is populated with the new CommandEntry structure
- Legacy fields (Command, Args, CmdWithArgs) remain empty after build
- Multiline commands are treated as scripts
This aligns with the design where Commands is the source of truth and legacy fields are kept only for JSON backward compatibility.
937-1050: Excellent comprehensive coverage for multiple commands.This test suite thoroughly covers:
- Multiple command parsing with various argument forms
- Pipe handling in commands
- Empty command filtering
- Type coercion (primitives to strings)
- Edge cases and error conditions
The test data is well-structured and validates both the Commands slice and the expected emptiness of legacy fields.
1870-2577: LGTM - Comprehensive executor capability validation tests.The validation test suite systematically covers all executor capabilities:
- Multiple commands support (shell, docker, ssh support; jq, http, archive don't)
- Script support (command, shell, docker, jq, http support; ssh, mail, archive don't)
- Shell configuration support (command, docker, ssh support; jq, http, mail don't)
- Container support (docker/container support; others don't)
- Sub-DAG support (dag, parallel support; others don't)
- Command support (most support; dag, subworkflow, parallel, mail don't)
- Worker selector support (dag, subworkflow, parallel support; others don't)
The test pattern is consistent and thorough, checking both supported and unsupported cases plus edge cases.
2579-2653: LGTM - Thorough conflict validation tests.The test correctly validates mutual exclusivity constraints:
- Sub-DAG fields (call/run/parallel) cannot coexist with executor, command, script, or container
- Container cannot coexist with executor or script
This ensures users get clear validation errors when specifying incompatible configurations.
16-53: TestMain capability registrations correctly mirror production. All executor types in TestMain match their production counterparts in internal/runtime/builtin, confirming the test environment accurately reflects production behavior.internal/core/spec/step.go (6)
195-241: LGTM - Well-structured validation pipeline.The validation pipeline is properly ordered:
- Early conflict validation (line 196) - Checks mutual exclusivity before building
- Action-defining transformations (lines 201-218) - Builds container, parallel, subDAG, executor, command, params
- Final capability validators (lines 221-240) - Validates executor capabilities after executor type is determined
This ordering ensures executor type is known before validating capability-dependent fields.
604-624: LGTM - Clean command parsing dispatch.The function cleanly handles the three cases:
- Nil command - no-op
- String command - delegates to buildSingleCommand
- Array command - delegates to buildMultipleCommands
Clear error message for invalid types.
627-656: LGTM - Correct single command parsing.The function correctly:
- Validates non-empty input
- Treats multiline strings as scripts
- Parses single-line commands into command and args
- Populates the Commands slice (not legacy fields)
The use of
cmdutil.SplitCommandensures consistent parsing including pipe handling.
660-711: LGTM - Flexible multiple command parsing.The function provides good flexibility:
- Accepts arrays of strings and primitives (converted to strings)
- Filters out empty entries automatically
- Provides clear error messages with array indices
- Validates final result is non-empty
The type coercion (lines 672-673) is useful for YAML where numbers might not be quoted.
713-817: LGTM - Consistent capability-based validation.All validation functions follow a consistent pattern:
- Early return if field is empty/nil
- Check capability via
core.Supports*()functions- Return clear validation error if unsupported
This leverages the executor capability registry introduced in the PR, providing centralized and maintainable validation logic.
819-850: LGTM - Clear conflict validation logic.The function effectively prevents incompatible field combinations:
Sub-DAG conflicts (lines 827-840):
- Sub-DAG fields (call/run/parallel) cannot combine with executor, command, script, or container
- Ensures sub-DAG execution mode is not mixed with other execution modes
Container conflicts (lines 842-847):
- Container cannot combine with executor (container itself implies executor type)
- Container cannot combine with script (enforces use of command field instead)
The use of predefined error constants (e.g.,
ErrSubDAGAndExecutorConflict) ensures consistent error messages across the codebase.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
internal/runtime/builtin/ssh/ssh.go (1)
114-114: Consider capturing session close errors for diagnostics.While ignoring
session.Close()errors is common practice, capturing them (e.g., via logging) could help diagnose connection issues in production when a command fails.💡 Optional improvement
- _ = session.Close() + if closeErr := session.Close(); closeErr != nil && err == nil { + err = fmt.Errorf("command %d: failed to close session: %w", i+1, closeErr) + }internal/core/spec/step_test.go (1)
1871-2578: Consider a helper function to reduce test duplication.The validation test functions (TestValidateMultipleCommands, TestValidateScript, TestValidateShell, etc.) follow an identical pattern with significant duplication. While the current explicit approach is clear, a helper function could reduce boilerplate:
💡 Optional refactoring suggestion
// Helper function to reduce duplication across validation tests func testValidation(t *testing.T, tests []struct { name string executorType string setupStep func(*core.Step) wantErr bool errContains string }, validateFunc func(*core.Step) error) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() step := &core.Step{ ExecutorConfig: core.ExecutorConfig{Type: tt.executorType}, } if tt.setupStep != nil { tt.setupStep(step) } err := validateFunc(step) if tt.wantErr { assert.Error(t, err) if tt.errContains != "" { assert.Contains(t, err.Error(), tt.errContains) } } else { assert.NoError(t, err) } }) } }This would significantly reduce the ~1400+ lines of validation test code while maintaining clarity.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
internal/common/cmdutil/shell.gointernal/common/cmdutil/shell_test.gointernal/core/capabilities.gointernal/core/capabilities_test.gointernal/core/spec/step_test.gointernal/runtime/builtin/ssh/ssh.gointernal/runtime/builtin/ssh/ssh_test.go
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*.go: Backend entrypoint incmd/orchestrates the scheduler and CLI; runtime, persistence, and service layers sit underinternal/*(for exampleinternal/runtime,internal/persistence)
Keep Go filesgofmt/goimportsclean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, andErr...names for package-level errors
Repository linting relies ongolangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers ininternal/common
Files:
internal/common/cmdutil/shell_test.gointernal/common/cmdutil/shell.gointernal/core/spec/step_test.gointernal/runtime/builtin/ssh/ssh_test.gointernal/core/capabilities_test.gointernal/core/capabilities.gointernal/runtime/builtin/ssh/ssh.go
**/*_test.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*_test.go: Co-locate Go tests as*_test.go; favour table-driven cases and cover failure paths
Usestretchr/testify/requireand shared fixtures frominternal/testinstead of duplicating mocks
Files:
internal/common/cmdutil/shell_test.gointernal/core/spec/step_test.gointernal/runtime/builtin/ssh/ssh_test.gointernal/core/capabilities_test.go
🧠 Learnings (5)
📓 Common learnings
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*.go : Backend entrypoint in `cmd/` orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under `internal/*` (for example `internal/runtime`, `internal/persistence`)
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: `make test` (or `make test-coverage`) executes the Go suite via `gotestsum`; append `TEST_TARGET=./internal/...` to focus packages
Applied to files:
internal/common/cmdutil/shell_test.gointernal/core/spec/step_test.gointernal/core/capabilities_test.go
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*_test.go : Co-locate Go tests as `*_test.go`; favour table-driven cases and cover failure paths
Applied to files:
internal/common/cmdutil/shell_test.gointernal/core/spec/step_test.gointernal/core/capabilities_test.go
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*_test.go : Use `stretchr/testify/require` and shared fixtures from `internal/test` instead of duplicating mocks
Applied to files:
internal/core/spec/step_test.gointernal/runtime/builtin/ssh/ssh_test.gointernal/core/capabilities_test.go
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*.go : Backend entrypoint in `cmd/` orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under `internal/*` (for example `internal/runtime`, `internal/persistence`)
Applied to files:
internal/runtime/builtin/ssh/ssh.go
🧬 Code graph analysis (6)
internal/common/cmdutil/shell_test.go (1)
internal/common/cmdutil/shell.go (2)
ShellQuote(8-34)ShellQuoteArgs(37-43)
internal/core/spec/step_test.go (3)
internal/core/capabilities.go (2)
RegisterExecutorCapabilities(46-48)ExecutorCapabilities(4-19)internal/core/step.go (4)
SubDAG(113-116)CommandEntry(120-127)Step(13-89)ExecutorConfig(181-189)internal/core/spec/errors.go (5)
ErrSubDAGAndExecutorConflict(35-35)ErrSubDAGAndCommandConflict(36-36)ErrSubDAGAndScriptConflict(37-37)ErrContainerAndExecutorConflict(27-27)ErrContainerAndScriptConflict(28-28)
internal/runtime/builtin/ssh/ssh_test.go (1)
internal/common/cmdutil/shell.go (2)
ShellQuote(8-34)ShellQuoteArgs(37-43)
internal/core/capabilities_test.go (1)
internal/core/capabilities.go (6)
ExecutorCapabilities(4-19)RegisterExecutorCapabilities(46-48)SupportsCommand(51-53)SupportsScript(61-63)SupportsWorkerSelector(81-83)SupportsShell(66-68)
internal/core/capabilities.go (1)
internal/runtime/builtin/command/shell.go (1)
Shell(13-20)
internal/runtime/builtin/ssh/ssh.go (3)
internal/common/cmdutil/shell.go (2)
ShellQuote(8-34)ShellQuoteArgs(37-43)internal/core/capabilities.go (1)
ExecutorCapabilities(4-19)internal/runtime/executor/executor.go (1)
RegisterExecutor(40-46)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Build
- GitHub Check: Test on ubuntu-latest
🔇 Additional comments (14)
internal/common/cmdutil/shell.go (2)
7-34: LGTM! Robust POSIX shell escaping implementation.The implementation correctly handles shell quoting for POSIX-compliant shells:
- Uses a conservative safe character set (ASCII alphanumeric, hyphen, underscore, dot, slash)
- Properly escapes single quotes using the
'\''pattern- Returns
''for empty strings to preserve argument boundaries
36-43: LGTM! Clean implementation of multi-argument quoting.Correctly applies
ShellQuoteto each argument and joins with spaces, preserving argument boundaries for shell execution.internal/runtime/builtin/ssh/ssh_test.go (1)
33-69: LGTM! Well-structured test for shell escaping.The table-driven test properly validates shell quoting behavior for SSH commands, covering simple commands, arguments with spaces, and special characters. The test aligns with the coding guidelines by using
testify/assertand following a table-driven pattern.internal/core/capabilities_test.go (1)
9-40: LGTM! Comprehensive test coverage for the capabilities system.The tests validate both the registry behavior (Get returns registered capabilities or empty for unregistered) and the predicate helpers (Supports* functions). The table-driven approach and use of
testify/assertalign with the project's testing guidelines.internal/common/cmdutil/shell_test.go (1)
10-265: LGTM! Exceptionally thorough test coverage.The test suite comprehensively validates shell quoting across:
- Edge cases (empty strings, safe characters, special characters)
- Security scenarios (command injection, shell metacharacters)
- Round-trip validation with actual shell execution when available
- All ASCII characters and Unicode
The table-driven approach and extensive coverage demonstrate excellent testing practices.
internal/runtime/builtin/ssh/ssh.go (2)
83-122: LGTM! Shell escaping properly implemented and multi-command support added.The refactored
Runmethod correctly:
- Creates a new SSH session per command (required since
session.Runcan only be called once)- Uses
cmdutil.ShellQuoteandcmdutil.ShellQuoteArgsto properly escape commands and arguments for shell execution, addressing the previous review concern about shell injection vulnerabilities- Checks context cancellation between commands for responsive termination
- Provides clear error messages with command indices for debugging
125-130: LGTM! Capabilities correctly declared for SSH executor.The SSH executor properly declares support for
Command,MultipleCommands, andShell, accurately reflecting its implementation capabilities.internal/core/capabilities.go (3)
3-19: LGTM! Well-defined capability flags for executor features.The
ExecutorCapabilitiesstruct clearly documents the supported features for each executor type, providing a clean foundation for validation and feature detection across the codebase.
21-43: LGTM! Strict default behavior catches unregistered executors.The registry correctly returns empty capabilities (all false) for unregistered executor types at line 42, addressing the previous review concern about permissive defaults. This ensures validation catches configuration errors early rather than deferring them to runtime.
Note: The registry lacks mutex protection, but this is acceptable if all registrations occur in
init()functions before concurrent access.
45-83: LGTM! Clean API for capability registration and querying.The helper functions provide a clean, type-safe API for both registering executor capabilities and querying individual capability flags. The predicate functions (
Supports*) offer a convenient way to check specific features throughout the codebase.internal/core/spec/step_test.go (4)
17-54: LGTM! Comprehensive test setup for executor capabilities.The TestMain function properly registers executor capabilities for all executor types, mirroring production initialization. This enables testing the new capability-based validation system.
1007-1012: Verify silent filtering of empty commands is intentional.The tests expect empty command strings to be silently filtered out while preserving valid commands. This lenient behavior might hide user errors (e.g., accidental empty entries).
Consider whether it would be clearer to return an error for arrays containing empty commands rather than silently filtering them.
Also applies to: 1161-1166
2580-2674: LGTM! Excellent conflict validation and integration testing.TestValidateConflicts properly validates all field conflicts using ErrorIs assertions, and TestUnregisteredExecutorValidation provides valuable end-to-end validation of the capability system with an actual YAML file.
1014-1021: Primitive type acceptance is intentional design.The implementation explicitly allows
string,int,int64,uint64,float64, andbooltypes in command arrays, with clear error messaging documenting this contract. This is necessary for proper YAML unmarshaling where numeric and boolean values deserialize as primitives. No changes needed.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
internal/runtime/builtin/ssh/ssh.go (1)
85-126: Well-structured multi-command implementation with proper shell escaping.The refactored
Runmethod correctly:
- Creates a new session per command (required by SSH's single-use session constraint)
- Applies shell quoting via
cmdutil.ShellQuoteto prevent argument parsing issues (addresses previous review comment)- Includes command index in error messages for clear diagnostics
- Checks context cancellation between commands
- Properly cleans up sessions with logged warnings
Optional enhancement: Context cancellation is checked between commands (lines 95-99), but
session.Run()blocks without context awareness. While the current approach is acceptable given SSH's blocking nature, you could improve responsiveness by launchingsession.Run()in a goroutine and selecting on both its completion andctx.Done(), callingKill()if cancelled mid-execution.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
internal/persistence/filedag/store_test.gointernal/runtime/builtin/ssh/ssh.go
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*.go: Backend entrypoint incmd/orchestrates the scheduler and CLI; runtime, persistence, and service layers sit underinternal/*(for exampleinternal/runtime,internal/persistence)
Keep Go filesgofmt/goimportsclean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, andErr...names for package-level errors
Repository linting relies ongolangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers ininternal/common
Files:
internal/persistence/filedag/store_test.gointernal/runtime/builtin/ssh/ssh.go
**/*_test.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*_test.go: Co-locate Go tests as*_test.go; favour table-driven cases and cover failure paths
Usestretchr/testify/requireand shared fixtures frominternal/testinstead of duplicating mocks
Files:
internal/persistence/filedag/store_test.go
🧠 Learnings (5)
📓 Common learnings
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*.go : Backend entrypoint in `cmd/` orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under `internal/*` (for example `internal/runtime`, `internal/persistence`)
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: `make test` (or `make test-coverage`) executes the Go suite via `gotestsum`; append `TEST_TARGET=./internal/...` to focus packages
Applied to files:
internal/persistence/filedag/store_test.go
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*_test.go : Co-locate Go tests as `*_test.go`; favour table-driven cases and cover failure paths
Applied to files:
internal/persistence/filedag/store_test.go
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*_test.go : Use `stretchr/testify/require` and shared fixtures from `internal/test` instead of duplicating mocks
Applied to files:
internal/persistence/filedag/store_test.go
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*.go : Backend entrypoint in `cmd/` orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under `internal/*` (for example `internal/runtime`, `internal/persistence`)
Applied to files:
internal/runtime/builtin/ssh/ssh.go
🧬 Code graph analysis (2)
internal/persistence/filedag/store_test.go (1)
internal/core/capabilities.go (2)
RegisterExecutorCapabilities(46-48)ExecutorCapabilities(4-19)
internal/runtime/builtin/ssh/ssh.go (5)
internal/common/logger/context.go (2)
Errorf(75-77)Warn(45-47)internal/common/cmdutil/shell.go (2)
ShellQuote(8-34)ShellQuoteArgs(37-43)internal/common/logger/tag/tag.go (1)
Args(408-410)internal/core/capabilities.go (1)
ExecutorCapabilities(4-19)internal/runtime/executor/executor.go (1)
RegisterExecutor(40-46)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Test on ubuntu-latest
- GitHub Check: Build
🔇 Additional comments (3)
internal/persistence/filedag/store_test.go (2)
12-12: LGTM!The import is necessary for the executor capabilities registration in
TestMainand is correctly ordered.
19-28: LGTM! Clean test setup for executor capabilities.The
TestMaincorrectly initializes executor capabilities for the test environment using the standard Go testing pattern. The capabilities registered (Command, MultipleCommands, Script, Shell) align with what's needed for DAG store tests.Note:
Container,SubDAG, andWorkerSelectorcapabilities are not set (will default to false). This appears intentional since this test file focuses on DAG persistence/store operations rather than executor-specific functionality.internal/runtime/builtin/ssh/ssh.go (1)
9-11: LGTM! Imports align with the new multi-command functionality.The new imports support shell quoting (
cmdutil) and structured logging (logger,tag), both of which are properly utilized in the refactoredRunmethod.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (2)
internal/runtime/builtin/docker/executor.go (2)
188-188: Duplicate: WorkingDir should be set from step.Dir.This issue is already documented in past review comments. The
ExecOptions{}should be initialized withWorkingDir: e.step.Dirto respect step-level working directory configuration, consistent with how environment variables are merged at line 368.
316-316: Duplicate: WorkingDir should be set from step.Dir.Same issue as line 188 - already documented in past review comments. The
ExecOptions{}should be initialized withWorkingDir: e.step.Dir.
🧹 Nitpick comments (4)
internal/runtime/builtin/ssh/ssh.go (1)
118-163: Session reference persists after close, but behavior is safe.The
e.sessionfield is set at line 125 but never cleared after the command completes. IfKill()is called afterrunCommand()finishes (e.g., between commands in the loop), it will callClose()on an already-closed session.This is technically safe since
ssh.Session.Close()is idempotent and the mutex prevents concurrent access, but clearing the session after use would be cleaner:🔎 Optional improvement to clear session after use
if closeErr := session.Close(); closeErr != nil { logger.Warn(ctx, "SSH session close error", tag.Error(closeErr)) } + + e.mu.Lock() + e.session = nil + e.mu.Unlock() if err != nil { return fmt.Errorf("command %d failed: %w", index+1, err)internal/runtime/builtin/docker/executor.go (2)
205-207: Consider extracting command-building logic to reduce duplication.The pattern of building a command slice from
cmdEntry.CommandandcmdEntry.Argsappears three times (lines 205-207, 259-261, 306-308). Consider extracting this into a small helper method to improve maintainability.🔎 Proposed helper function
Add this helper method to the
dockerstruct:// buildCommand constructs a command slice from a CommandEntry. // Returns nil if Command is empty, otherwise returns [Command, Args...]. func (e *docker) buildCommand(cmdEntry core.CommandEntry) []string { if cmdEntry.Command == "" { return nil } return append([]string{cmdEntry.Command}, cmdEntry.Args...) }Then replace the three occurrences:
-var cmd []string -if cmdEntry.Command != "" { - cmd = append([]string{cmdEntry.Command}, cmdEntry.Args...) -} +cmd := e.buildCommand(cmdEntry)Also applies to: 259-261, 306-308
228-232: Consider checking context cancellation before each command for consistency.In
runInExistingContainer, the context cancellation check occurs after command execution (lines 228-232), whereas inrunInNewContainer(lines 298-303), the check occurs before execution. Checking before execution is preferable because it avoids starting a new command when cancellation has been requested.🔎 Suggested adjustment
Move the context check to before command execution:
// Execute each command sequentially for i, cmdEntry := range e.step.Commands { + // Check context cancellation between commands + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + var cmd []string if cmdEntry.Command != "" { cmd = append([]string{cmdEntry.Command}, cmdEntry.Args...) } logger.Debug(ctx, "Docker executor: executing command in existing container", slog.Int("commandIndex", i+1), slog.Int("totalCommands", len(e.step.Commands)), slog.Any("cmd", cmd), ) exitCode, err := cli.Exec(ctx, cmd, e.stdout, e.stderr, execOpts) e.mu.Lock() e.exitCode = exitCode e.mu.Unlock() if err != nil { if tail := tw.Tail(); tail != "" { return fmt.Errorf("command %d failed: %w\nrecent stderr (tail):\n%s", i+1, err, tail) } return fmt.Errorf("command %d failed: %w", i+1, err) } - - // Check context cancellation between commands - select { - case <-ctx.Done(): - return ctx.Err() - default: - } }internal/core/spec/step_test.go (1)
2644-2653: Consider addingt.Parallel()to subtests for consistency.Other validation test functions (e.g.,
TestValidateMultipleCommands,TestValidateScript) callt.Parallel()inside the subtestt.Run. For consistency and to maximize parallel test execution:Proposed fix
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() err := validateConflicts(&tt.step) if tt.wantErr != nil { assert.ErrorIs(t, err, tt.wantErr)
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
internal/core/spec/step_test.gointernal/runtime/builtin/docker/executor.gointernal/runtime/builtin/ssh/ssh.go
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*.go: Backend entrypoint incmd/orchestrates the scheduler and CLI; runtime, persistence, and service layers sit underinternal/*(for exampleinternal/runtime,internal/persistence)
Keep Go filesgofmt/goimportsclean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, andErr...names for package-level errors
Repository linting relies ongolangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers ininternal/common
Files:
internal/runtime/builtin/docker/executor.gointernal/runtime/builtin/ssh/ssh.gointernal/core/spec/step_test.go
**/*_test.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*_test.go: Co-locate Go tests as*_test.go; favour table-driven cases and cover failure paths
Usestretchr/testify/requireand shared fixtures frominternal/testinstead of duplicating mocks
Files:
internal/core/spec/step_test.go
🧠 Learnings (5)
📓 Common learnings
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*.go : Backend entrypoint in `cmd/` orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under `internal/*` (for example `internal/runtime`, `internal/persistence`)
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*.go : Backend entrypoint in `cmd/` orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under `internal/*` (for example `internal/runtime`, `internal/persistence`)
Applied to files:
internal/runtime/builtin/ssh/ssh.go
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: `make test` (or `make test-coverage`) executes the Go suite via `gotestsum`; append `TEST_TARGET=./internal/...` to focus packages
Applied to files:
internal/core/spec/step_test.go
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*_test.go : Co-locate Go tests as `*_test.go`; favour table-driven cases and cover failure paths
Applied to files:
internal/core/spec/step_test.go
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*_test.go : Use `stretchr/testify/require` and shared fixtures from `internal/test` instead of duplicating mocks
Applied to files:
internal/core/spec/step_test.go
🧬 Code graph analysis (2)
internal/runtime/builtin/docker/executor.go (4)
internal/runtime/builtin/docker/client.go (1)
ExecOptions(89-92)internal/common/logger/tag/tag.go (3)
Command(403-405)Args(408-410)Container(310-312)internal/core/capabilities.go (1)
ExecutorCapabilities(4-19)internal/runtime/executor/executor.go (1)
RegisterExecutor(40-46)
internal/runtime/builtin/ssh/ssh.go (5)
internal/common/logger/context.go (2)
Errorf(75-77)Warn(45-47)internal/common/cmdutil/shell.go (2)
ShellQuote(8-34)ShellQuoteArgs(37-43)internal/common/logger/tag/tag.go (1)
Args(408-410)internal/core/capabilities.go (1)
ExecutorCapabilities(4-19)internal/runtime/executor/executor.go (1)
RegisterExecutor(40-46)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Test on ubuntu-latest
- GitHub Check: Build
🔇 Additional comments (14)
internal/runtime/builtin/ssh/ssh.go (5)
36-43: Good addition of mutex for thread safety.The mutex properly guards the
sessionfield against concurrent access betweenKill()andrunCommand(), preventing race conditions when cancellation occurs during command execution.
80-89: LGTM!The lock-snapshot-unlock pattern correctly avoids holding the mutex during the potentially blocking
session.Close()call while still ensuring thread-safe access to the session field.
91-113: Clean implementation of sequential multi-command execution.The method properly handles:
- Early return for empty command list
- Context cancellation checks between commands for responsiveness
- Sequential execution with error propagation
This aligns well with SSH session semantics where
session.Run()can only be called once per session.
131-134: Shell escaping properly implemented.Good use of
cmdutil.ShellQuoteandShellQuoteArgsto safely escape the command and arguments before passing to the remote shell. This correctly addresses the previous review concern about special character handling.
165-171: Capabilities correctly declared.The executor properly declares
Command: trueandMultipleCommands: truewhile omittingShell(defaults to false), which accurately reflects that this executor does not use step-level shell configuration fields. This addresses the previous review concern about incorrect capability registration.internal/runtime/builtin/docker/executor.go (2)
486-492: LGTM: Capability registration is accurate.The executor correctly declares support for
Command,MultipleCommands, andContainercapabilities, while appropriately excludingScriptandShellsince the docker executor uses the command/args model rather than script-based execution.
134-184: LGTM: Run method properly delegates to execution-path helpers.The refactored
Runmethod correctly routes execution torunInExistingContainerwhen a shared container client exists (line 156-158) andrunInNewContainerotherwise. The conditioncli != nil && e.cfg == nilappropriately ensures that step-level container configuration takes precedence over DAG-level shared containers.internal/core/spec/step_test.go (7)
17-54: Well-structured test setup using TestMain.The executor capabilities registration mirrors production behavior (via init functions) and ensures tests have proper context. Grouping by capability with clear comments improves maintainability.
878-936: Tests properly updated to validate new Commands-based model.Good approach verifying both the new
Commandsslice population and explicitly asserting that legacy fields (Command,Args,CmdWithArgs) remain unpopulated.
1014-1021: Verify non-string primitive conversion is intentional behavior.The test case validates that
123,true,45.6are converted to command strings. While this adds flexibility, silently accepting non-string values might mask YAML configuration errors where users accidentally provide wrong types.
1053-1208: Comprehensive unit tests for command parsing functions.Good coverage of:
- Single command parsing with script fallback for multiline input
- Multiple commands with proper error handling for invalid types (maps, nested arrays)
- Empty and whitespace-only input rejection
1871-2488: Thorough validation tests for executor capabilities.Excellent coverage pattern:
- Each validation function tested with executors that support and don't support the feature
- Empty/nil values correctly bypass validation
- Error messages verified to include executor type for debugging
The tests properly validate the new capability registry system. As per coding guidelines, uses table-driven tests with
testify/require.
2490-2578: WorkerSelector validation tests follow established pattern.Consistent with other validation tests - covers supported executors (dag, subworkflow, parallel) and properly rejects unsupported ones while allowing nil/empty values.
2656-2674: Integration test validates end-to-end executor validation.Good use of
t.TempDir()for automatic cleanup. This test ensures the capability validation is properly wired into the loading pipeline, catching unregistered executor types with appropriate error messages.
|
All looks good. One point, the granularity of stepwise error tracking may not be available. And the short hand syntax for steps actually had similar effect. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1506 +/- ##
==========================================
- Coverage 61.48% 61.40% -0.09%
==========================================
Files 203 206 +3
Lines 22339 22701 +362
==========================================
+ Hits 13735 13939 +204
- Misses 7219 7371 +152
- Partials 1385 1391 +6
... and 4 files with indirect coverage changes Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
Summary by CodeRabbit
New Features
Validation
API
Integration & UX
✏️ Tip: You can customize this high-level summary in your review settings.