Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pkg/executor/flow_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,10 @@ func (fr *FlowRunner) executeStep(idx int, step flow.Step) (report.Status, strin
switch s := step.(type) {
// JS/Scripting steps - handled by ScriptEngine
case *flow.DefineVariablesStep:
fr.script.ExpandStep(step)
result = fr.script.ExecuteDefineVariables(s)
case *flow.RunScriptStep:
fr.script.ExpandStep(step)
result = fr.script.ExecuteRunScript(s)
case *flow.EvalScriptStep:
result = fr.script.ExecuteEvalScript(s)
Expand Down Expand Up @@ -796,8 +798,10 @@ func (fr *FlowRunner) executeNestedStep(step flow.Step) *core.CommandResult {

switch s := step.(type) {
case *flow.DefineVariablesStep:
fr.script.ExpandStep(step)
result = fr.script.ExecuteDefineVariables(s)
case *flow.RunScriptStep:
fr.script.ExpandStep(step)
result = fr.script.ExecuteRunScript(s)
case *flow.EvalScriptStep:
result = fr.script.ExecuteEvalScript(s)
Expand Down
38 changes: 30 additions & 8 deletions pkg/executor/scripting.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,13 @@ func (se *ScriptEngine) RunScript(script string, env map[string]string) error {
// Expand variables in script
script = se.ExpandVariables(script)

// Apply env variables
// Apply env variables. Values are expanded through ExpandVariables so
// `${VAR}` / `${VAR || "default"}` references resolve against the parent
// scope (CLI -e flags, flow Config.Env, prior runScript output). Mirrors
// withEnvVars used by runFlow — keeps env semantics consistent across
// step types so the YAML pattern `MY_KEY: ${MY_KEY}` works the same way.
for k, v := range env {
se.SetVariable(k, v)
se.SetVariable(k, se.ExpandVariables(v))
}

// Pre-define potential env variables as undefined to avoid ReferenceError.
Expand Down Expand Up @@ -475,6 +479,18 @@ func conditionTimeout(cond flow.Condition, sel *flow.Selector) int {
return 0 // Optional=true on the step means driver uses OptionalFindTimeout (7s)
}

// expandEnvMap expands ${VAR} / ${VAR || "default"} in every value of a YAML
// `env:` map, in place. Used by ExpandStep for any step type whose Env field
// references parent-scope variables — keeps env semantics identical across
// runScript, runFlow, runBrowserScript, runWebViewScript, retry, and
// LaunchAppStep.Environment / .Arguments. No-op for keys whose values contain
// no `${...}` so plain literals stay untouched.
func (se *ScriptEngine) expandEnvMap(env map[string]string) {
for k, v := range env {
env[k] = se.ExpandVariables(v)
}
}

// withEnvVars applies environment variables and returns a restore function.
// Values are expanded through ExpandVariables to support ${VAR || "default"} syntax.
func (se *ScriptEngine) withEnvVars(env map[string]string) func() {
Expand Down Expand Up @@ -536,9 +552,17 @@ func (se *ScriptEngine) ExpandStep(step flow.Step) {
s.Arguments[k] = se.ExpandVariables(str)
}
}
for k, v := range s.Environment {
s.Environment[k] = se.ExpandVariables(v)
}
se.expandEnvMap(s.Environment)
case *flow.RunScriptStep:
se.expandEnvMap(s.Env)
case *flow.RunBrowserScriptStep:
se.expandEnvMap(s.Env)
case *flow.RunWebViewScriptStep:
se.expandEnvMap(s.Env)
case *flow.RetryStep:
se.expandEnvMap(s.Env)
case *flow.DefineVariablesStep:
se.expandEnvMap(s.Env)
case *flow.StopAppStep:
s.AppID = se.ExpandVariables(s.AppID)
case *flow.KillAppStep:
Expand All @@ -565,9 +589,7 @@ func (se *ScriptEngine) ExpandStep(step flow.Step) {
s.When.Platform = se.ExpandVariables(s.When.Platform)
}
}
for k, v := range s.Env {
s.Env[k] = se.ExpandVariables(v)
}
se.expandEnvMap(s.Env)
}
}

Expand Down
90 changes: 90 additions & 0 deletions pkg/executor/scripting_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,31 @@ func TestScriptEngine_RunScript_WithEnv(t *testing.T) {
}
}

func TestScriptEngine_RunScript_EnvExpandsVariableRefs(t *testing.T) {
// runScript's `env:` block must expand ${VAR} against the parent scope,
// matching runFlow's withEnvVars semantics. This is the YAML pattern
// - runScript:
// env:
// BASE_URL: ${BASE_URL}
// where the outer BASE_URL came from a CLI -e flag or flow Config.Env.
se := NewScriptEngine()
defer se.Close()

se.SetVariable("BASE_URL", "https://api.example.com")

err := se.RunScript("output.url = BASE_URL", map[string]string{
"BASE_URL": "${BASE_URL}",
})
if err != nil {
t.Fatalf("RunScript() error = %v", err)
}

if got := se.GetVariable("url"); got != "https://api.example.com" {
t.Errorf("url = %q, want %q (env value was passed through verbatim instead of expanded)",
got, "https://api.example.com")
}
}

func TestScriptEngine_RunScript_Error(t *testing.T) {
se := NewScriptEngine()
defer se.Close()
Expand Down Expand Up @@ -1018,6 +1043,71 @@ func TestScriptEngine_ExpandStep_LaunchAppStep_Environment(t *testing.T) {
}
}

func TestScriptEngine_ExpandStep_RunScriptStep_Env(t *testing.T) {
se := NewScriptEngine()
defer se.Close()

se.SetVariable("BASE_URL", "https://api.example.com")
se.SetVariable("STAGE", "staging")

step := &flow.RunScriptStep{
File: "seed.js",
Env: map[string]string{
"BASE_URL": "${BASE_URL}",
"ENV": "${STAGE}",
"LITERAL": "hardcoded_value",
},
}

se.ExpandStep(step)

if step.Env["BASE_URL"] != "https://api.example.com" {
t.Errorf("Env[BASE_URL] = %q, want %q", step.Env["BASE_URL"], "https://api.example.com")
}
if step.Env["ENV"] != "staging" {
t.Errorf("Env[ENV] = %q, want %q", step.Env["ENV"], "staging")
}
if step.Env["LITERAL"] != "hardcoded_value" {
t.Errorf("Env[LITERAL] = %q (literals must pass through untouched)", step.Env["LITERAL"])
}
}

func TestScriptEngine_ExpandStep_RunBrowserScriptStep_Env(t *testing.T) {
se := NewScriptEngine()
defer se.Close()

se.SetVariable("API_KEY", "secret123")

step := &flow.RunBrowserScriptStep{
File: "probe.js",
Env: map[string]string{"KEY": "${API_KEY}"},
}

se.ExpandStep(step)

if step.Env["KEY"] != "secret123" {
t.Errorf("Env[KEY] = %q, want %q", step.Env["KEY"], "secret123")
}
}

func TestScriptEngine_ExpandStep_RunWebViewScriptStep_Env(t *testing.T) {
se := NewScriptEngine()
defer se.Close()

se.SetVariable("API_KEY", "secret123")

step := &flow.RunWebViewScriptStep{
File: "probe.js",
Env: map[string]string{"KEY": "${API_KEY}"},
}

se.ExpandStep(step)

if step.Env["KEY"] != "secret123" {
t.Errorf("Env[KEY] = %q, want %q", step.Env["KEY"], "secret123")
}
}

func TestScriptEngine_ExpandStep_StopAppStep(t *testing.T) {
se := NewScriptEngine()
defer se.Close()
Expand Down