fix: correct CLI --driver flag priority in workflow execution#873
fix: correct CLI --driver flag priority in workflow execution#873
Conversation
The design doc specifies driver resolution priority as: CLI > step > job > workflow > definition > raw But the implementation had CLI as lowest priority — it only applied when step, job, and workflow didn't specify a driver. This flips options.driver to first position in the ?? chain so the CLI flag overrides all other sources, matching the documented behavior. Also adds a cli parameter to resolveDriverConfig() to keep the resolution contract in sync with the design doc, and adds tests for CLI override behavior.
There was a problem hiding this comment.
Adversarial Review
Critical / High
-
HIGH —
driverConfignot updated to match newdriverpriority inexecution_service.ts:1463-1466The
driverresolution was correctly reordered tooptions.driver ?? step.driver ?? job.driver ?? workflow.driver, but thedriverConfigon the very next line was not updated:driver: options.driver ?? step.driver ?? job.driver ?? workflow.driver, driverConfig: step.driverConfig ?? job.driverConfig ?? workflow.driverConfig,
Breaking scenario: A workflow step defines
driver: dockerwithdriverConfig: { image: "node:18" }. User runs with--driver raw. Result:driver: "raw"butdriverConfig: { image: "node:18" }— a docker config applied to the raw driver. Before this PR, the mismatch was nearly impossible becauseoptions.driverwas last priority and only won when no other level set a driver (and therefore likely no driverConfig either). Now thatoptions.driveris first priority, this mismatch becomes the common case whenever CLI override is used.Suggested fix: When
options.driverwins,driverConfigshould beundefined(or from the same source). The cleanest fix is to useresolveDriverConfig()fromdriver_resolution.ts— which already exists and correctly pairs driver+config from the winning level — instead of the manual??chains. Alternatively, adddriverConfigtoStepOptionsand thread CLI driver config through, then apply the same priority order. -
HIGH —
resolveDriverConfig()is never used in production codeThe PR adds a
cliparameter toresolveDriverConfig()indriver_resolution.tsand adds tests for it, butexecution_service.ts(the actual production path) does NOT callresolveDriverConfig()at all — it uses a hand-rolled??chain. The function is exported frommod.tsbut has zero production callers. This means:- The well-tested resolution function with correct driver+config pairing is dead code
- The actual production path at
execution_service.ts:1463-1466has the driver/config mismatch bug described above - The new tests give false confidence — they test a function that isn't used where it matters
Suggested fix: Refactor
execution_service.tsto callresolveDriverConfig()instead of the manual??chains. This would fix finding #1 and eliminate the dead code problem.
Medium
-
MEDIUM —
execution_service.ts:464-465has the same split-resolution patternLine 464-465 also resolves
driveranddriverConfigindependently:driver: ctx.driver ?? evaluatedDefinition.driver, driverConfig: ctx.driverConfig ?? evaluatedDefinition.driverConfig,
This is a pre-existing issue (not introduced by this PR) but is the same anti-pattern. If
ctx.driveris set butctx.driverConfigis not, the config fromevaluatedDefinitionleaks through for a different driver. Worth noting since this PR is fixing driver resolution.
Low
None.
Verdict
FAIL — The driverConfig resolution in execution_service.ts was not updated to match the new driver priority, creating a driver/config mismatch whenever --driver CLI flag overrides a workflow-level driver that has config. The resolveDriverConfig() function (which handles this correctly) is dead code.
There was a problem hiding this comment.
Code Review
Blocking Issues
-
driverConfignot updated to matchdriverpriority (execution_service.ts:1465): Thedriverchain was correctly reordered tooptions.driver ?? step.driver ?? job.driver ?? workflow.driver, but thedriverConfigchain on the very next line is stillstep.driverConfig ?? job.driverConfig ?? workflow.driverConfig. This means when CLI--driver rawwins, thedriverConfigwill still come from a lower-priority source (e.g., a Docker step's{ image: "node:18" }), producing a driver/config mismatch. The design doc states: "The first non-undefined driver value wins. Its corresponding driverConfig is used as-is — configs are not merged across levels."The cleanest fix would be to use the already-updated
resolveDriverConfig()function here instead of inline??chains. It correctly pairs driver with its matching config from the winning source, and it's already exported fromsrc/domain/drivers/mod.tsbut currently unused in production code. Something like:const resolved = resolveDriverConfig( { driver: options.driver }, { driver: step.driver, driverConfig: step.driverConfig }, { driver: job.driver, driverConfig: job.driverConfig }, { driver: workflow.driver, driverConfig: workflow.driverConfig }, ); // then use resolved.driver and resolved.driverConfig
Suggestions
-
resolveDriverConfig()is dead code in production: The function is exported frommod.tsand well-tested, but no production code calls it. If the blocking issue above is fixed by using it inexecution_service.ts, this is resolved automatically. -
Test name change on line 75: The rename from
"step driver wins over all"to"step driver wins over job"is more precise — nice improvement. Just noting it accurately reflects the new semantics where CLI is now the highest priority.
Overall this is a well-motivated bug fix with good test coverage for the resolveDriverConfig function itself. The remaining gap is in wiring it into the actual execution path.
Addresses review feedback: when CLI --driver wins, driverConfig must come from the same source (undefined for CLI) rather than leaking from a lower-priority level. Replaces the inline ?? chains in execution_service.ts with resolveDriverConfig() which correctly pairs driver and driverConfig from the winning resolution level. This also eliminates resolveDriverConfig as dead code — it is now used in the production execution path.
There was a problem hiding this comment.
Code Review
This is a clean, well-scoped bug fix that correctly aligns the CLI --driver flag priority with the design doc (design/execution-drivers.md). The change is minimal and focused.
Blocking Issues
None.
Suggestions
-
Missing blank line before doc comment (line 93-94 in
execution_service.ts): The newimport { resolveDriverConfig }line is immediately followed by a/**doc comment with no separating blank line. Not a formatting error per se (deno fmt passed), but slightly inconsistent with the surrounding style. -
definitionsource not passed at call site: TheresolveDriverConfigcall inexecution_service.ts:1464passes 4 sources (cli, step, job, workflow) but omits thedefinitionsource. The old code also didn't include it — it was resolved elsewhere at line 466 (ctx.driverConfig ?? evaluatedDefinition.driverConfig). This is a pre-existing gap, not a regression. Worth a follow-up to unify definition-level resolution throughresolveDriverConfigso all 6 levels from the design doc go through one path.
What looks good
- Fix correctly matches the design doc priority order
resolveDriverConfigis a proper stateless domain service — correct DDD building block- Test coverage is thorough: 11 tests covering every precedence level including 3 new CLI override tests
- Test naming follows project conventions (
functionName: describes behavior) - License headers present, import paths are domain-internal (no libswamp boundary violation)
- No security concerns
There was a problem hiding this comment.
Adversarial Review
Critical / High
None.
Medium
-
Undocumented behavioral change in
driverConfigresolution (execution_service.ts:1464-1469)The old code resolved
driveranddriverConfigindependently:driver: step.driver ?? job.driver ?? workflow.driver ?? options.driver, driverConfig: step.driverConfig ?? job.driverConfig ?? workflow.driverConfig,
The new code couples them via
resolveDriverConfig—driverConfigalways comes from the same level as the winningdriver.Breaking scenario: A workflow where a step sets
driver: dockerbut nodriverConfig, and the job setsdriverConfig: { image: "node:18" }. Old behavior: step's driver + job's driverConfig. New behavior: step's driver +undefineddriverConfig (falling through toevaluatedDefinition.driverConfigat line 466).This is arguably the correct behavior per the design doc ("no merging across levels"), but it's a silent behavioral change beyond the stated priority fix that could affect existing workflows. Worth calling out in the PR description.
-
Missing
definitionin call site (execution_service.ts:1464-1469)resolveDriverConfigaccepts adefinitionparameter but the call site doesn't pass it — the definition-level driver/driverConfig is handled separately downstream at line 465-466 with independent??chains:driver: ctx.driver ?? evaluatedDefinition.driver, driverConfig: ctx.driverConfig ?? evaluatedDefinition.driverConfig,
This means the "no merging across levels" guarantee of
resolveDriverConfigis circumvented for the definition level: if CLI wins withdriver: docker(no driverConfig),ctx.driverConfigis undefined, soevaluatedDefinition.driverConfigis used — mixing CLI driver with definition-level config. Not necessarily wrong, but it's inconsistent with the resolution function's stated contract.
Low
- Missing blank line between import and JSDoc (
execution_service.ts:93-94) — The new import at line 93 butts directly against the JSDoc comment at line 94. Style-only, not functional.
Verdict
PASS — The core fix is correct: CLI --driver now takes highest priority as specified in the design doc. The resolveDriverConfig function is clean and well-tested. The medium findings are about secondary behavioral changes that align with the design intent but should be documented.
Summary
--driverflag was resolved as lowest priority (fallback), but the design doc (design/execution-drivers.md) specifies it should be highest priorityoptions.driverto first position in the??chain inexecution_service.tsso CLI overrides step/job/workflow driver settingscliparameter toresolveDriverConfig()indriver_resolution.tsto keep the resolution function's contract in sync with the design docWhat was wrong
In
src/domain/workflows/execution_service.ts(line 1463), the driver resolution was:This made the CLI
--driverflag a fallback — it only applied when step, job, and workflow didn't specify a driver.What it should be (per design doc)
The fix
Also updated
resolveDriverConfig()to accept acliparameter as first in the resolution chain (cli > step > job > workflow > definition > raw), and added tests for CLI override behavior.Impact
Users passing
--driveron the CLI will now correctly override any driver configuration set in workflow YAML files. Previously, a workflow step withdriver: dockerwould silently ignore--driver rawfrom the CLI.Files changed
src/domain/workflows/execution_service.ts— flip??chain prioritysrc/domain/drivers/driver_resolution.ts— addcliparameter to resolution functionsrc/domain/drivers/driver_resolution_test.ts— update existing tests + add CLI override testsTest plan
deno check— type checking cleandeno lint— no warningsdeno fmt— properly formatted🤖 Generated with Claude Code