Skip to content

Add Deno-backed external actions - #2166

Closed
yohamta0 wants to merge 5 commits into
mainfrom
feature/deno-actions
Closed

yohamta0 wants to merge 5 commits into
mainfrom
feature/deno-actions

Conversation

@yohamta0

@yohamta0 yohamta0 commented May 16, 2026 •

Copy link
Copy Markdown
Member

Summary

Add a generic external action executor backed by Deno so Dagu can run third-party actions without expanding core builtins for every integration.

Changes

  • Add action executor support for Deno-based external actions.
  • Normalize action: source:...@... and action: pkg:...@... into the executor config.
  • Support explicit type: action with ref and input config.
  • Resolve source actions from local paths or git refs, with cached clones.
  • Resolve packaged actions from local registry directories or file:// package dirs.
  • Load dagu-action.yaml, validate inputs and permissions, and run Deno with constrained read/write/net/env permissions.
  • Install the requested Deno version through the existing aqua-backed tools cache.
  • Expose DAGU_TOOLS_DIR for runs without declared DAG tools.
  • Fix aqua first-run manifest handling when manifest.json does not exist yet.

Related Issues

None linked.

Validation

  • go test ./internal/core ./internal/runtime ./internal/runtime/builtin/... ./internal/engine ./internal/cmd ./internal/service/worker ./internal/tools ./internal/tools/aqua ./internal/cmn/schema
  • DAGU_ACTION_DENO_INTEGRATION=1 go test -count=1 -run TestExecutorRunsLocalSourceActionWithRealDenoIntegration -v ./internal/runtime/builtin/action
  • DAGU_AQUA_INTEGRATION=1 go test -count=1 -run TestInstallerInstallIntegration -v ./internal/tools/aqua
  • go fix -diff ./internal/runtime/builtin/action

Checklist

  • Code is formatted and modernized for the repo's Go version.
  • Unit tests cover source/pkg normalization, package registry resolution, manifest validation, write permission path resolution, and tools-dir env exposure.
  • Integration tests verify real Deno execution through aqua when explicitly enabled.
  • Documentation and public examples will be handled separately.

@coderabbitai

coderabbitai Bot commented May 16, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ba5b8017-1989-4ad4-a8cc-d5eefeffe8f9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces a new "action" executor type for DAG steps that can execute Deno-based actions from remote sources and registries. The implementation spans schema updates, step normalization, bundle resolution with git caching, manifest validation, and a full Deno-based executor with permission-driven CLI invocation.

Changes

Action Executor Framework

Layer / File(s) Summary
Schema and step type recognition
internal/cmn/schema/dag.schema.json, internal/cmn/schema/dag_schema_test.go, internal/core/spec/step_types.go
DAG schema now recognizes "action" as a builtin executor type with actionExecutorConfig (required ref, optional input). Updated actionName validation to accept source: and pkg: reference patterns. Step type constants include "action" to prevent name conflicts with user-defined actions.
Step normalization for remote action references
internal/core/spec/step_v2.go, internal/core/spec/step_v2_test.go, internal/core/step.go
normalizeActionStep detects remote references (source:/pkg: prefixes) and normalizes them into type: action with ref and optional input fields derived from YAML with blocks. New ExecutorTypeAction constant added to core step types.
Action bundle resolution
internal/runtime/builtin/action/resolver.go, internal/runtime/builtin/action/action_test.go
Complete resolver for source: and pkg: action references. Source refs use git cloning with SHA-256 content-addressed caching and optional workdir-relative resolution. Package refs use registry-based lookup under ToolsDir or explicit RegistryDir. Includes strict path confinement validation and traversal checks.
Action manifest and Deno runtime integration
internal/runtime/builtin/action/manifest.go, internal/runtime/builtin/action/deno.go, internal/runtime/builtin/action/action_test.go
Manifest loading/validation for dagu-action.yaml enforcing required name/runtime/entrypoint and safe relative permission paths. Deno resolver handles version requests, aqua-based installation when needed, and verification that installed manifest exposes the deno command.
Action executor implementation
internal/runtime/builtin/action/action.go, internal/runtime/builtin/action/action_integration_test.go
Main executor that parses ref/input config, creates run directories, writes input.json, installs Deno dependencies, executes deno with permission flags from manifest (read/write/net/env), captures and validates output.json. Supports stdout/stderr redirection and process termination via Kill.
Tool environment and executor wiring
internal/tools/tools.go, internal/tools/prepare.go, internal/engine/run.go, internal/tools/aqua/installer.go, internal/tools/tools_test.go
Extended tools infrastructure with DAGU_TOOLS_DIR environment variable constant and ToolDirEnvVars() helper. Updated PrepareDAG to return tool env vars even when DAG has no declared tools. Engine local execution passes tool dirs via ExtraEnvs. Updated aqua installer to use modern errors.Is pattern.
Builtin executor registration
internal/runtime/builtin/builtin.go
Registers the action executor implementation with the runtime's builtin factory via init side effects.

