Skip to content

Commit 00d83b8

Browse files
lokesh755Copilot
andcommitted
Add background step syntax to workflow parser and language service
Co-authored-by: Copilot <copilot@github.com>
1 parent 77ed325 commit 00d83b8

21 files changed

Lines changed: 677 additions & 42 deletions

File tree

expressions/src/features.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ describe("FeatureFlags", () => {
5757
"blockScalarChompingWarning",
5858
"allowCaseFunction",
5959
"allowCopilotRequestsPermission",
60-
"allowConcurrencyQueue"
60+
"allowConcurrencyQueue",
61+
"allowBackgroundSteps"
6162
]);
6263
});
6364
});

expressions/src/features.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ export interface ExperimentalFeatures {
4646
* @default false
4747
*/
4848
allowConcurrencyQueue?: boolean;
49+
50+
/**
51+
* Enable background workflow steps and related wait/cancel steps.
52+
* @default false
53+
*/
54+
allowBackgroundSteps?: boolean;
4955
}
5056

5157
/**
@@ -62,7 +68,8 @@ const allFeatureKeys: ExperimentalFeatureKey[] = [
6268
"blockScalarChompingWarning",
6369
"allowCaseFunction",
6470
"allowCopilotRequestsPermission",
65-
"allowConcurrencyQueue"
71+
"allowConcurrencyQueue",
72+
"allowBackgroundSteps"
6673
];
6774

6875
export class FeatureFlags {

languageserver/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ initializationOptions: {
128128
| `missingInputsQuickfix` | Code action to add missing required inputs for actions |
129129
| `blockScalarChompingWarning` | Warn when block scalars (`\|` or `>`) use implicit clip chomping, which adds a trailing newline that may be unintentional |
130130
| `allowConcurrencyQueue` | Enable the `concurrency.queue` workflow property |
131+
| `allowBackgroundSteps` | Enable background workflow steps and related wait/cancel steps |
131132

132133
Individual feature flags take precedence over `all`. For example, `{ all: true, missingInputsQuickfix: false }` enables all experimental features except `missingInputsQuickfix`.
133134

languageserver/src/connection.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@ export function initConnection(connection: Connection) {
168168
contextProviderConfig: repoContext && contextProviders(client, repoContext, cache),
169169
fileProvider: getFileProvider(client, cache, repoContext?.workspaceUri, async path => {
170170
return await connection.sendRequest(Requests.ReadFile, {path});
171-
})
171+
}),
172+
featureFlags
172173
});
173174
});
174175
});

languageservice/src/complete.test.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -305,27 +305,43 @@ jobs:
305305
- run: echo
306306
- |`;
307307
const result = await complete(...getPositionFromCursor(input));
308-
expect(result).toHaveLength(11);
309-
expect(result.map(x => x.label)).toEqual([
310-
"continue-on-error",
311-
"env",
312-
"id",
313-
"if",
314-
"name",
315-
"run",
316-
"shell",
317-
"timeout-minutes",
318-
"uses",
319-
"with",
320-
"working-directory"
321-
]);
308+
expect(result.map(x => x.label)).toEqual(
309+
expect.arrayContaining([
310+
"continue-on-error",
311+
"env",
312+
"id",
313+
"if",
314+
"name",
315+
"run",
316+
"shell",
317+
"timeout-minutes",
318+
"uses",
319+
"with",
320+
"working-directory"
321+
])
322+
);
323+
expect(result.map(x => x.label)).toEqual(expect.not.arrayContaining(["background", "cancel", "wait", "wait-all"]));
322324

323325
// Includes detail when available. Using continue-on-error as a sample here.
324326
expect(result.map(x => (x.documentation as MarkupContent)?.value)).toContain(
325327
"Prevents a job from failing when a step fails. Set to `true` to allow a job to pass when this step fails."
326328
);
327329
});
328330

331+
it("empty step includes background step keys when enabled", async () => {
332+
const input = `on: push
333+
jobs:
334+
build:
335+
runs-on: ubuntu-latest
336+
steps:
337+
- run: echo
338+
- |`;
339+
const result = await complete(...getPositionFromCursor(input), {
340+
featureFlags: new FeatureFlags({allowBackgroundSteps: true})
341+
});
342+
expect(result.map(x => x.label)).toEqual(expect.arrayContaining(["background", "cancel", "wait", "wait-all"]));
343+
});
344+
329345
it("loose mapping keys have no completion suggestions", async () => {
330346
const input = `
331347
on:

languageservice/src/complete.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ import {Value, ValueProviderConfig} from "./value-providers/config.js";
4242
import {defaultValueProviders} from "./value-providers/default.js";
4343
import {DefinitionValueMode, definitionValues, TokenStructure} from "./value-providers/definition.js";
4444

45+
const backgroundStepCompletionLabels = new Set(["background", "wait", "wait-all", "cancel"]);
46+
4547
export function getExpressionInput(input: string, pos: number): string {
4648
// Find start marker around the cursor position
4749
let startPos = input.lastIndexOf(OPEN_EXPRESSION, pos);
@@ -88,7 +90,7 @@ export async function complete(
8890
// Parse the document
8991
const parsedTemplate = isAction
9092
? getOrParseAction(file, textDocument.uri, true)
91-
: getOrParseWorkflow(file, textDocument.uri, true);
93+
: getOrParseWorkflow(file, textDocument.uri, true, config?.featureFlags);
9294
if (!parsedTemplate.value) {
9395
return [];
9496
}
@@ -172,6 +174,10 @@ export async function complete(
172174
values = values.filter(v => v.label !== "copilot-requests");
173175
}
174176

177+
if (!isAction && !config?.featureFlags?.isEnabled("allowBackgroundSteps")) {
178+
values = values.filter(v => !backgroundStepCompletionLabels.has(v.label));
179+
}
180+
175181
// Offer "(switch to list)" / "(switch to mapping)" when the schema allows alternative forms
176182
const escapeHatches = getEscapeHatchCompletions(token, keyToken, indentString, newPos, schema);
177183
values.push(...escapeHatches);

languageservice/src/context-providers/env.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export function getEnvContext(workflowContext: WorkflowContext): DescriptionDict
77
const d = new DescriptionDictionary();
88

99
//step env
10-
if (workflowContext.step?.env) {
10+
if (workflowContext.step && "env" in workflowContext.step && workflowContext.step.env) {
1111
envContext(workflowContext.step.env, d);
1212
}
1313

languageservice/src/context/workflow-context.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,10 @@ export function getWorkflowContext(
6767
break;
6868
}
6969
case "regular-step":
70-
case "run-step": {
70+
case "run-step":
71+
case "wait-step":
72+
case "wait-all-step":
73+
case "cancel-step": {
7174
if (isMapping(token)) {
7275
stepToken = token;
7376
}

languageservice/src/hover.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {FeatureFlags} from "@actions/expressions";
12
import {isString} from "@actions/workflow-parser";
23
import {DescriptionProvider, hover, HoverConfig} from "./hover.js";
34
import {getPositionFromCursor} from "./test-utils/cursor-position.js";
@@ -203,3 +204,79 @@ jobs:
203204
);
204205
});
205206
});
207+
208+
describe("hover for background step keywords", () => {
209+
const bgConfig: HoverConfig = {featureFlags: new FeatureFlags({allowBackgroundSteps: true})};
210+
211+
it("on background key", async () => {
212+
const input = `on: push
213+
jobs:
214+
build:
215+
runs-on: ubuntu-latest
216+
steps:
217+
- id: server
218+
run: echo hi
219+
ba|ckground: true`;
220+
const result = await hover(...getPositionFromCursor(input), bgConfig);
221+
expect(result).not.toBeNull();
222+
expect(result?.contents).toContain("runs this step in the background");
223+
});
224+
225+
it("on wait key", async () => {
226+
const input = `on: push
227+
jobs:
228+
build:
229+
runs-on: ubuntu-latest
230+
steps:
231+
- id: server
232+
run: echo hi
233+
background: true
234+
- wa|it: server`;
235+
const result = await hover(...getPositionFromCursor(input), bgConfig);
236+
expect(result).not.toBeNull();
237+
expect(result?.contents).toContain("background steps to wait for");
238+
});
239+
240+
it("on wait-all key", async () => {
241+
const input = `on: push
242+
jobs:
243+
build:
244+
runs-on: ubuntu-latest
245+
steps:
246+
- id: server
247+
run: echo hi
248+
background: true
249+
- wa|it-all:`;
250+
const result = await hover(...getPositionFromCursor(input), bgConfig);
251+
expect(result).not.toBeNull();
252+
expect(result?.contents).toContain("Wait for all prior background steps");
253+
});
254+
255+
it("on cancel key", async () => {
256+
const input = `on: push
257+
jobs:
258+
build:
259+
runs-on: ubuntu-latest
260+
steps:
261+
- id: server
262+
run: echo hi
263+
background: true
264+
- ca|ncel: server`;
265+
const result = await hover(...getPositionFromCursor(input), bgConfig);
266+
expect(result).not.toBeNull();
267+
expect(result?.contents).toContain("background step to cancel");
268+
});
269+
270+
it("no hover for background keywords when feature flag is off", async () => {
271+
const input = `on: push
272+
jobs:
273+
build:
274+
runs-on: ubuntu-latest
275+
steps:
276+
- id: server
277+
run: echo hi
278+
ba|ckground: true`;
279+
const result = await hover(...getPositionFromCursor(input));
280+
expect(result).toBeNull();
281+
});
282+
});

languageservice/src/hover.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {data, DescriptionDictionary, Parser} from "@actions/expressions";
1+
import {data, DescriptionDictionary, FeatureFlags, Parser} from "@actions/expressions";
22
import {FunctionDefinition, FunctionInfo} from "@actions/expressions/funcs/info";
33
import {Lexer} from "@actions/expressions/lexer";
44
import {parseAction} from "@actions/workflow-parser/actions/action-parser";
@@ -35,6 +35,7 @@ export type HoverConfig = {
3535
descriptionProvider?: DescriptionProvider;
3636
contextProviderConfig?: ContextProviderConfig;
3737
fileProvider?: FileProvider;
38+
featureFlags?: FeatureFlags;
3839
};
3940

4041
export type DescriptionProvider = {
@@ -58,7 +59,9 @@ export async function hover(document: TextDocument, position: Position, config?:
5859
const isAction = isActionDocument(document.uri);
5960

6061
// Parse document
61-
const parsedTemplate = isAction ? parseAction(file, nullTrace) : getOrParseWorkflow(file, document.uri);
62+
const parsedTemplate = isAction
63+
? parseAction(file, nullTrace)
64+
: getOrParseWorkflow(file, document.uri, false, config?.featureFlags);
6265
if (!parsedTemplate?.value) {
6366
return null;
6467
}

0 commit comments

Comments
 (0)