Skip to content

feat(all): multiple commands syntax#1506

Merged
yottahmd merged 32 commits intomainfrom
support-multiple-command
Dec 26, 2025
Merged

feat(all): multiple commands syntax#1506
yottahmd merged 32 commits intomainfrom
support-multiple-command

Conversation

@yottahmd
Copy link
Copy Markdown
Collaborator

@yottahmd yottahmd commented Dec 25, 2025

Summary by CodeRabbit

  • New Features

    • Steps can contain multiple commands executed sequentially; UIs and runners display and run command lists.
  • Validation

    • Executors advertise capabilities and produce clearer errors when features conflict or are unsupported (multi-command vs sub-workflow/script/container).
  • API

    • Step payloads now use a commands list for command entries; legacy single-command fields remain supported for compatibility.
  • Integration & UX

    • Schemas, APIs, reporting, and UI components updated to present and handle the commands array.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 25, 2025

📝 Walkthrough

Walkthrough

Step 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

Cohort / File(s) Summary
Core model & capabilities
internal/core/step.go, internal/core/step_test.go, internal/core/capabilities.go, api/v2/api.gen.go, api/v2/api.yaml
Added CommandEntry and Commands []CommandEntry on Step; Unmarshal migration from legacy fields; HasMultipleCommands(); new ExecutorCapabilities registry; OpenAPI/schema updates.
Spec parsing & validation
internal/core/spec/step.go, internal/core/spec/builder.go, internal/core/spec/errors.go, internal/core/spec/*_test.go
Builder split into single vs multiple command parsing (buildSingleCommand/buildMultipleCommands), added conflict validators (SubDAG vs command/script/container/executor) and new error vars; command-step detection now uses len(step.Commands) > 0.
Executors & multi-command runtime
internal/runtime/builtin/command/*, internal/runtime/builtin/command/multi_command.go, internal/runtime/builtin/docker/*, internal/runtime/builtin/ssh/ssh.go, internal/runtime/builtin/*
Executors consume Commands[]; multi-command runner added; Docker supports background start + per-command Exec; SSH executes per-command sessions with cancellation; executors register capabilities including MultipleCommands.
Other builtins updated
internal/runtime/builtin/http/http.go, internal/runtime/builtin/jq/jq.go, internal/runtime/builtin/gha/*, internal/runtime/builtin/archive/*, internal/runtime/builtin/mail/mail.go
Extract command/args from Commands[0] or iterate Commands; executor registration calls updated to include capabilities.
Runtime node/data/reporting
internal/runtime/node.go, internal/runtime/data.go, internal/runtime/agent/reporter.go, internal/runtime/agent/*
Evaluation and Args()/SetArgs() operate on Commands[0]; node evaluation now iterates/evaluates per-entry Commands; reporter/HTML/table rendering use formatted join of Commands.
API, frontend & CLI
internal/service/frontend/api/v1/dags.go, internal/service/frontend/api/v2/transformer.go, internal/cmd/exec_spec.go, ui/src/*, schemas/dag.schema.json
API and transformer map new Commands collection; CLI exec spec builds escaped command string; UI components updated to accept commands[]; JSON/YAML schema and OpenAPI updated for multi-command support.
Tests & integration
internal/integration/*, many internal/*_test.go
Wide test updates to construct Steps with Commands slices; new integration tests for multi-command behavior; some tests removed/adjusted and test parallelism tweaked.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Runner
participant Executor
participant Backend as "Container / SSH / Local"
note over Runner,Executor: Step carries Commands[] (one or more)
Runner->>Executor: Start step with Step.Commands[]
alt multiple commands
loop for each Commands[i]
Executor->>Backend: Exec/Run Commands[i] (Command + Args)
Backend-->>Executor: stdout/stderr + exit code
alt command failed
Executor-->>Runner: fail with exit code and stderr tail
break
end
Note right of Executor: check ctx cancellation between commands
end
else single command
Executor->>Backend: Run Commands[0]
Backend-->>Executor: result
end
Executor-->>Runner: final status (success/failure)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through code with nimble paws,

One command became an ordered list because,
Executors march and run in line,
Tests, UI, API learned the new design.
A crunchy carrot for each passing case 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.28% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(all): multiple commands syntax' accurately and concisely summarizes the primary change: adding support for multiple commands per step across the entire codebase.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch support-multiple-command

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 the singleCommandOnlyExecutors restriction map.

The "gha" executor type is registered in internal/runtime/builtin/gha/executor.go:550 alongside "github_action" and "github-action", but it is missing from the singleCommandOnlyExecutors map. 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 using executor: gha with 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) and newGitHubAction (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) == 1 to 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].CmdWithArgs with a guard for empty Commands, but there's no validation that the query is non-empty. An empty query will cause gojq.Parse(e.query) to fail at runtime in the Run() 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 method and url from Commands[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 UnmarshalJSON implementation successfully migrates legacy Command/Args fields to Commands, but leaves the legacy fields populated. Since all fields have json:"...,omitempty" tags and no custom MarshalJSON filters 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.

SetStdout and SetStderr iterate over e.configs while holding the lock, but Run creates commandExecutor instances from these configs without holding the lock. If SetStdout/SetStderr are called concurrently with Run, there's a potential data race on cfg.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 commands
  • Run(cmd) for single command
  • StartBackground() + Exec(cmd) for multiple commands

While functionally correct, the choice of Run for single commands vs StartBackground+Exec for multiple could affect container lifecycle differently. Consider adding a comment explaining why single commands use Run instead of the same StartBackground+Exec approach.

📝 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 jq might 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

📥 Commits

Reviewing files that changed from the base of the PR and between 64f1884 and ea33af4.

📒 Files selected for processing (33)
  • internal/core/spec/builder.go
  • internal/core/spec/builder_test.go
  • internal/core/spec/dag_test.go
  • internal/core/spec/errors.go
  • internal/core/spec/loader_test.go
  • internal/core/spec/step.go
  • internal/core/spec/step_test.go
  • internal/core/step.go
  • internal/integration/multi_command_test.go
  • internal/integration/shell_test.go
  • internal/runtime/agent/agent_test.go
  • internal/runtime/agent/reporter.go
  • internal/runtime/agent/reporter_test.go
  • internal/runtime/builtin/archive/archive.go
  • internal/runtime/builtin/archive/archive_test.go
  • internal/runtime/builtin/command/command.go
  • internal/runtime/builtin/command/command_script.go
  • internal/runtime/builtin/command/command_test.go
  • internal/runtime/builtin/command/multi_command.go
  • internal/runtime/builtin/docker/client.go
  • internal/runtime/builtin/docker/executor.go
  • internal/runtime/builtin/gha/executor.go
  • internal/runtime/builtin/http/http.go
  • internal/runtime/builtin/http/http_test.go
  • internal/runtime/builtin/jq/jq.go
  • internal/runtime/builtin/jq/jq_test.go
  • internal/runtime/builtin/ssh/ssh.go
  • internal/runtime/data.go
  • internal/runtime/node.go
  • internal/runtime/node_test.go
  • internal/runtime/runner_helper_test.go
  • internal/service/frontend/api/v1/dags.go
  • internal/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 in cmd/ orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under internal/* (for example internal/runtime, internal/persistence)
Keep Go files gofmt/goimports clean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, and Err... names for package-level errors
Repository linting relies on golangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers in internal/common

Files:

  • internal/runtime/builtin/http/http.go
  • internal/runtime/builtin/command/command_script.go
  • internal/service/frontend/api/v1/dags.go
  • internal/core/spec/errors.go
  • internal/runtime/builtin/jq/jq.go
  • internal/runtime/builtin/ssh/ssh.go
  • internal/runtime/agent/reporter.go
  • internal/runtime/builtin/archive/archive.go
  • internal/runtime/builtin/archive/archive_test.go
  • internal/runtime/builtin/command/multi_command.go
  • internal/runtime/builtin/docker/executor.go
  • internal/runtime/builtin/jq/jq_test.go
  • internal/runtime/node.go
  • internal/core/spec/loader_test.go
  • internal/runtime/agent/agent_test.go
  • internal/core/spec/step_test.go
  • internal/runtime/builtin/gha/executor.go
  • internal/core/spec/dag_test.go
  • internal/core/spec/step.go
  • internal/core/spec/builder.go
  • internal/runtime/builtin/command/command.go
  • internal/runtime/data.go
  • internal/runtime/node_test.go
  • internal/runtime/builtin/docker/client.go
  • internal/integration/multi_command_test.go
  • internal/runtime/runner_helper_test.go
  • internal/runtime/builtin/http/http_test.go
  • internal/runtime/agent/reporter_test.go
  • internal/service/frontend/api/v2/transformer.go
  • internal/core/step.go
  • internal/runtime/builtin/command/command_test.go
  • internal/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
Use stretchr/testify/require and shared fixtures from internal/test instead of duplicating mocks

Files:

  • internal/runtime/builtin/archive/archive_test.go
  • internal/runtime/builtin/jq/jq_test.go
  • internal/core/spec/loader_test.go
  • internal/runtime/agent/agent_test.go
  • internal/core/spec/step_test.go
  • internal/core/spec/dag_test.go
  • internal/runtime/node_test.go
  • internal/integration/multi_command_test.go
  • internal/runtime/runner_helper_test.go
  • internal/runtime/builtin/http/http_test.go
  • internal/runtime/agent/reporter_test.go
  • internal/runtime/builtin/command/command_test.go
  • internal/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.go
  • 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: `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
  • internal/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.go
  • internal/core/spec/dag_test.go
  • internal/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.go
  • internal/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.TrimSpace ensures clean input for action resolution.

internal/runtime/builtin/docker/client.go (2)

471-495: LGTM! Well-implemented background startup method.

The StartBackground method correctly delegates to CreateContainerKeepAlive, 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 startNewContainer more self-contained by allowing it to use StartCmd when no explicit cmd parameter is provided in "command" startup mode. The validation check len(c.cfg.StartCmd) > 0 ensures the fallback only applies when StartCmd is configured, maintaining safe behavior.

While current callers already provide the cmd parameter, 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 != "" to len(step.Commands) > 0 properly 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 ErrExecutorDoesNotSupportMultipleCmd provides 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 toStep function properly extracts command information from Commands[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 method SetArgs() or prevent invalid CommandEntry state.

Data.SetArgs() creates a CommandEntry with empty Command field, which contradicts all other code patterns where Command is 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 empty Command is 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) > 0 before accessing Commands[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, and CmdWithArgs API fields. If multi-command visibility is needed in the API response in the future, consider adding a Commands field to api.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 Commands slice 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].CmdWithArgs with appropriate HTML escaping.


586-593: LGTM! Consistent with the other HTML renderer.

Same correct pattern as renderHTML, using CmdWithArgs for 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 hasCommands variable 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. The cfg.Ctx stored in the config does not control execution—commandExecutor.Run(ctx) receives and uses the context parameter passed from multiCommandExecutor.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 Commands slice structure, maintaining both Command and CmdWithArgs for 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 Commands slice structure. Using cmdutil.SplitCommand ensures consistent parsing, and preserving CmdWithArgs maintains display fidelity.

internal/runtime/builtin/command/command.go (2)

293-314: LGTM!

The config derivation correctly handles both populated and empty Commands slices. When Commands is empty, command and args remain 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 newMultiCommandExecutor when len(step.Commands) > 1. The implementation iterates through all commands sequentially, stops on first failure, and handles context cancellation between commands. Edge cases are safe: empty Commands slice and single-command path both route to NewCommandConfig, which safely checks len(step.Commands) > 0 before accessing Commands[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:

  1. Deep-clones the Commands slice to prevent race conditions
  2. Evaluates each command entry's Args independently
  3. Sets ShellCmdArgs only for the first command when using shell executor

One observation: The CmdWithArgs field 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, Command with/without Args) are properly converted to the new Commands structure. 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 Commands structure. The pattern of checking Len(Commands, 1) before accessing Commands[0].Command is 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 Commands slice and Script fields. 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!

buildSingleCommand tests properly validate both command parsing and script fallback behavior for multiline commands.


1092-1147: LGTM!

buildMultipleCommands tests correctly validate array processing and empty-value filtering.


1149-1195: LGTM!

HasMultipleCommands tests 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 cmdutil to support command splitting in test helpers.


1042-1057: LGTM!

Test helpers correctly updated to populate Commands slice. The withNodeCmdArgs helper properly uses cmdutil.SplitCommand for consistent parsing, while withNodeCommand creates 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 Run once 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: The e.session field is overwritten on each iteration (line 104), so calling Kill() during multi-command execution will only close the current session. However, since SSH commands run synchronously and Kill() 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 Commands slice with CmdWithArgs for the JQ query. The Script field 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.CommandEntry structure 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 accessing Commands[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 via t.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 StartBackground delegates to CreateContainerKeepAlive, 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 test represents a realistic use case.


610-628: SubDAG migration correctly uses SubDAG.Name and SubDAG.Params.

The tests properly validate that both call and legacy run fields populate SubDAG.Name and SubDAG.Params instead 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 buildStepCommand properly 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:

  1. Trims whitespace
  2. Detects multi-line as script (line 612)
  3. Splits command using cmdutil.SplitCommand (line 618)
  4. 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.

Comment thread internal/runtime/agent/reporter.go Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 to map[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.Join properly 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 the go-shellquote library.

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

📥 Commits

Reviewing files that changed from the base of the PR and between ea33af4 and 7746156.

📒 Files selected for processing (2)
  • internal/cmd/exec_spec.go
  • internal/core/spec/step.go
🧰 Additional context used
📓 Path-based instructions (1)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Backend entrypoint in cmd/ orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under internal/* (for example internal/runtime, internal/persistence)
Keep Go files gofmt/goimports clean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, and Err... names for package-level errors
Repository linting relies on golangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers in internal/common

Files:

  • internal/cmd/exec_spec.go
  • internal/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 buildStepCommand and buildStepExecutor, ensuring that both result.Commands and result.ExecutorConfig.Type are 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 CmdWithArgs field 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 []string to string for the Command field aligns with the new approach of storing a single shell-quoted command string. The YAML tag is appropriate.


72-72: LGTM!

The use of commandStr is consistent with the type change and properly utilizes the shell-quoted command string.

Comment thread internal/core/spec/step.go Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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.Fatalf instead of testify/require, which is inconsistent with the project's coding guidelines.

As per coding guidelines: "Use stretchr/testify/require and shared fixtures from internal/test instead 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

📥 Commits

Reviewing files that changed from the base of the PR and between 7746156 and 518f81a.

📒 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 in cmd/ orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under internal/* (for example internal/runtime, internal/persistence)
Keep Go files gofmt/goimports clean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, and Err... names for package-level errors
Repository linting relies on golangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers in internal/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
Use stretchr/testify/require and shared fixtures from internal/test instead 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 os to os/exec is appropriate for the new git initialization logic.


17-29: The node:25-bookworm Docker image exists on Docker Hub and is available for use.

Likely an incorrect or invalid review comment.

Comment thread internal/integration/gha_test.go
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 making CmdWithArgs consistent with Args for realism

This fixture is valid, but Args: []{"hello","arg1","arg2"} with CmdWithArgs: "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 from SplitCommand.

The error return from cmdutil.SplitCommand is 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: Align CommandEntry API shape with internal/core or clarify divergence

api/v2.CommandEntry only exposes command and args, while internal/core/step.go’s CommandEntry also has CmdWithArgs for 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 + args and that cmdWithArgs is intentionally internal-only.

internal/runtime/builtin/command/command_test.go (1)

2295-2408: Quote outputFile in shell snippets to tolerate spaces in temp paths

The multi-command tests build shell commands like echo first >> + outputFile without 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 ... >> outputFile commands.

internal/runtime/runner_helper_test.go (1)

80-92: Handler construction and option wiring look correct; consider de-duplicating command setup

The switch to step.Commands = []core.CommandEntry{...} in withCommand and newHandlerStep matches the new core.Step shape, and withOnSuccess / withOnFailure / withOnExit / withOnCancel now correctly wire each handler to the corresponding runtime.Config field.

You do duplicate the SplitCommand + CommandEntry construction between withCommand and newHandlerStep, which could drift over time. If you want to DRY this up later, you could factor a tiny helper that returns a core.CommandEntry from 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 responses

The new usage of CommandDisplay on desktop and the explicit node.step.commands.map(...) rendering on mobile correctly handle 0/1/many commands and look aligned with the new commands API.

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 commands in both places. That would keep command display working when commands isn’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 tiny maxLength

truncateText assumes maxLength is reasonably larger than 3; when maxLength is very small (e.g. 0–4), maxLength - 3 or maxLength - 5 can go negative, and you end up relying on JS substring’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

📥 Commits

Reviewing files that changed from the base of the PR and between 518f81a and 144a7bd.

📒 Files selected for processing (20)
  • api/v2/api.gen.go
  • api/v2/api.yaml
  • internal/core/step.go
  • internal/integration/container_test.go
  • internal/runtime/builtin/command/command.go
  • internal/runtime/builtin/command/command_test.go
  • internal/runtime/env_test.go
  • internal/runtime/executor/dag_runner_test.go
  • internal/runtime/node.go
  • internal/runtime/node_test.go
  • internal/runtime/output_test.go
  • internal/runtime/plan_test.go
  • internal/runtime/runner_helper_test.go
  • internal/runtime/runner_test.go
  • internal/runtime/transform/status_test.go
  • internal/service/frontend/api/v2/transformer.go
  • ui/src/api/v2/schema.ts
  • ui/src/components/ui/command-display.tsx
  • ui/src/features/dags/components/dag-details/DAGStepTableRow.tsx
  • ui/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 in cmd/ orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under internal/* (for example internal/runtime, internal/persistence)
Keep Go files gofmt/goimports clean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, and Err... names for package-level errors
Repository linting relies on golangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers in internal/common

Files:

  • internal/integration/container_test.go
  • internal/runtime/node.go
  • internal/runtime/executor/dag_runner_test.go
  • internal/runtime/transform/status_test.go
  • internal/runtime/node_test.go
  • internal/runtime/env_test.go
  • internal/core/step.go
  • internal/runtime/plan_test.go
  • internal/runtime/builtin/command/command.go
  • api/v2/api.gen.go
  • internal/runtime/runner_test.go
  • internal/runtime/builtin/command/command_test.go
  • internal/runtime/output_test.go
  • internal/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
Use stretchr/testify/require and shared fixtures from internal/test instead of duplicating mocks

Files:

  • internal/integration/container_test.go
  • internal/runtime/executor/dag_runner_test.go
  • internal/runtime/transform/status_test.go
  • internal/runtime/node_test.go
  • internal/runtime/env_test.go
  • internal/runtime/plan_test.go
  • internal/runtime/runner_test.go
  • internal/runtime/builtin/command/command_test.go
  • internal/runtime/output_test.go
  • internal/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 like dark: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 with whitespace-normal break-words
Use consistent metadata styling with bg-slate-200 dark:bg-slate-700 backgrounds and maintain text hierarchy with primary/secondary/muted text colors
Use flexbox-first layouts with min-h-0 and overflow-hidden to 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.ts
  • ui/src/components/ui/command-display.tsx
  • ui/src/features/dags/components/dag-details/NodeStatusTableRow.tsx
  • ui/src/features/dags/components/dag-details/DAGStepTableRow.tsx
ui/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

ui/**/*.{ts,tsx}: The React + TypeScript frontend resides in ui/, with production bundles copied to internal/service/frontend/assets by make ui
UI code follows ESLint + Prettier (2-space indent) and Tailwind utilities; name React components in PascalCase (JobList.tsx) and hooks with use* (useJobs.ts)

