Feat: $( ) conditions in a step's if, and a step's status under steps

Merged
BR
7d ago
+1200-59

Description of your changes

Merge after #271, which this branch sits on. Only the last three commits are this PR's.

A step's if is CUE today. With this PR, an if that is exactly one $( ) expression is CEL, evaluated with the same roots as the step's properties (#271), in a run that resolves expressions.

steps:
  - name: notify
    type: notification
    if: '$(steps.build.failed && context.name != "dev")'
  - name: cleanup
    type: cleanup
    if: status.build.failed            # CUE, as before
  • Nothing that works changes. Only a value that is entirely one $( ) is CEL. always, an empty if, any CUE expression, and a $( inside a CUE string are unchanged. A whole $(...) never worked as CUE ($ is an identifier, so $(x) is a call to something undefined), and in a run without expressions enabled a $( ) if still fails as it does today.
  • Roots: context, steps, each and the host's roots, as in properties. A condition must be a bool.
  • steps.<name> now offers a finished step's status: phase, message, reason, succeeded, failed, skipped, timeout, failedAfterRetries, terminate, as status.<name> does in a CUE if. A read waits while the step has still to finish; it works in properties too.
  • Ordering: a condition's reads of steps are implied dependsOn, as a property's are (#271), so if: $(steps.build.failed) waits for build without a written dependsOn.
  • Loops: a loop step's own if applies to the loop as a whole, decided once, so it cannot read each; a looping group's sub-steps' if is decided each pass, with that pass's each.
  • Waiting: a read still to be answered holds the step back, running, rather than skipping it.
  • Admission: the webhook checks a $( ) if as it does properties: roots, the steps and outputs it reads, the order it can wait in, cycles, each placement.
  • The executor's pre-check hook cannot reach the evaluator, so the runners (a step, a step group, a loop) pass the condition in PreCheckOptions.Condition; PreCheckResult.Wait carries the wait. PropertyEvaluator gains Condition.

I have:

  • Read and followed KubeVela's pull request process.
  • All commits are signed off (DCO).
  • Related Docs updated properly: to follow.
  • Run make reviewable to ensure this PR is ready for review: golangci-lint and staticcheck with Go 1.27, 0 issues.

How has this code been tested

  • Unit: condition detection; Condition (true, false, every root, a host root given the run, not a bool, waiting); each status field in each finished phase, waiting while running or retrying, a loop's status from outside and inside, a step reading its own status; the executor's decision (true, false, wait, error, CUE left to CUE, no condition offered); a group's and a loop's condition (skip, wait, CUE, no expressions; a loop's decided once); validation of conditions and their implied dependencies.
  • e2e (e2e/expressions_test.go) on k3d: a WorkflowRun where a true condition runs its step, a false one skips it, a looping group's sub-step is decided each pass, a CUE if beside them is unchanged, and the reads are recorded as dependencies. With the condition switched off in the executor, the spec fails.

Summary by cubic

Step properties, forEach.items, and a step's if can now be $( ) CEL expressions, resolved when the step runs with the same roots as its properties (context, steps, the host's roots, and each inside a loop pass).

  • steps.<name>.outputs.<output> reads an output the step declares; steps.<name> exposes a finished step's status (phase, succeeded, failed, skipped, timeout, and so on). A read waits while the step has still to finish; a failed step fails the read, and a skipped step is read as skipped — its status never changes, and its outputs read as absent.
  • Reads of steps are implied dependsOn, which—unlike a written one—does not skip the reader when the step read is skipped. The run status records what each step waits on and whether dependsOn or an expression implied it; restarting a step also restarts every step that reads it.
  • A whole $( ) if is CEL only when the run opts in via the EnableCelExpressions feature gate and the workflowrun.oam.dev/cel-expressions annotation; any other if, including $( ) inside CUE text, stays CUE. An escaped $( still counts as an expression.
  • A loop step's own if is decided once for the loop and cannot read each; a looping group's sub-step if is decided each pass, and a group whose condition errors runs none of its sub-steps.
  • Admission checks every expression without evaluating it and refuses a read that does not compile, names a step or output that does not exist, uses each outside a forEach body, or could never be answered (the reader itself, a later step in order, a cycle), plus reads below a status field. forEach.items must be one sole expression; text around it is refused.