Sequence Diagram(s)

sequenceDiagram
  participant Client as DAG Step
  participant Resolver as resolveBundle
  participant Git as Git Ops
  participant Cache as Cache Dir
  participant Manifest as loadManifest
  participant Deno as resolveDeno
  participant Executor as Deno Process
  participant Output as output.json

  Client->>Resolver: source:github.com/...@v1.0
  alt Cached
    Resolver->>Cache: Check SHA-keyed cache
    Cache-->>Resolver: Return cached RootDir
  else Not Cached
    Resolver->>Git: Clone repo depth-1
    Git->>Git: Checkout version/branch
    Git-->>Resolver: Get commit SHA
    Resolver->>Cache: Store under SHA key
  end
  
  Resolver-->>Client: actionBundle{RootDir, Ref, Version}
  Client->>Manifest: Load dagu-action.yaml
  Manifest-->>Client: {name, runtime, entrypoint, permissions}
  
  Client->>Deno: Resolve deno `@version`
  alt Existing
    Deno->>Deno: Check manifest
    Deno-->>Client: /tools/deno/bin/deno
  else Install
    Deno->>Deno: aqua install denoland/deno
    Deno-->>Client: /tools/deno/bin/deno
  end
  
  Client->>Executor: deno run --allow-read --allow-write --allow-net --allow-env
  Executor->>Output: Action writes JSON
  Output-->>Client: {key: value}
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • dagucloud/dagu#2122: Extends action step validation and normalization in schema and step_v2 for the same "action" executor type.
  • dagucloud/dagu#2150: Related tool environment infrastructure (EnvToolsDir, ToolDirEnvVars) and aqua tools wiring that this PR builds upon.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add Deno-backed external actions' accurately and concisely describes the main change: introducing a new Deno-based external action executor. It is clear, specific, and directly related to the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description covers all required template sections with clear summary, detailed changes, validation evidence, and a comprehensive checklist.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/deno-actions

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/runtime/builtin/action/action.go`:
- Around line 225-231: The write permissions are being rebased under the run
output directory instead of the action root; update calls that pass outputDir
into writePermissions (e.g., in denoRunArgs) to pass the action root (rootDir)
so writePermissions resolves entries relative to the action root as validated by
loadManifest; apply the same change to the other call site that currently uses
outputDir (the analogous block around where writePermissions is invoked for
tests/other runs) and ensure you keep using the same permissionManifest
(perms.Write) and helper function names (writePermissions, readPermissions,
loadManifest) so resolution and validation remain consistent.

In `@internal/runtime/builtin/action/resolver.go`:
- Around line 181-183: The temporary clone path currently built as tmp :=
fmt.Sprintf("%s.%d.tmp", root, os.Getpid()) can collide for concurrent
resolution attempts; change creation of the temp directory used by runGit in
resolver.go to use a truly unique per-attempt directory (e.g., os.MkdirTemp /
os.CreateTemp or append a monotonic timestamp/UUID to root+pid) and use that
returned path as tmp, keeping the existing cleanup (os.RemoveAll(tmp)) and
subsequent atomic rename logic intact; update the code that references tmp and
the runGit call to use the new unique tmp variable to avoid races.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c610026c-760c-4c88-945b-b7014f10ff95

📥 Commits

Reviewing files that changed from the base of the PR and between 4d567dd and d809ed5.

📒 Files selected for processing (19)
  • internal/cmn/schema/dag.schema.json
  • internal/cmn/schema/dag_schema_test.go
  • internal/core/spec/step_types.go
  • internal/core/spec/step_v2.go
  • internal/core/spec/step_v2_test.go
  • internal/core/step.go
  • internal/engine/run.go
  • internal/runtime/builtin/action/action.go
  • internal/runtime/builtin/action/action_integration_test.go
  • internal/runtime/builtin/action/action_test.go
  • internal/runtime/builtin/action/deno.go
  • internal/runtime/builtin/action/manifest.go
  • internal/runtime/builtin/action/resolver.go
  • internal/runtime/builtin/builtin.go
  • internal/tools/aqua/installer.go
  • internal/tools/aqua/installer_test.go
  • internal/tools/prepare.go
  • internal/tools/tools.go
  • internal/tools/tools_test.go

Comment on lines +225 to +231
func denoRunArgs(rootDir, entrypoint, inputPath, outputDir string, perms permissionManifest) []string {
args := []string{
"run",
"--cached-only",
"--no-prompt",
"--allow-read=" + strings.Join(readPermissions(rootDir, inputPath, perms.Read), ","),
"--allow-write=" + strings.Join(writePermissions(outputDir, perms.Write), ","),

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 | 🟠 Major | ⚡ Quick win

Resolve permissions.write against the action root, not outputDir.

loadManifest validates write paths as action-root-relative paths, but writePermissions rebases the same entries under the run output directory. A manifest like permissions.write: ["cache/state.json"] will validate, then Deno gets access to <runDir>/cache/state.json instead of <actionDir>/cache/state.json.

Suggested fix
-		"--allow-write=" + strings.Join(writePermissions(outputDir, perms.Write), ","),
+		"--allow-write=" + strings.Join(writePermissions(rootDir, outputDir, perms.Write), ","),
-func writePermissions(outputDir string, extra []string) []string {
+func writePermissions(rootDir, outputDir string, extra []string) []string {
 	paths := []string{filepath.Clean(outputDir)}
 	for _, item := range extra {
 		item = strings.TrimSpace(item)
 		if item == "" || filepath.IsAbs(item) {
 			continue
 		}
-		path := filepath.Join(outputDir, item)
-		if isPathWithin(outputDir, path) {
-			paths = append(paths, filepath.Clean(path))
+		if path, err := safeRelativePath(rootDir, item); err == nil {
+			paths = append(paths, path)
 		}
 	}
 	return dedupeStrings(paths)
 }

Also applies to: 277-289

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/runtime/builtin/action/action.go` around lines 225 - 231, The write
permissions are being rebased under the run output directory instead of the
action root; update calls that pass outputDir into writePermissions (e.g., in
denoRunArgs) to pass the action root (rootDir) so writePermissions resolves
entries relative to the action root as validated by loadManifest; apply the same
change to the other call site that currently uses outputDir (the analogous block
around where writePermissions is invoked for tests/other runs) and ensure you
keep using the same permissionManifest (perms.Write) and helper function names
(writePermissions, readPermissions, loadManifest) so resolution and validation
remain consistent.