Files:

  • ui/src/api/v2/schema.ts
  • ui/src/components/ui/command-display.tsx
  • ui/src/features/dags/components/dag-details/NodeStatusTableRow.tsx
  • ui/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.go
  • internal/runtime/node_test.go
  • internal/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.go
  • internal/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 to Commands looks correct

Using 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 with Step.Commands

All 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 API

Conditioning on step.commands?.length > 0 and passing commands={step.commands} matches the updated schema and the new CommandDisplay API; no additional handling needed here.

internal/runtime/plan_test.go (1)

59-62: Plan dependency tests now exercise Commands without altering semantics

Adding trivial Commands entries 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 semantics

The new evaluateCommandArgs correctly:

  • Guards with cmdEvaluated.
  • Applies cmdutil.WithoutExpandEnv() only for command executors with a shell.
  • Evaluates each arg and (when set) CmdWithArgs per entry, then writes back a fresh []core.CommandEntry via SetStep.

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 models commands and CommandEntry

Exposing Step.commands?: CommandEntry[] plus the new CommandEntry { 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 to Commands

All affected tests now construct steps via Commands with sh -c <python script> while preserving Output and 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 cmdutil import is correctly added and used by the withNodeCmdArgs helper function on line 1050.


1059-1066: LGTM!

The helper correctly sets both Command and CmdWithArgs to the same value. For simple commands like "true" or "sleep 3", the executor will use CmdWithArgs via the shell when Args is empty, which is the expected behavior.


676-681: LGTM!

The test data correctly uses the new Commands []core.CommandEntry structure with properly populated Command and Args fields.


736-741: LGTM!

Consistent with the new multi-command data model.


884-890: LGTM!

The Output field is correctly placed at the step level (not inside CommandEntry), which aligns with the core.Step structure 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 legacy Command/Args fields to the Commands slice during JSON unmarshalling. When old-format configurations are deserialized, the migration happens before the code at lines 296-319 executes, so extracting from step.Commands[0] and falling back to step.ShellCmdArgs is correct and sufficient. No additional fallback to deprecated fields is needed in NewCommandConfig.


273-286: LGTM. The branching logic correctly routes to multiCommandExecutor when multiple commands exist. The newMultiCommandExecutor implementation properly iterates over step.Commands and constructs configs for each command. The single-command path handles both len(step.Commands) == 0 and len(step.Commands) == 1 cases via NewCommandConfig, with proper fallback to step.ShellCmdArgs. Code follows Go conventions with proper tab indentation, error wrapping, and naming patterns.

api/v2/api.gen.go (2)

869-871: Step.Commands shape looks consistent with multi‑command support

Using Commands *[]CommandEntry with omitempty matches 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 driven

The updated swaggerSpec base64/gzip blob is expected after changing the OpenAPI for Step.commands/CommandEntry. As long as this file is entirely generated from the updated spec (e.g., via your normal oapi-codegen/make generate flow) and not hand‑edited, this block looks fine.

internal/runtime/builtin/command/command_test.go (1)

450-476: Single-command tests correctly migrated to Commands API

All updated tests now use core.Step{ Name: "…", Commands: []core.CommandEntry{{Command: "...", Args: ...}} }, which aligns with NewCommand’s len(step.Commands) > 1 switch 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 use Commands

The DAG in TestRunner_StepRetryExecution now uses Commands: []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 from dag.Steps[i] lines up with this; no issues spotted.


2408-2606: Handler ID-access tests correctly exercise newHandlerStep and new handler wiring

The event-handler tests now:

  • Build handlers via newHandlerStep(t, name, id, command) so each handler has a proper Commands entry and optional ID.
  • Use withOnSuccess, withOnFailure, and withOnExit that now correctly assign to runtime.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.

Comment thread internal/core/step.go
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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:

  1. Fixed and enabled
  2. Conditionally skipped with a clear reason (e.g., check for required dependencies)
  3. 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.T parameter is not used in this function (it's marked with _). Consider removing it or using t.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 singleCommandOnlyExecutors map 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

📥 Commits

Reviewing files that changed from the base of the PR and between ed9a723 and d75c716.

📒 Files selected for processing (12)
  • internal/cmd/exec_spec.go
  • internal/core/spec/step.go
  • internal/core/spec/step_test.go
  • internal/core/step.go
  • internal/core/step_test.go
  • internal/integration/container_test.go
  • internal/integration/gha_test.go
  • internal/runtime/agent/reporter.go
  • internal/runtime/builtin/command/command_test.go
  • internal/runtime/env_test.go
  • internal/runtime/node_test.go
  • internal/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 in cmd/ orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under internal/* (for example internal/runtime, internal/persistence)
Keep Go files gofmt/goimports clean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, and Err... names for package-level errors
Repository linting relies on golangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers in internal/common

Files:

  • internal/integration/container_test.go
  • internal/core/spec/step.go
  • internal/runtime/env_test.go
  • internal/runtime/builtin/command/command_test.go
  • internal/core/step.go
  • internal/cmd/exec_spec.go
  • internal/core/spec/step_test.go
  • internal/runtime/node_test.go
  • internal/integration/gha_test.go
  • internal/runtime/runner_helper_test.go
  • internal/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
Use stretchr/testify/require and shared fixtures from internal/test instead of duplicating mocks

Files:

  • internal/integration/container_test.go
  • internal/runtime/env_test.go
  • internal/runtime/builtin/command/command_test.go
  • internal/core/spec/step_test.go
  • internal/runtime/node_test.go
  • internal/integration/gha_test.go
  • internal/runtime/runner_helper_test.go
  • internal/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.go
  • internal/runtime/node_test.go
  • internal/integration/gha_test.go
  • internal/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.go
  • internal/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.go
  • internal/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 CmdWithArgs takes 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 BuildCommandEscapedString properly handles argument escaping and quoting, which is necessary when converting from []string args to a single command string for YAML serialization.


39-43: Aligned with the new command model.

The execStep struct now uses a single Command string field which will be parsed by the spec loader into the Commands []CommandEntry structure during DAG loading.

internal/core/step.go (3)

118-141: Well-designed CommandEntry type with proper String() method.

The CommandEntry struct cleanly encapsulates command data, and the String() method correctly prioritizes CmdWithArgs for display purposes, falling back to building from Command and Args.


148-178: Migration logic now properly guards against dummy entries.

The UnmarshalJSON implementation correctly applies the fix from the past review: it only populates Commands when at least one legacy field (Command, Args, or CmdWithArgs) is actually set. This prevents creating dummy CommandEntry entries 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 by NewCommand to 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 uses multiCommandExecutor
  • Exit code propagation: Ensures the failing command's exit code is properly captured

The tests follow table-driven patterns and use require/assert from testify as per coding guidelines.

internal/runtime/runner_helper_test.go (2)

80-91: Clean helper for parsing command strings into CommandEntry.

The parseCommand helper correctly uses cmdutil.SplitCommand to parse command strings and constructs a CommandEntry with all fields populated. Using panic is 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 withCommand step option now correctly builds a Commands slice 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 withNodeCmdArgs helper correctly uses cmdutil.SplitCommand to parse command strings and populates the new Commands slice with a CommandEntry. The use of t.Fatalf for parse errors is appropriate for test setup code.


1063-1070: LGTM!

The updated withNodeCommand helper correctly populates the Commands slice 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 withNodeCmdArgs for command strings that contain quotes, escapes, and special characters, ensuring proper parsing through cmdutil.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 Commands slice
  • 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 pattern is parsed with |, grep, and pattern as separate args. This is expected behavior from cmdutil.SplitCommand but 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 on core.Step.

internal/core/spec/step.go (5)

208-210: LGTM!

The validation is correctly placed after buildStepCommand to 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.SplitCommand for proper shell-like parsing
  • Populates only the new Commands field, 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, bool but not uint or int32. YAML parsers typically unmarshal to int or int64, 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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between d75c716 and b0d8588.

📒 Files selected for processing (15)
  • internal/core/capabilities.go
  • internal/core/spec/step.go
  • internal/core/spec/step_test.go
  • internal/runtime/builtin/archive/archive.go
  • internal/runtime/builtin/command/command.go
  • internal/runtime/builtin/dag/dag.go
  • internal/runtime/builtin/dag/parallel.go
  • internal/runtime/builtin/docker/executor.go
  • internal/runtime/builtin/gha/executor.go
  • internal/runtime/builtin/http/http.go
  • internal/runtime/builtin/jq/jq.go
  • internal/runtime/builtin/mail/mail.go
  • internal/runtime/builtin/ssh/ssh.go
  • internal/runtime/builtin/ssh/ssh_test.go
  • internal/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 in cmd/ orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under internal/* (for example internal/runtime, internal/persistence)
Keep Go files gofmt/goimports clean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, and Err... names for package-level errors
Repository linting relies on golangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers in internal/common

Files:

  • internal/runtime/builtin/dag/dag.go
  • internal/runtime/builtin/command/command.go
  • internal/runtime/builtin/dag/parallel.go
  • internal/runtime/executor/executor.go
  • internal/runtime/builtin/gha/executor.go
  • internal/core/spec/step_test.go
  • internal/core/capabilities.go
  • internal/runtime/builtin/docker/executor.go
  • internal/core/spec/step.go
  • internal/runtime/builtin/ssh/ssh.go
  • internal/runtime/builtin/mail/mail.go
  • internal/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
Use stretchr/testify/require and shared fixtures from internal/test instead 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: true capability, 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 RegisterExecutor function properly integrates the capabilities system. The unconditional call to core.RegisterExecutorCapabilities is correct since the zero-value of ExecutorCapabilities (all fields false) 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.Command field to the new step.Commands slice. 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: true only (not MultipleCommands), 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 TestMain to register executor capabilities before tests run. This ensures tests are isolated from runtime init() 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:

  1. Single-line commands populate Commands slice (not legacy fields)
  2. Multi-line commands become Script instead
  3. Legacy fields (Command, Args, CmdWithArgs) remain empty

This 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 pattern to 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 the script field or ensure the shell executor is invoked.

This test documents the current behavior, which is acceptable.


1917-2102: LGTM!

Thorough validation tests for validateMultipleCommands covering:

  • 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. Using CmdWithArgs instead of Command is the right choice here.


147-148: LGTM!

Registration with Command: true, Script: true correctly 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 subworkflow and dag executor types with shared SubDAG: true capability. Reusing the caps variable 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 that step.Commands is 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 for step.ShellCmdArgs is already properly implemented.

The field is defined in the Step struct 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 using ShellCmdArgs instead of the Commands array continue to work. This pattern mirrors the deprecated Command field, 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 nil validator is appropriate. The spec layer (internal/core/spec/step.go) validates executor capabilities using core.SupportsCommand(), core.SupportsMultipleCommands(), and core.SupportsShell() checks, which query the capabilities registered via RegisterExecutor. 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 nil to invoke the image's default entrypoint, with proper error handling and stderr tail augmentation.


255-279: LGTM!

Single command path uses the straightforward Run approach 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.

Comment thread internal/core/capabilities.go
slog.Any("cmd", cmd),
)

exitCode, err := e.container.Exec(ctx, cmd, e.stdout, e.stderr, ExecOptions{})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the executor.go file around the mentioned lines
wc -l internal/runtime/builtin/docker/executor.go

Repository: 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 10

Repository: dagu-org/dagu

Length of output: 1522


🏁 Script executed:

# Check how ExecOptions is used throughout the codebase
rg -n "ExecOptions{" --type=go

Repository: 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 3

Repository: 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 -60

Repository: 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=go

Repository: 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 -20

Repository: dagu-org/dagu

Length of output: 785


🏁 Script executed:

# Find Step type definition
fd -e go | xargs grep -l "type Step struct" --type=go

Repository: 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 -80

Repository: 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.go

Repository: 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 -20

Repository: 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 -60

Repository: 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.go

Repository: 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/*.go

Repository: 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 -100

Repository: 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 3

Repository: 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 20

Repository: 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 -30

Repository: 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 2

Repository: 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.

Comment thread internal/runtime/builtin/ssh/ssh.go
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 return ExecutorCapabilities{} (all false) remains valid.

🧹 Nitpick comments (1)
internal/core/capabilities.go (1)

30-33: Consider validating executorType parameter.

The Register method doesn't validate that executorType is 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

📥 Commits

Reviewing files that changed from the base of the PR and between b0d8588 and 923f304.

📒 Files selected for processing (6)
  • internal/core/capabilities.go
  • internal/core/spec/errors.go
  • internal/core/spec/step.go
  • internal/core/spec/step_test.go
  • internal/runtime/builtin/dag/dag.go
  • internal/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 in cmd/ orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under internal/* (for example internal/runtime, internal/persistence)
Keep Go files gofmt/goimports clean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, and Err... names for package-level errors
Repository linting relies on golangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers in internal/common

Files:

  • internal/core/spec/step_test.go
  • internal/core/capabilities.go
  • internal/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
Use stretchr/testify/require and shared fixtures from internal/test instead 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 ExecutorCapabilities struct 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 (RegisterExecutorCapabilities and the Supports* 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 RegisterExecutorCapabilities calls occur exclusively in init() functions across the builtin executors (command, docker, ssh, jq, http, mail, archive, gha, dag, parallel). Since Go serializes all init() functions before main() executes, all registrations complete before any concurrent access. After initialization, the map is only read via the public Get() and Supports*() 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:

  1. Commands slice is populated with the new CommandEntry structure
  2. Legacy fields (Command, Args, CmdWithArgs) remain empty after build
  3. 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:

  1. Early conflict validation (line 196) - Checks mutual exclusivity before building
  2. Action-defining transformations (lines 201-218) - Builds container, parallel, subDAG, executor, command, params
  3. 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:

  1. Nil command - no-op
  2. String command - delegates to buildSingleCommand
  3. Array command - delegates to buildMultipleCommands

Clear error message for invalid types.


627-656: LGTM - Correct single command parsing.

The function correctly:

  1. Validates non-empty input
  2. Treats multiline strings as scripts
  3. Parses single-line commands into command and args
  4. Populates the Commands slice (not legacy fields)

The use of cmdutil.SplitCommand ensures consistent parsing including pipe handling.


660-711: LGTM - Flexible multiple command parsing.

The function provides good flexibility:

  1. Accepts arrays of strings and primitives (converted to strings)
  2. Filters out empty entries automatically
  3. Provides clear error messages with array indices
  4. 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:

  1. Early return if field is empty/nil
  2. Check capability via core.Supports*() functions
  3. 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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 7445908 and 622fef5.

📒 Files selected for processing (7)
  • internal/common/cmdutil/shell.go
  • internal/common/cmdutil/shell_test.go
  • internal/core/capabilities.go
  • internal/core/capabilities_test.go
  • internal/core/spec/step_test.go
  • internal/runtime/builtin/ssh/ssh.go
  • internal/runtime/builtin/ssh/ssh_test.go
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Backend entrypoint in cmd/ orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under internal/* (for example internal/runtime, internal/persistence)
Keep Go files gofmt/goimports clean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, and Err... names for package-level errors
Repository linting relies on golangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers in internal/common

Files:

  • internal/common/cmdutil/shell_test.go
  • internal/common/cmdutil/shell.go
  • internal/core/spec/step_test.go
  • internal/runtime/builtin/ssh/ssh_test.go
  • internal/core/capabilities_test.go
  • internal/core/capabilities.go
  • internal/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
Use stretchr/testify/require and shared fixtures from internal/test instead of duplicating mocks

Files:

  • internal/common/cmdutil/shell_test.go
  • internal/core/spec/step_test.go
  • internal/runtime/builtin/ssh/ssh_test.go
  • internal/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.go
  • internal/core/spec/step_test.go
  • internal/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.go
  • internal/core/spec/step_test.go
  • internal/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.go
  • internal/runtime/builtin/ssh/ssh_test.go
  • internal/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 ShellQuote to 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/assert and 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/assert align 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 Run method correctly:

  • Creates a new SSH session per command (required since session.Run can only be called once)
  • Uses cmdutil.ShellQuote and cmdutil.ShellQuoteArgs to 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, and Shell, accurately reflecting its implementation capabilities.

internal/core/capabilities.go (3)

3-19: LGTM! Well-defined capability flags for executor features.

The ExecutorCapabilities struct 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, and bool types 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.

@yottahmd yottahmd changed the title Add support for multiple command feat(all): multiple command support Dec 26, 2025
@yottahmd yottahmd changed the title feat(all): multiple command support feat(all): multiple commands syntax Dec 26, 2025
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 Run method correctly:

  • Creates a new session per command (required by SSH's single-use session constraint)
  • Applies shell quoting via cmdutil.ShellQuote to 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 launching session.Run() in a goroutine and selecting on both its completion and ctx.Done(), calling Kill() if cancelled mid-execution.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 622fef5 and 6df3b5e.

📒 Files selected for processing (2)
  • internal/persistence/filedag/store_test.go
  • internal/runtime/builtin/ssh/ssh.go
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Backend entrypoint in cmd/ orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under internal/* (for example internal/runtime, internal/persistence)
Keep Go files gofmt/goimports clean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, and Err... names for package-level errors
Repository linting relies on golangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers in internal/common

Files:

  • internal/persistence/filedag/store_test.go
  • internal/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
Use stretchr/testify/require and shared fixtures from internal/test instead 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 TestMain and is correctly ordered.


19-28: LGTM! Clean test setup for executor capabilities.

The TestMain correctly 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, and WorkerSelector capabilities 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 refactored Run method.

Comment thread internal/runtime/builtin/ssh/ssh.go
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 with WorkingDir: e.step.Dir to 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 with WorkingDir: 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.session field is set at line 125 but never cleared after the command completes. If Kill() is called after runCommand() finishes (e.g., between commands in the loop), it will call Close() 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.Command and cmdEntry.Args appears 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 docker struct:

// 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 in runInNewContainer (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 adding t.Parallel() to subtests for consistency.

Other validation test functions (e.g., TestValidateMultipleCommands, TestValidateScript) call t.Parallel() inside the subtest t.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

📥 Commits

Reviewing files that changed from the base of the PR and between 6df3b5e and b595bdd.

📒 Files selected for processing (3)
  • internal/core/spec/step_test.go
  • internal/runtime/builtin/docker/executor.go
  • internal/runtime/builtin/ssh/ssh.go
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Backend entrypoint in cmd/ orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under internal/* (for example internal/runtime, internal/persistence)
Keep Go files gofmt/goimports clean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, and Err... names for package-level errors
Repository linting relies on golangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers in internal/common

Files:

  • internal/runtime/builtin/docker/executor.go
  • internal/runtime/builtin/ssh/ssh.go
  • internal/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
Use stretchr/testify/require and shared fixtures from internal/test instead 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 session field against concurrent access between Kill() and runCommand(), 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.ShellQuote and ShellQuoteArgs to 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: true and MultipleCommands: true while omitting Shell (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, and Container capabilities, while appropriately excluding Script and Shell since 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 Run method correctly routes execution to runInExistingContainer when a shared container client exists (line 156-158) and runInNewContainer otherwise. The condition cli != nil && e.cfg == nil appropriately 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 Commands slice 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.6 are 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.

@ghansham
Copy link
Copy Markdown

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.

@yottahmd yottahmd merged commit 3e9d49d into main Dec 26, 2025
6 checks passed
@yottahmd yottahmd deleted the support-multiple-command branch December 26, 2025 18:42
@codecov
Copy link
Copy Markdown

codecov Bot commented Dec 26, 2025

Codecov Report

❌ Patch coverage is 55.71178% with 252 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.40%. Comparing base (05452f9) to head (b595bdd).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
internal/runtime/builtin/docker/executor.go 5.82% 97 Missing ⚠️
internal/runtime/builtin/ssh/ssh.go 9.75% 37 Missing ⚠️
internal/core/spec/step.go 83.73% 16 Missing and 11 partials ⚠️
internal/runtime/builtin/command/multi_command.go 67.69% 21 Missing ⚠️
internal/core/step.go 29.62% 19 Missing ⚠️
internal/runtime/builtin/docker/client.go 0.00% 12 Missing ⚠️
internal/runtime/builtin/gha/executor.go 30.76% 9 Missing ⚠️
internal/runtime/data.go 0.00% 7 Missing ⚠️
internal/runtime/builtin/dag/dag.go 0.00% 5 Missing ⚠️
internal/core/capabilities.go 81.81% 4 Missing ⚠️
... and 4 more
Additional details and impacted files

Impacted file tree graph

@@            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     
Files with missing lines Coverage Δ
internal/cmd/exec_spec.go 62.29% <100.00%> (+0.62%) ⬆️
internal/common/cmdutil/shell.go 100.00% <100.00%> (ø)
internal/core/spec/builder.go 82.06% <100.00%> (ø)
internal/runtime/agent/reporter.go 91.13% <100.00%> (+0.51%) ⬆️
internal/runtime/builtin/command/command.go 92.81% <100.00%> (+0.69%) ⬆️
internal/runtime/builtin/command/command_script.go 68.88% <100.00%> (+0.70%) ⬆️
internal/runtime/builtin/http/http.go 73.26% <100.00%> (+1.10%) ⬆️
internal/runtime/builtin/jq/jq.go 79.01% <100.00%> (+0.80%) ⬆️
internal/runtime/builtin/mail/mail.go 42.85% <100.00%> (ø)
internal/runtime/executor/executor.go 0.00% <0.00%> (ø)
... and 13 more

... and 4 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 64f1884...b595bdd. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants