Skip to content

fix: skip globalArgument expressions with missing inputs in workflow execution#655

Merged
stack72 merged 1 commit intomainfrom
fix/skip-missing-inputs-workflow-653
Mar 8, 2026
Merged

fix: skip globalArgument expressions with missing inputs in workflow execution#655
stack72 merged 1 commit intomainfrom
fix/skip-missing-inputs-workflow-653

Conversation

@stack72
Copy link
Copy Markdown
Contributor

@stack72 stack72 commented Mar 8, 2026

Summary

Fixes #653.

The workflow execution path (evaluateDefinitionExpressions() in execution_service.ts) was evaluating all globalArgument CEL expressions unconditionally. When a factory model defines globalArguments referencing multiple inputs fields (e.g., instanceName, routeTableId, natGatewayId) but a workflow step only provides a subset (e.g., just instanceName for a delete operation), the CEL evaluator would fail with:

Invalid expression: No such key: cidrBlock

>    1 | inputs.cidrBlock
                ^

The CLI path (ExpressionEvaluationService.evaluateDefinition()) already had selective skipping logic that checks whether referenced inputs are actually provided before evaluating. This PR adds the same two-phase check to the workflow path:

  1. Skip expressions referencing unprovided inputs — builds a providedInputKeys set from context.inputs and skips any expression whose inputs.* references aren't in that set
  2. Skip expressions referencing unavailable model data — skips expressions referencing model.X.resource or model.X.file when that model data isn't in context

Skipped expressions stay as raw ${{ ... }} strings. The existing Proxy in method_execution_service.ts throws a clear error only if the method code actually tries to access an unresolved globalArgument at runtime — this is the correct design because it means expressions are evaluated lazily and errors only surface when a value is genuinely needed.

Design rationale

This selective skipping is the correct approach because:

  • Factory models reuse one definition for multiple lifecycle methods (create, delete, sync). Each method needs different inputs — delete typically only needs instanceName + identifier, not the full set of create-time parameters like cidrBlock or gatewayId.
  • Forcing all inputs defeats the purpose of factory models. Without this fix, delete workflow steps must duplicate all create-time parameters even though the delete method never accesses them.
  • Both code paths now behave identically. The CLI path (model method run) and the workflow path (workflow run) apply the same skipping logic, eliminating a confusing behavioral difference.

Testing

Automated tests

  • Added integration/workflow_partial_inputs_test.ts — creates a factory model with instanceName, cidrBlock, and gatewayId inputs in globalArguments, then runs a workflow providing only instanceName. Verifies the workflow succeeds.
  • All 2773 existing tests pass.

Manual verification

Created a test repo in /tmp/swamp-653-test with a factory model (partial-input-test) that has three inputs (instanceName, cidrBlock, gatewayId) wired into globalArguments, and a workflow that only provides instanceName.

Old binary (on PATH) — fails:

{
  "status": "failed",
  "error": "Invalid expression: No such key: cidrBlock\n\n>    1 | inputs.cidrBlock\n                ^"
}

Fixed binary (compiled) — succeeds:

{
  "status": "succeeded",
  "jobs": [{ "status": "succeeded", "steps": [{ "status": "succeeded" }] }]
}

Files changed

File Change
src/domain/workflows/execution_service.ts Add 2 imports + selective skipping in evaluateDefinitionExpressions()
integration/workflow_partial_inputs_test.ts New integration test for partial input evaluation in workflow path

Verification

  • deno check — passes
  • deno lint — passes
  • deno fmt — passes
  • deno run test — 2773 passed, 0 failed
  • deno run compile — binary compiles successfully

🤖 Generated with Claude Code

