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
74 changes: 74 additions & 0 deletions workflow-parser/src/model/convert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -868,4 +868,78 @@ jobs:
expect(template.errors!.some(e => e.Message.includes("Nested 'parallel' blocks are not allowed"))).toBe(true);
});
});

describe("conditions on control-flow steps", () => {
const conditionError =
"'if' is not supported on 'wait', 'wait-all', and 'cancel' steps. These steps always run and cannot be conditional.";

it.each([
[
"wait",
` - id: bg\n run: echo hi\n background: true\n - if: \${{ always() }}\n wait: bg`
],
[
"wait-all",
` - id: bg\n run: echo hi\n background: true\n - if: \${{ always() }}\n wait-all:`
],
[
"cancel",
` - id: bg\n run: echo hi\n background: true\n - if: \${{ always() }}\n cancel: bg`
]
])("rejects 'if' on a %s step", async (_name, stepsYaml) => {
const context = new TemplateContext(new TemplateValidationErrors(), getWorkflowSchema(), nullTrace);
context.state.featureFlags = new FeatureFlags({allowBackgroundSteps: true});

const result = parseWorkflow(
{
name: "wf.yaml",
content: `on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
${stepsYaml}`
},
context
);

const template = await convertWorkflowTemplate(result.context, result.value!, undefined, {
errorPolicy: ErrorPolicy.TryConversion,
featureFlags: new FeatureFlags({allowBackgroundSteps: true})
});

expect(template.errors).toBeDefined();
expect(template.errors!.some(e => e.Message.includes(conditionError))).toBe(true);
});

it("rejects 'if' on a cancel step even when the if expression is invalid", async () => {
const context = new TemplateContext(new TemplateValidationErrors(), getWorkflowSchema(), nullTrace);
context.state.featureFlags = new FeatureFlags({allowBackgroundSteps: true});

const result = parseWorkflow(
{
name: "wf.yaml",
content: `on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- id: bg
run: echo hi
background: true
- if: \${{ not-a-real-function() }}
cancel: bg`
},
context
);

const template = await convertWorkflowTemplate(result.context, result.value!, undefined, {
errorPolicy: ErrorPolicy.TryConversion,
featureFlags: new FeatureFlags({allowBackgroundSteps: true})
});

expect(template.errors).toBeDefined();
expect(template.errors!.some(e => e.Message.includes(conditionError))).toBe(true);
});
});
});
15 changes: 15 additions & 0 deletions workflow-parser/src/model/converter/steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import {handleTemplateTokenErrors} from "./handle-errors.js";
import {IdBuilder} from "./id-builder.js";
import {FeatureFlags} from "@actions/expressions/features";

// wait, wait-all, and cancel steps always run and cannot be made conditional.
const CONTROL_FLOW_CONDITION_ERROR =
"'if' is not supported on 'wait', 'wait-all', and 'cancel' steps. These steps always run and cannot be conditional.";

export function convertSteps(context: TemplateContext, steps: TemplateToken): Step[] {
if (!isSequence(steps)) {
context.error(steps, "Invalid format for steps");
Expand Down Expand Up @@ -83,6 +87,7 @@ function convertStep(
let continueOnError: boolean | ScalarToken | undefined;
let env: MappingToken | undefined;
let ifCondition: BasicExpressionToken | undefined;
let ifKeyToken: StringToken | undefined;
for (const item of mapping) {
const key = item.key.assertString("steps item key");
switch (key.value) {
Expand Down Expand Up @@ -126,6 +131,7 @@ function convertStep(
env = item.value.assertMapping("step env");
break;
case "if":
ifKeyToken = key;
ifCondition = convertToIfCondition(context, item.value);
break;
case "continue-on-error":
Expand Down Expand Up @@ -162,6 +168,9 @@ function convertStep(
}

if (wait) {
if (ifKeyToken) {
context.error(ifKeyToken, CONTROL_FLOW_CONDITION_ERROR);
}
Comment thread
Copilot marked this conversation as resolved.
return {
id: id?.value || "",
name: name || createSyntheticStepName("Wait"),
Expand All @@ -171,6 +180,9 @@ function convertStep(
}

if (waitAll !== undefined) {
if (ifKeyToken) {
context.error(ifKeyToken, CONTROL_FLOW_CONDITION_ERROR);
}
Comment thread
Copilot marked this conversation as resolved.
return {
id: id?.value || "",
name: name || createSyntheticStepName("Wait for all"),
Expand All @@ -180,6 +192,9 @@ function convertStep(
}

if (cancel) {
if (ifKeyToken) {
context.error(ifKeyToken, CONTROL_FLOW_CONDITION_ERROR);
}
Comment thread
Copilot marked this conversation as resolved.
return {
id: id?.value || "",
name: name || createSyntheticStepName("Cancel"),
Expand Down
3 changes: 3 additions & 0 deletions workflow-parser/src/workflow-v1.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -2212,6 +2212,7 @@
"properties": {
"name": "step-name",
"id": "step-id",
"if": "step-if",
"continue-on-error": "step-continue-on-error",
"wait": {
"type": "step-wait-target",
Expand All @@ -2225,6 +2226,7 @@
"properties": {
"name": "step-name",
"id": "step-id",
"if": "step-if",
"continue-on-error": "step-continue-on-error",
"wait-all": {
"type": "step-wait-all-value",
Expand All @@ -2238,6 +2240,7 @@
"properties": {
"name": "step-name",
"id": "step-id",
"if": "step-if",
"continue-on-error": "step-continue-on-error",
"cancel": {
"type": "step-cancel-target",
Expand Down
Loading