-
-
Notifications
You must be signed in to change notification settings - Fork 231
Description
Base Config workingDir Cannot Be Inherited
Issue Summary
Affected Versions: 1.30.3 (likely all versions with base config support)
Severity: Minor — easy workaround, but surprising behavior
Problem
Setting workingDir in the base config file (DAGU_BASE_CONFIG) has no effect. Every workflow gets its own workingDir regardless of the base config value.
# base.yaml (pointed to by DAGU_BASE_CONFIG)
workingDir: ${PROJECT_ROOT}# workflows/bootstrap.yaml — no workingDir set
name: bootstrap
steps:
- name: check
command: nu -c "print (pwd)"
# Expected: C:\project (from base config)
# Actual: C:\project\workflows (workflow file's parent directory)Root Cause
buildWorkingDir in internal/core/spec/dag.go always returns a non-empty string. When the workflow doesn't set workingDir, the builder falls back to the workflow file's parent directory:
func buildWorkingDir(ctx BuildContext, d *dag) (string, error) {
switch {
case d.WorkingDir != "":
// Workflow explicitly sets workingDir — use it
wd := d.WorkingDir
// ... resolve and return
case ctx.opts.DefaultWorkingDir != "":
// Sub-DAG execution inherits parent's workingDir
return ctx.opts.DefaultWorkingDir, nil
case ctx.file != "":
// Fallback: use workflow file's parent directory
return filepath.Dir(ctx.file), nil // <-- always non-empty
default:
dir, _ := os.Getwd()
// ... return cwd or home
}
}The merge in loadDAGsFromFile uses mergo.Merge(dest, src, mergo.WithOverride) where dest is the base DAG and src is the workflow DAG. With WithOverride, non-zero src values overwrite dst values. Since the workflow's WorkingDir is always non-empty (from the fallback above), it always overwrites the base config's value.
// In loadDAGsFromFile:
dest = baseDAG // workingDir: "C:\project" (from base config)
dag = spec.build(ctx) // workingDir: "C:\project\workflows" (fallback to file dir)
merge(dest, dag) // result: "C:\project\workflows" — workflow winsWhy Other Fields Work
Fields like dotenv work because the mergeTransformer has special handling for []string — it appends instead of overwriting. And fields that aren't set in the workflow spec get zero values from the builder, so the base config's values survive the merge.
workingDir is unique in that it always gets a non-zero fallback value from the builder, even when the user didn't set it.
Suggested Fix
The fix is to distinguish between "user explicitly set workingDir" and "builder filled in a default". The simplest approach: return empty from buildWorkingDir when the user didn't set it, and apply the fallback after the merge.
Option A: Return empty when not explicitly set
In buildWorkingDir, return empty when falling through to the file-directory fallback. Then apply the fallback in InitializeDefaults (or a new post-merge step):
func buildWorkingDir(ctx BuildContext, d *dag) (string, error) {
switch {
case d.WorkingDir != "":
wd := d.WorkingDir
if !ctx.opts.Has(BuildFlagNoEval) {
wd = os.ExpandEnv(wd)
switch {
case filepath.IsAbs(wd) || strings.HasPrefix(wd, "~"):
wd = fileutil.ResolvePathOrBlank(wd)
case ctx.file != "":
wd = filepath.Join(filepath.Dir(ctx.file), wd)
default:
wd = fileutil.ResolvePathOrBlank(wd)
}
}
return wd, nil
case ctx.opts.DefaultWorkingDir != "":
return ctx.opts.DefaultWorkingDir, nil
default:
// Don't fill in a default — let the merge preserve the base config value.
// The fallback is applied after merge in InitializeDefaults.
return "", nil
}
}Then in InitializeDefaults:
func (d *DAG) initializeDefaults() {
// ... existing defaults ...
// Apply workingDir fallback after merge
if d.WorkingDir == "" {
if d.Location != "" {
d.WorkingDir = filepath.Dir(d.Location)
} else {
d.WorkingDir, _ = os.Getwd()
}
}
}This way, a workflow that doesn't set workingDir gets an empty value from the builder, the merge preserves the base config's value, and InitializeDefaults applies the file-directory fallback only if neither the base nor the workflow set it.
Option B: Track explicit vs. implicit in a flag
Add a WorkingDirExplicit bool field to core.DAG that's set to true only when the user provides workingDir in the YAML. The merge logic can then skip overwriting when the workflow's WorkingDirExplicit is false.
This is more complex but avoids changing the builder's return semantics.
Workaround
Core workflows that need workingDir set to the piper root can use:
workingDir: ${PROJECT_ROOT}${PROJECT_ROOT} is set on the dagu service process environment (not from dotenv), so it expands correctly at build time via os.ExpandEnv.
Bundle workflows can omit workingDir entirely — the default (workflow file's parent directory) is correct for them since scripts are relative to the bundle root.
References
- Source:
internal/core/spec/dag.go—buildWorkingDirfunction - Source:
internal/core/spec/loader.go—loadDAGsFromFile,mergefunction - Discovered: 2026-02-10