…execution (#653)

The workflow execution path (evaluateDefinitionExpressions) was evaluating
all globalArgument CEL expressions unconditionally, causing "No such key"
errors when factory model steps provided only partial inputs. The CLI path
(evaluateDefinition) already had selective skipping logic that checked
whether referenced inputs were actually provided before evaluating.

This adds the same two-phase check to the workflow path:
1. Skip expressions referencing inputs not in the caller's provided set
2. Skip expressions referencing model resource/file data not in context

Skipped expressions stay as raw ${{ ... }} strings. The existing Proxy in
method_execution_service.ts throws a clear error only if the method code
actually tries to access an unresolved globalArgument at runtime.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Review Summary

This PR correctly fixes #653 by adding selective skipping of globalArgument expressions in the workflow execution path, aligning it with the existing CLI path behavior.

No Blocking Issues

The code is well-implemented and ready to merge.

Code Quality Assessment

Compliance with CLAUDE.md:

  • TypeScript strict mode: No any types
  • Named exports used consistently
  • AGPLv3 license header present in the new test file
  • Integration test correctly placed in integration/ directory

DDD Perspective:
The implementation correctly applies domain logic to skip expressions referencing unavailable context (missing inputs or model data). The two-phase check approach is sound.

Test Coverage:
The integration test (integration/workflow_partial_inputs_test.ts) thoroughly covers the primary use case - a workflow step providing only a subset of inputs when globalArguments reference multiple inputs. This tests the fix through the actual workflow execution path.

Security:
No security vulnerabilities introduced. The code safely handles missing context by skipping evaluation rather than exposing errors.

Suggestions (Non-blocking)

  1. Code Duplication: The selective skipping logic in evaluateDefinitionExpressions() (lines 666-701) closely mirrors the logic in ExpressionEvaluationService.evaluateDefinition() (lines 217-254). Consider extracting this into a shared utility function to prevent future drift between the CLI and workflow paths. However, since both paths now behave identically, this is a future improvement opportunity rather than a blocking concern.

Summary

The PR:

  • Correctly fixes the bug where workflow steps failed with "No such key" errors when globalArguments referenced unprovided inputs
  • Aligns workflow path behavior with the existing CLI path
  • Includes a well-structured integration test
  • Follows project code style and DDD principles

LGTM!

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Adversarial Review

I performed a systematic adversarial review attempting to break this code across logic, error handling, security, concurrency, and data integrity dimensions.

Critical / High

None found. The code correctly mirrors the CLI path implementation.

Medium

  1. Bracket notation model references not detected by dependency extractor (execution_service.ts:683)

    • extractDependencies() uses regex model\.([a-zA-Z0-9_-]+)\.(input|resource|file|execution|definition) which matches dot notation only
    • If a user writes ${{ model["hyphenated-name"].resource }} directly (bracket notation), the dependency check won't detect it
    • Breaking example: Definition with globalArguments.arg: '${{ model["vpc-main"].resource.net.id }}' where vpc-main hasn't run - expression is evaluated and CEL throws
    • Mitigation: This is pre-existing behavior (same regex used in CLI path). The more common pattern model.vpc-main.resource IS matched correctly (hyphens allowed in character class).
    • Impact: Low - unusual syntax, fails safely with CEL error
  2. Only resource/file dependencies are checked (execution_service.ts:685)

    • extractDependencies() returns types including "data", "execution", "definition", but only "resource" and "file" trigger skip logic
    • If expression references model.X.execution.* and that model hasn't run, the expression is evaluated anyway
    • Mitigation: Consistent with CLI path (lines 236-249 in expression_evaluation_service.ts have identical behavior). The execution context is typically set by prior workflow steps.

Low

  1. Dynamic input key access not detected - Expression like ${{ inputs[someVariable] }} (non-literal key) wouldn't have its dependencies extracted due to regex-based parsing. Fails safely with CEL error if variable undefined.

  2. Test model schema declares both inputs required (workflow_partial_inputs_test.ts:102-103) - The test model has required: ["instanceName", "cidrBlock"] but only provides instanceName. This works because:

    • Expression skipping prevents cidrBlock evaluation
    • Method execution strips unresolved fields before Zod validation
    • Proxy guards runtime access
      This is intentional per the design rationale, just noting the interaction is non-obvious.

Verification

The implementation correctly:

  • Extracts provided input keys from context.inputs (lines 655-657)
  • Uses extractInputReferencesFromCel() which handles both dot (inputs.field) and bracket (inputs["field"]) notation
  • Skips expressions with missing inputs before CEL evaluation
  • Checks model resource/file availability in context
  • Matches the CLI path logic exactly (compare lines 207-258 in expression_evaluation_service.ts)

The Proxy in method_execution_service.ts (lines 154-167) provides the safety net: if skipped expressions are accessed at runtime, a clear error is thrown.

Verdict

PASS - The implementation is correct and matches the existing CLI path behavior. No critical or high severity issues. The medium/low findings are pre-existing patterns, not regressions introduced by this PR.

@stack72 stack72 merged commit 517c014 into main Mar 8, 2026
6 checks passed
@stack72 stack72 deleted the fix/skip-missing-inputs-workflow-653 branch March 8, 2026 21:32
stack72 added a commit that referenced this pull request Mar 22, 2026
PR #655 introduced a skip-guard in evaluateDefinitionExpressions() and
evaluateDefinition() that used extractInputReferencesFromCel() — a regex
scan — to detect all inputs.* references and skip evaluation if any were
absent. This was intended to fix issue #653 (factory model delete steps
failing with 'No such key: cidrBlock' when create-time inputs are omitted).

The guard is too aggressive: for a ternary such as
  inputs.transport == "lan" ? inputs.lan_host : inputs.tailnet_host
the regex reports all three inputs as required. When lan_host is absent
(because transport is "wan"), the entire expression is skipped — leaving
globalArguments.host as a raw ${{ }} string. The shell model then tries
to execute that literal string, producing exit code 1 and a failed
workflow step.

CEL natively short-circuits ternaries, so missing branch inputs are never
accessed at runtime. The correct fix is to attempt evaluation and catch
any CEL error (which only fires for inputs referenced directly, not inside
a non-taken branch). This preserves the #653 behaviour — a directly-
missing input still throws at eval time and is caught, leaving the
expression unresolved — while allowing ternary and other conditional
expressions to evaluate correctly.

Changes:
- execution_service.ts: remove providedInputKeys / extractInputReferencesFromCel
  skip block; wrap celEvaluator.evaluate() in try-catch; rename hasMissing
  → hasMissingModelDep for the retained model-resource dependency check
- expression_evaluation_service.ts: identical change for the CLI path
- cel_evaluator_test.ts: two new tests proving CEL ternary short-circuit
  and direct-missing-input throw behaviour
- expression_parser_test.ts: documents why extractInputReferencesFromCel
  must not be used as a skip gate (reports all ternary branch inputs)
- expression_evaluation_service_test.ts: two model-agnostic unit tests for
  evaluateDefinition() with globalArguments ternary (primary regression proof)
- workflow_partial_inputs_test.ts: end-to-end ternary test through the
  shell model execution path

Fixes #814. Preserves #653 fix.

Co-authored-by: Adam Stacoviak <[email protected]>
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
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.

GlobalArguments CEL expressions fail when factory inputs are omitted — selective evaluation not working

1 participant