Comment on lines +181 to +183
tmp := fmt.Sprintf("%s.%d.tmp", root, os.Getpid())
_ = os.RemoveAll(tmp)
if err := runGit(ctx, "", "clone", "--depth", "1", "--branch", version, repoURL, tmp); err != nil {

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 | 🟠 Major | ⚡ Quick win

Make the clone temp directory unique per resolution attempt.

This path is only keyed by root and os.Getpid(), so parallel first-time installs of the same action inside one worker process will race on the same *.tmp directory. One clone can remove or rename the other and make resolution fail nondeterministically.

Suggested fix
-	tmp := fmt.Sprintf("%s.%d.tmp", root, os.Getpid())
-	_ = os.RemoveAll(tmp)
+	tmp, err := os.MkdirTemp(filepath.Dir(root), filepath.Base(root)+".*.tmp")
+	if err != nil {
+		return "", "", fmt.Errorf("create action source temp dir: %w", err)
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
tmp := fmt.Sprintf("%s.%d.tmp", root, os.Getpid())
_ = os.RemoveAll(tmp)
if err := runGit(ctx, "", "clone", "--depth", "1", "--branch", version, repoURL, tmp); err != nil {
tmp, err := os.MkdirTemp(filepath.Dir(root), filepath.Base(root)+".*.tmp")
if err != nil {
return "", "", fmt.Errorf("create action source temp dir: %w", err)
}
if err := runGit(ctx, "", "clone", "--depth", "1", "--branch", version, repoURL, tmp); err != nil {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/runtime/builtin/action/resolver.go` around lines 181 - 183, The
temporary clone path currently built as tmp := fmt.Sprintf("%s.%d.tmp", root,
os.Getpid()) can collide for concurrent resolution attempts; change creation of
the temp directory used by runGit in resolver.go to use a truly unique
per-attempt directory (e.g., os.MkdirTemp / os.CreateTemp or append a monotonic
timestamp/UUID to root+pid) and use that returned path as tmp, keeping the
existing cleanup (os.RemoveAll(tmp)) and subsequent atomic rename logic intact;
update the code that references tmp and the runGit call to use the new unique
tmp variable to avoid races.

@yohamta0 yohamta0 closed this May 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant