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
33 changes: 19 additions & 14 deletions languageservice/src/complete.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,20 +305,25 @@ jobs:
- run: echo
- |`;
const result = await complete(...getPositionFromCursor(input));
expect(result).toHaveLength(11);
expect(result.map(x => x.label)).toEqual([
"continue-on-error",
"env",
"id",
"if",
"name",
"run",
"shell",
"timeout-minutes",
"uses",
"with",
"working-directory"
]);
expect(result.map(x => x.label)).toEqual(
expect.arrayContaining([
"background",
"cancel",
"continue-on-error",
"env",
"id",
"if",
"name",
"run",
"shell",
"timeout-minutes",
"uses",
"wait",
"wait-all",
"with",
"working-directory"
])
);

// Includes detail when available. Using continue-on-error as a sample here.
expect(result.map(x => (x.documentation as MarkupContent)?.value)).toContain(
Expand Down
2 changes: 1 addition & 1 deletion languageservice/src/context-providers/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export function getEnvContext(workflowContext: WorkflowContext): DescriptionDict
const d = new DescriptionDictionary();

//step env
if (workflowContext.step?.env) {
if (workflowContext.step && "env" in workflowContext.step && workflowContext.step.env) {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

not all steps has env now.

envContext(workflowContext.step.env, d);
}

Expand Down
5 changes: 4 additions & 1 deletion languageservice/src/context/workflow-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ export function getWorkflowContext(
break;
}
case "regular-step":
case "run-step": {
case "run-step":
case "wait-step":
case "wait-all-step":
case "cancel-step": {
if (isMapping(token)) {
stepToken = token;
}
Expand Down
43 changes: 43 additions & 0 deletions languageservice/src/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,49 @@ describe("validation", () => {
expect(result.length).toBe(0);
});

it("background step keywords are accepted", async () => {
const result = await validate(
createDocument(
"wf.yaml",
`on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- id: server
run: npm start
background: true
- wait: server
continue-on-error: true
- wait-all:
continue-on-error: false
- cancel: server`
)
);

expect(result.length).toBe(0);
});

it("wait-all false is rejected", async () => {
const result = await validate(
createDocument(
"wf.yaml",
`on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- wait-all: false`
)
);

expect(result).toContainEqual(
expect.objectContaining({
message: "The value of 'wait-all' must be true or omitted"
})
);
});

it("missing jobs key", async () => {
const result = await validate(createDocument("wf.yaml", "on: push"));

Expand Down
127 changes: 122 additions & 5 deletions workflow-parser/src/model/converter/steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {TemplateContext} from "../../templates/template-context.js";
import {
BasicExpressionToken,
MappingToken,
NullToken,
ScalarToken,
StringToken,
TemplateToken
Expand All @@ -20,10 +21,13 @@ export function convertSteps(context: TemplateContext, steps: TemplateToken): St
}

const idBuilder = new IdBuilder();
const knownStepIds = new Set<string>();

const result: Step[] = [];
for (const item of steps) {
const step = handleTemplateTokenErrors(steps, context, undefined, () => convertStep(context, idBuilder, item));
const step = handleTemplateTokenErrors(steps, context, undefined, () =>
convertStep(context, idBuilder, knownStepIds, item)
);
if (step) {
result.push(step);
}
Expand All @@ -37,6 +41,12 @@ export function convertSteps(context: TemplateContext, steps: TemplateToken): St
let id = "";
if (isActionStep(step)) {
id = createActionStepId(step);
} else if ("wait" in step) {
id = "wait";
} else if ("wait-all" in step) {
id = "wait-all";
} else if ("cancel" in step) {
id = "cancel";
}

if (!id) {
Expand All @@ -50,13 +60,22 @@ export function convertSteps(context: TemplateContext, steps: TemplateToken): St
return result;
}

function convertStep(context: TemplateContext, idBuilder: IdBuilder, step: TemplateToken): Step | undefined {
function convertStep(
context: TemplateContext,
idBuilder: IdBuilder,
knownStepIds: Set<string>,
step: TemplateToken
): Step | undefined {
const mapping = step.assertMapping("steps item");

let run: ScalarToken | undefined;
let id: StringToken | undefined;
let name: ScalarToken | undefined;
let uses: StringToken | undefined;
let background: boolean | undefined;
let wait: StringToken[] | undefined;
let waitAll: boolean | undefined;
let cancel: StringToken | undefined;
let continueOnError: boolean | ScalarToken | undefined;
let env: MappingToken | undefined;
let ifCondition: BasicExpressionToken | undefined;
Expand All @@ -69,6 +88,8 @@ function convertStep(context: TemplateContext, idBuilder: IdBuilder, step: Templ
const error = idBuilder.tryAddKnownId(id.value);
if (error) {
context.error(id, error);
} else {
knownStepIds.add(id.value);
}
}
break;
Expand All @@ -81,6 +102,19 @@ function convertStep(context: TemplateContext, idBuilder: IdBuilder, step: Templ
case "uses":
uses = item.value.assertString("steps item uses");
break;
case "background":
background = item.value.assertBoolean("steps item background").value;
break;
case "wait":
wait = convertWaitTargets(context, knownStepIds, item.value, id);
break;
case "wait-all":
waitAll = convertWaitAllValue(context, item.value);
break;
case "cancel":
cancel = item.value.assertString("steps item cancel");
validateTargetStepId(context, knownStepIds, cancel, id);
break;
case "env":
env = item.value.assertMapping("step env");
break;
Expand All @@ -103,7 +137,8 @@ function convertStep(context: TemplateContext, idBuilder: IdBuilder, step: Templ
if: ifCondition || new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined),
"continue-on-error": continueOnError,
env,
run
run,
background
};
}

Expand All @@ -114,10 +149,38 @@ function convertStep(context: TemplateContext, idBuilder: IdBuilder, step: Templ
if: ifCondition || new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined),
"continue-on-error": continueOnError,
env,
uses
uses,
background
};
}

if (wait) {
return {
id: id?.value || "",
name: name || createSyntheticStepName("Wait"),
"continue-on-error": continueOnError,
wait
};
}
context.error(step, "Expected uses or run to be defined");

if (waitAll !== undefined) {
return {
id: id?.value || "",
name: name || createSyntheticStepName("Wait for all"),
"continue-on-error": continueOnError,
"wait-all": waitAll
};
}

if (cancel) {
return {
id: id?.value || "",
name: name || createSyntheticStepName("Cancel"),
"continue-on-error": continueOnError,
cancel
};
}
context.error(step, "Expected one of uses, run, wait, wait-all, or cancel to be defined");
}

function createActionStepId(step: ActionStep): string {
Expand All @@ -144,3 +207,57 @@ function createActionStepId(step: ActionStep): string {

return "";
}

function createSyntheticStepName(value: string): ScalarToken {
return new StringToken(undefined, undefined, value, undefined, undefined, undefined);
}

function convertWaitTargets(
context: TemplateContext,
knownStepIds: Set<string>,
token: TemplateToken,
ownStepId?: StringToken
): StringToken[] {
if (token instanceof StringToken) {
validateTargetStepId(context, knownStepIds, token, ownStepId);
return [token];
}

const sequence = token.assertSequence("steps item wait");
const targets: StringToken[] = [];
for (let i = 0; i < sequence.count; i++) {
const target = sequence.get(i).assertString("steps item wait item");
validateTargetStepId(context, knownStepIds, target, ownStepId);
targets.push(target);
}

return targets;
}

function convertWaitAllValue(context: TemplateContext, token: TemplateToken): boolean {
if (token instanceof NullToken) {
return true;
}

const value = token.assertBoolean("steps item wait-all").value;
if (!value) {
context.error(token, "The value of 'wait-all' must be true or omitted");
}

return true;
}

function validateTargetStepId(
context: TemplateContext,
knownStepIds: Set<string>,
target: StringToken,
ownStepId?: StringToken
) {
if (target.value.startsWith("__")) {
context.error(target, `The identifier '${target.value}' is invalid. IDs starting with '__' are reserved.`);
} else if (ownStepId && target.value.toLowerCase() === ownStepId.value.toLowerCase()) {
context.error(target, `Step '${ownStepId.value}' cannot reference itself`);
} else if (!knownStepIds.has(target.value)) {
context.error(target, `Step references unknown step ID '${target.value}'`);
}
}
26 changes: 22 additions & 4 deletions workflow-parser/src/model/workflow-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,22 +84,40 @@ export type Credential = {
password: StringToken | undefined;
};

export type Step = ActionStep | RunStep;
export type Step = ActionStep | RunStep | WaitStep | WaitAllStep | CancelStep;

type BaseStep = {
id: string;
name?: ScalarToken;
if: BasicExpressionToken;
if?: BasicExpressionToken;
"continue-on-error"?: boolean | ScalarToken;
};

type ExecutionStep = BaseStep & {
if: BasicExpressionToken;
env?: MappingToken;
};

export type RunStep = BaseStep & {
export type RunStep = ExecutionStep & {
run: ScalarToken;
background?: boolean;
};

export type ActionStep = BaseStep & {
export type ActionStep = ExecutionStep & {
uses: StringToken;
background?: boolean;
};

export type WaitStep = BaseStep & {
wait: StringToken[];
};

export type WaitAllStep = BaseStep & {
"wait-all": boolean;
};

export type CancelStep = BaseStep & {
cancel: StringToken;
};

export type EventsConfig = {
Expand Down
Loading
Loading