From 8c74ec1c0572badbaba23bdd266b254af7ad422e Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Mon, 12 Dec 2022 13:48:31 -0800 Subject: [PATCH 01/11] Add job context provider --- .../src/context-providers/default.ts | 4 ++ .../src/context-providers/job.ts | 64 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 actions-languageservice/src/context-providers/job.ts diff --git a/actions-languageservice/src/context-providers/default.ts b/actions-languageservice/src/context-providers/default.ts index ba83a74..00c16fd 100644 --- a/actions-languageservice/src/context-providers/default.ts +++ b/actions-languageservice/src/context-providers/default.ts @@ -3,6 +3,7 @@ import {Kind} from "@github/actions-expressions/data/expressiondata"; import {WorkflowContext} from "../context/workflow-context"; import {ContextProviderConfig} from "./config"; import {getInputsContext} from "./inputs"; +import {getJobContext} from "./job"; import {getMatrixContext} from "./matrix"; import {getNeedsContext} from "./needs"; import {getStepsContext} from "./steps"; @@ -70,6 +71,9 @@ function getDefaultContext(name: string, workflowContext: WorkflowContext, mode: case "matrix": return getMatrixContext(workflowContext, mode); + + case "job": + return getJobContext(workflowContext); } return undefined; diff --git a/actions-languageservice/src/context-providers/job.ts b/actions-languageservice/src/context-providers/job.ts new file mode 100644 index 0000000..1ecb46f --- /dev/null +++ b/actions-languageservice/src/context-providers/job.ts @@ -0,0 +1,64 @@ +import {data} from "@github/actions-expressions"; +import { Dictionary } from "@github/actions-expressions/data/dictionary"; +import {isMapping, isScalar, isString} from "@github/actions-workflow-parser"; +import {WorkflowContext} from "../context/workflow-context"; +import {scalarToData} from "../utils/scalar-to-data"; + +export function getJobContext(workflowContext: WorkflowContext): data.Dictionary { + // https://docs.github.com/en/actions/learn-github-actions/contexts#job-context + const keys = ["container", "services", "status"]; + const containerKeys = ["id", "network"]; + const serviceKeys = containerKeys.concat("ports"); + + const job = workflowContext.job; + if (!job) { + return new data.Dictionary(); + } + + const jobContext = new data.Dictionary(); + for (const pair of job) { + if (!isString(pair.key)) { + continue; + } + if (!keys.includes(pair.key.value)) { + continue; + } + + const value = isScalar(pair.value) ? scalarToData(pair.value) : new data.Null(); + jobContext.add(pair.key.value, value); + } + + for (const key of keys) { + if (!jobContext.get(key)) { + switch (key) { + case "container": + var containerDictionary = new data.Dictionary(); + for (const containerKey of containerKeys) { + if (job.container[containerKey]) { + containerDictionary.add(containerKey, job.container[containerKey]); + } + } + jobContext.add(key, containerDictionary); + break; + case "services": + var services = new data.Dictionary(); + for (const service of job.services) { + var serviceDictionary = new data.Dictionary(); + for (const serviceKey of serviceKeys) { + if (service[serviceKey]) { + serviceDictionary.add(serviceKey, service[serviceKey]); + } + } + services.add(service, serviceDictionary); + } + jobContext.add(key, services); + break; + case "status": + jobContext.add(key, job.status); + break; + } + } + } + + return jobContext; +} From d48adfa651eb312dc882b1446bfc578e745d5036 Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Mon, 12 Dec 2022 15:10:28 -0800 Subject: [PATCH 02/11] Add tests and use null context --- .../src/context-providers/job.ts | 53 +++++++--------- .../src/validate.expressions.test.ts | 61 +++++++++++++++++++ 2 files changed, 83 insertions(+), 31 deletions(-) diff --git a/actions-languageservice/src/context-providers/job.ts b/actions-languageservice/src/context-providers/job.ts index 1ecb46f..3353be8 100644 --- a/actions-languageservice/src/context-providers/job.ts +++ b/actions-languageservice/src/context-providers/job.ts @@ -16,45 +16,36 @@ export function getJobContext(workflowContext: WorkflowContext): data.Dictionary } const jobContext = new data.Dictionary(); - for (const pair of job) { - if (!isString(pair.key)) { - continue; - } - if (!keys.includes(pair.key.value)) { - continue; - } - - const value = isScalar(pair.value) ? scalarToData(pair.value) : new data.Null(); - jobContext.add(pair.key.value, value); - } - for (const key of keys) { if (!jobContext.get(key)) { switch (key) { case "container": - var containerDictionary = new data.Dictionary(); - for (const containerKey of containerKeys) { - if (job.container[containerKey]) { - containerDictionary.add(containerKey, job.container[containerKey]); - } - } - jobContext.add(key, containerDictionary); + // var containerDictionary = new data.Dictionary(); + // for (const containerKey of containerKeys) { + // if (job.container[containerKey]) { + // containerDictionary.add(containerKey, job.container[containerKey]); + // } + // } + // jobContext.add(key, containerDictionary); + jobContext.add(key, new data.Null()); break; case "services": - var services = new data.Dictionary(); - for (const service of job.services) { - var serviceDictionary = new data.Dictionary(); - for (const serviceKey of serviceKeys) { - if (service[serviceKey]) { - serviceDictionary.add(serviceKey, service[serviceKey]); - } - } - services.add(service, serviceDictionary); - } - jobContext.add(key, services); + // var services = new data.Dictionary(); + // for (const service of job.services) { + // var serviceDictionary = new data.Dictionary(); + // for (const serviceKey of serviceKeys) { + // if (service[serviceKey]) { + // serviceDictionary.add(serviceKey, service[serviceKey]); + // } + // } + // services.add(service, serviceDictionary); + // } + // jobContext.add(key, services); + jobContext.add(key, new data.Null()); break; case "status": - jobContext.add(key, job.status); + // jobContext.add(key, job.status); + jobContext.add(key, new data.Null()); break; } } diff --git a/actions-languageservice/src/validate.expressions.test.ts b/actions-languageservice/src/validate.expressions.test.ts index b914ec9..58b6597 100644 --- a/actions-languageservice/src/validate.expressions.test.ts +++ b/actions-languageservice/src/validate.expressions.test.ts @@ -224,6 +224,67 @@ jobs: }); }); + describe("job context", () => { + it("job.status", async () => { + const input = ` +on: push + +jobs: + test: + runs-on: ubuntu-latest + steps: + - run: echo \${{ job.status }} +`; + const result = await validate(createDocument("wf.yaml", input)); + + expect(result).toEqual([]); + }); + + it("job.container", async () => { + const input = ` +on: push + +jobs: + test: + runs-on: ubuntu-latest + container: + image: node:14.16 + env: + NODE_ENV: development + ports: + - 80 + volumes: + - my_docker_volume:/volume_mount + options: --cpus 1 + steps: + - run: echo \${{ job.container }} +`; + const result = await validate(createDocument("wf.yaml", input)); + + expect(result).toEqual([]); + }); + + it("job.services.", async () => { + const input = ` +on: push + +jobs: + test: + runs-on: ubuntu-latest + services: + nginx: + image: nginx + ports: + - 8080:80 + steps: + - run: echo \${{ job.services.nginx }} +`; + const result = await validate(createDocument("wf.yaml", input)); + + expect(result).toEqual([]); + }); + }); + describe("strategy context", () => { it("reference within a matrix job", async () => { const input = ` From 873bdcc57511a6d54c824880efc065e41b2f1d63 Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Wed, 14 Dec 2022 16:05:58 -0800 Subject: [PATCH 03/11] Uncomment context traversal --- .../src/context-providers/job.ts | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/actions-languageservice/src/context-providers/job.ts b/actions-languageservice/src/context-providers/job.ts index 3353be8..4a246c2 100644 --- a/actions-languageservice/src/context-providers/job.ts +++ b/actions-languageservice/src/context-providers/job.ts @@ -20,32 +20,32 @@ export function getJobContext(workflowContext: WorkflowContext): data.Dictionary if (!jobContext.get(key)) { switch (key) { case "container": - // var containerDictionary = new data.Dictionary(); - // for (const containerKey of containerKeys) { - // if (job.container[containerKey]) { - // containerDictionary.add(containerKey, job.container[containerKey]); - // } - // } - // jobContext.add(key, containerDictionary); - jobContext.add(key, new data.Null()); + var containerDictionary = new data.Dictionary(); + for (const containerKey of containerKeys) { + if (job.container[containerKey]) { + containerDictionary.add(containerKey, job.container[containerKey]); + } + } + jobContext.add(key, containerDictionary); + // jobContext.add(key, new data.Null()); break; case "services": - // var services = new data.Dictionary(); - // for (const service of job.services) { - // var serviceDictionary = new data.Dictionary(); - // for (const serviceKey of serviceKeys) { - // if (service[serviceKey]) { - // serviceDictionary.add(serviceKey, service[serviceKey]); - // } - // } - // services.add(service, serviceDictionary); - // } - // jobContext.add(key, services); - jobContext.add(key, new data.Null()); + var services = new data.Dictionary(); + for (const service of job.services) { + var serviceDictionary = new data.Dictionary(); + for (const serviceKey of serviceKeys) { + if (service[serviceKey]) { + serviceDictionary.add(serviceKey, service[serviceKey]); + } + } + services.add(service, serviceDictionary); + } + jobContext.add(key, services); + // jobContext.add(key, new data.Null()); break; case "status": - // jobContext.add(key, job.status); - jobContext.add(key, new data.Null()); + jobContext.add(key, job.status); + // jobContext.add(key, new data.Null()); break; } } From 83deacf81c7df833f2c846564b696c77f4ed4ea5 Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Thu, 15 Dec 2022 14:31:38 -0800 Subject: [PATCH 04/11] This is wrong :( --- .../src/context-providers/job.ts | 110 +++++++++++------- .../src/validate.expressions.test.ts | 37 +++--- 2 files changed, 90 insertions(+), 57 deletions(-) diff --git a/actions-languageservice/src/context-providers/job.ts b/actions-languageservice/src/context-providers/job.ts index 4a246c2..db7c13c 100644 --- a/actions-languageservice/src/context-providers/job.ts +++ b/actions-languageservice/src/context-providers/job.ts @@ -1,55 +1,79 @@ import {data} from "@github/actions-expressions"; -import { Dictionary } from "@github/actions-expressions/data/dictionary"; -import {isMapping, isScalar, isString} from "@github/actions-workflow-parser"; +import {isMapping, isSequence, isString} from "@github/actions-workflow-parser"; +import { MappingToken } from "@github/actions-workflow-parser/templates/tokens/mapping-token"; import {WorkflowContext} from "../context/workflow-context"; -import {scalarToData} from "../utils/scalar-to-data"; export function getJobContext(workflowContext: WorkflowContext): data.Dictionary { // https://docs.github.com/en/actions/learn-github-actions/contexts#job-context const keys = ["container", "services", "status"]; - const containerKeys = ["id", "network"]; - const serviceKeys = containerKeys.concat("ports"); - - const job = workflowContext.job; - if (!job) { - return new data.Dictionary(); - } const jobContext = new data.Dictionary(); - for (const key of keys) { - if (!jobContext.get(key)) { - switch (key) { - case "container": - var containerDictionary = new data.Dictionary(); - for (const containerKey of containerKeys) { - if (job.container[containerKey]) { - containerDictionary.add(containerKey, job.container[containerKey]); - } - } - jobContext.add(key, containerDictionary); - // jobContext.add(key, new data.Null()); - break; - case "services": - var services = new data.Dictionary(); - for (const service of job.services) { - var serviceDictionary = new data.Dictionary(); - for (const serviceKey of serviceKeys) { - if (service[serviceKey]) { - serviceDictionary.add(serviceKey, service[serviceKey]); - } - } - services.add(service, serviceDictionary); - } - jobContext.add(key, services); - // jobContext.add(key, new data.Null()); - break; - case "status": - jobContext.add(key, job.status); - // jobContext.add(key, new data.Null()); - break; - } - } + const job = workflowContext.job; + if (!job) { + return new data.Dictionary( + ...keys.map(key => { + return {key, value: new data.Null()}; + }) + ); } + // Container + const jobContainer = job.container; + if (jobContainer && isMapping(jobContainer)) { + let containerContext = createContainerContext(jobContainer); + jobContext.add("container", containerContext); + } + else { + jobContext.add("container", new data.Null()); + } + + // Services + const jobServices = job.services; + if (jobServices && isMapping(jobServices)) { + const servicesContext = new data.Dictionary(); + for (const service of jobServices) { + if (!isMapping(service.value)) { + continue + } + let serviceContext = createContainerContext(service.value); + servicesContext.add(service.key.toString(), serviceContext); + } + jobContext.add("services", servicesContext); + } + else { + jobContext.add("services", new data.Null()); + } + + // Status + jobContext.add("status", new data.Null()); + return jobContext; } + +function createContainerContext(container: MappingToken): data.Dictionary { + const containerContext = new data.Dictionary(); + for (const token of container) { + if (isString(token.value)) { + // image and options + containerContext.add(token.key.toString(), new data.StringData(token.value.toString())); + } + else if (isSequence(token.value)) { + // ports and volumes + const sequence = new data.Array(); + for (const item of token.value) { + sequence.add(new data.StringData(item.toString())); + } + containerContext.add(token.key.toString(), new data.Array(sequence)); + } + else if (isMapping(token.value)) { + // credentials and env + const dict = new data.Dictionary(); + for (const item of token.value) { + containerContext.add(item.key.toString(), new data.StringData(item.value.toString())); + } + containerContext.add(token.key.toString(), dict); + } + } + return containerContext; +} + diff --git a/actions-languageservice/src/validate.expressions.test.ts b/actions-languageservice/src/validate.expressions.test.ts index 58b6597..4d1766d 100644 --- a/actions-languageservice/src/validate.expressions.test.ts +++ b/actions-languageservice/src/validate.expressions.test.ts @@ -232,32 +232,27 @@ on: push jobs: test: runs-on: ubuntu-latest + container: + image: node:14.16 steps: - - run: echo \${{ job.status }} + - run: echo \${{ job.container }} + - run: echo \${{ job.container.id }} + - run: echo \${{ job.container.network }} `; const result = await validate(createDocument("wf.yaml", input)); expect(result).toEqual([]); }); - it("job.container", async () => { + it("job.status", async () => { const input = ` on: push jobs: test: runs-on: ubuntu-latest - container: - image: node:14.16 - env: - NODE_ENV: development - ports: - - 80 - volumes: - - my_docker_volume:/volume_mount - options: --cpus 1 steps: - - run: echo \${{ job.container }} + - run: echo \${{ job.status }} `; const result = await validate(createDocument("wf.yaml", input)); @@ -273,11 +268,25 @@ jobs: runs-on: ubuntu-latest services: nginx: - image: nginx + image: node:14.16 + env: + NODE_ENV: development ports: - - 8080:80 + - 80 + volumes: + - my_docker_volume:/volume_mount + options: --cpus 1 + credentials: + username: actor + password: password steps: - run: echo \${{ job.services.nginx }} + - run: echo \${{ job.services.nginx.image }} + - run: echo \${{ job.services.nginx.env }} + - run: echo \${{ job.services.nginx.ports }} + - run: echo \${{ job.services.nginx.volumes }} + - run: echo \${{ job.services.nginx.options }} + - run: echo \${{ job.services.nginx.credentials }} `; const result = await validate(createDocument("wf.yaml", input)); From 77674c74e16b790fc75afe9982dcfb209a018402 Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Thu, 15 Dec 2022 15:13:29 -0800 Subject: [PATCH 05/11] This is right but also not very useful --- .../src/context-providers/job.ts | 39 ++++++++----------- .../src/validate.expressions.test.ts | 15 +------ 2 files changed, 18 insertions(+), 36 deletions(-) diff --git a/actions-languageservice/src/context-providers/job.ts b/actions-languageservice/src/context-providers/job.ts index db7c13c..8ad8fda 100644 --- a/actions-languageservice/src/context-providers/job.ts +++ b/actions-languageservice/src/context-providers/job.ts @@ -5,27 +5,18 @@ import {WorkflowContext} from "../context/workflow-context"; export function getJobContext(workflowContext: WorkflowContext): data.Dictionary { // https://docs.github.com/en/actions/learn-github-actions/contexts#job-context - const keys = ["container", "services", "status"]; - const jobContext = new data.Dictionary(); const job = workflowContext.job; if (!job) { - return new data.Dictionary( - ...keys.map(key => { - return {key, value: new data.Null()}; - }) - ); + return jobContext } // Container const jobContainer = job.container; if (jobContainer && isMapping(jobContainer)) { - let containerContext = createContainerContext(jobContainer); + let containerContext = createContainerContext(jobContainer, false); jobContext.add("container", containerContext); } - else { - jobContext.add("container", new data.Null()); - } // Services const jobServices = job.services; @@ -35,14 +26,11 @@ export function getJobContext(workflowContext: WorkflowContext): data.Dictionary if (!isMapping(service.value)) { continue } - let serviceContext = createContainerContext(service.value); + let serviceContext = createContainerContext(service.value, true); servicesContext.add(service.key.toString(), serviceContext); } jobContext.add("services", servicesContext); } - else { - jobContext.add("services", new data.Null()); - } // Status jobContext.add("status", new data.Null()); @@ -50,30 +38,35 @@ export function getJobContext(workflowContext: WorkflowContext): data.Dictionary return jobContext; } -function createContainerContext(container: MappingToken): data.Dictionary { +function createContainerContext(container: MappingToken, isServices: boolean): data.Dictionary { const containerContext = new data.Dictionary(); for (const token of container) { if (isString(token.value)) { // image and options - containerContext.add(token.key.toString(), new data.StringData(token.value.toString())); + // containerContext.add(token.key.toString(), new data.StringData(token.value.toString())); } else if (isSequence(token.value)) { // ports and volumes + // service ports are the only thing that is part of the job context const sequence = new data.Array(); for (const item of token.value) { - sequence.add(new data.StringData(item.toString())); + if (item.toString() === "ports" && isServices) { + sequence.add(new data.StringData(item.toString())); + } } containerContext.add(token.key.toString(), new data.Array(sequence)); } else if (isMapping(token.value)) { // credentials and env - const dict = new data.Dictionary(); - for (const item of token.value) { - containerContext.add(item.key.toString(), new data.StringData(item.value.toString())); - } - containerContext.add(token.key.toString(), dict); + // const dict = new data.Dictionary(); + // for (const item of token.value) { + // containerContext.add(item.key.toString(), new data.StringData(item.value.toString())); + // } + // containerContext.add(token.key.toString(), dict); } } + containerContext.add("id", new data.Null()); + containerContext.add("network", new data.Null()); return containerContext; } diff --git a/actions-languageservice/src/validate.expressions.test.ts b/actions-languageservice/src/validate.expressions.test.ts index 4d1766d..a6e7499 100644 --- a/actions-languageservice/src/validate.expressions.test.ts +++ b/actions-languageservice/src/validate.expressions.test.ts @@ -269,24 +269,13 @@ jobs: services: nginx: image: node:14.16 - env: - NODE_ENV: development ports: - 80 - volumes: - - my_docker_volume:/volume_mount - options: --cpus 1 - credentials: - username: actor - password: password steps: - run: echo \${{ job.services.nginx }} - - run: echo \${{ job.services.nginx.image }} - - run: echo \${{ job.services.nginx.env }} + - run: echo \${{ job.services.nginx.id }} + - run: echo \${{ job.services.nginx.network }} - run: echo \${{ job.services.nginx.ports }} - - run: echo \${{ job.services.nginx.volumes }} - - run: echo \${{ job.services.nginx.options }} - - run: echo \${{ job.services.nginx.credentials }} `; const result = await validate(createDocument("wf.yaml", input)); From 3340bd853894563508f91d73b123c97f662fa0c6 Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Thu, 15 Dec 2022 15:38:16 -0800 Subject: [PATCH 06/11] Turn ports into arrays and restrict context to port mappings --- .../src/context-providers/job.ts | 14 ++++++++++---- .../src/validate.expressions.test.ts | 4 +++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/actions-languageservice/src/context-providers/job.ts b/actions-languageservice/src/context-providers/job.ts index 8ad8fda..1cc9ccd 100644 --- a/actions-languageservice/src/context-providers/job.ts +++ b/actions-languageservice/src/context-providers/job.ts @@ -48,13 +48,19 @@ function createContainerContext(container: MappingToken, isServices: boolean): d else if (isSequence(token.value)) { // ports and volumes // service ports are the only thing that is part of the job context - const sequence = new data.Array(); + if (token.key.toString() !== "ports") { + continue; + } + const ports = new data.Dictionary() for (const item of token.value) { - if (item.toString() === "ports" && isServices) { - sequence.add(new data.StringData(item.toString())); + // We can determine the context mapping fully only if the port is defined + // as a mapping (i.e. :), single ports are assigned randomly + const portParts = item.toString().split(":") + if (isServices && portParts.length === 2) { + ports.add(portParts[1], new data.StringData(portParts[0])); } } - containerContext.add(token.key.toString(), new data.Array(sequence)); + containerContext.add(token.key.toString(), ports); } else if (isMapping(token.value)) { // credentials and env diff --git a/actions-languageservice/src/validate.expressions.test.ts b/actions-languageservice/src/validate.expressions.test.ts index a6e7499..8e7f6fe 100644 --- a/actions-languageservice/src/validate.expressions.test.ts +++ b/actions-languageservice/src/validate.expressions.test.ts @@ -269,8 +269,10 @@ jobs: services: nginx: image: node:14.16 + volumes: + - my_docker_volume:/volume_mount ports: - - 80 + - 80:8080 steps: - run: echo \${{ job.services.nginx }} - run: echo \${{ job.services.nginx.id }} From f10599820f14f94ca715ebcdf514ab0ff9e09221 Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Thu, 15 Dec 2022 16:01:14 -0800 Subject: [PATCH 07/11] Remove extra matrix switch case --- actions-languageservice/src/context-providers/default.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/actions-languageservice/src/context-providers/default.ts b/actions-languageservice/src/context-providers/default.ts index a586cdf..e80cd45 100644 --- a/actions-languageservice/src/context-providers/default.ts +++ b/actions-languageservice/src/context-providers/default.ts @@ -76,9 +76,6 @@ function getDefaultContext(name: string, workflowContext: WorkflowContext, mode: case "strategy": return getStrategyContext(workflowContext); - case "matrix": - return getMatrixContext(workflowContext, mode); - case "job": return getJobContext(workflowContext); } From 9b8fd4034407ee991b5ee05f8720f886102f4626 Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Fri, 16 Dec 2022 10:58:09 -0800 Subject: [PATCH 08/11] Update tests --- .../src/complete.expressions.test.ts | 27 ++++++++++++++++ .../src/context-providers/job.ts | 8 +++-- .../src/validate.expressions.test.ts | 32 +++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/actions-languageservice/src/complete.expressions.test.ts b/actions-languageservice/src/complete.expressions.test.ts index 6b5e41b..55f2d60 100644 --- a/actions-languageservice/src/complete.expressions.test.ts +++ b/actions-languageservice/src/complete.expressions.test.ts @@ -674,6 +674,33 @@ jobs: }); }); + describe("job context", () => { + it("job context is suggested within a job", async () => { + const input = ` +on: push + +jobs: + test: + runs-on: ubuntu-latest + container: + image: node:14.16 + steps: + - run: echo \${{ job.| }} +`; + + const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig); + expect(result.map(x => x.label)).toContain("container"); + expect(result.map(x => x.label)).toContain("services"); + expect(result.map(x => x.label)).toContain("status"); + }); + + it("job context is suggested within a job", async () => { + }); + + it("job context is suggested within a job", async () => { + }); + }); + it("context completion items include kind and insert text", async () => { const input = ` on: push diff --git a/actions-languageservice/src/context-providers/job.ts b/actions-languageservice/src/context-providers/job.ts index 1cc9ccd..245e922 100644 --- a/actions-languageservice/src/context-providers/job.ts +++ b/actions-languageservice/src/context-providers/job.ts @@ -14,7 +14,7 @@ export function getJobContext(workflowContext: WorkflowContext): data.Dictionary // Container const jobContainer = job.container; if (jobContainer && isMapping(jobContainer)) { - let containerContext = createContainerContext(jobContainer, false); + const containerContext = createContainerContext(jobContainer, false); jobContext.add("container", containerContext); } @@ -26,7 +26,7 @@ export function getJobContext(workflowContext: WorkflowContext): data.Dictionary if (!isMapping(service.value)) { continue } - let serviceContext = createContainerContext(service.value, true); + const serviceContext = createContainerContext(service.value, true); servicesContext.add(service.key.toString(), serviceContext); } jobContext.add("services", servicesContext); @@ -59,6 +59,10 @@ function createContainerContext(container: MappingToken, isServices: boolean): d if (isServices && portParts.length === 2) { ports.add(portParts[1], new data.StringData(portParts[0])); } + else { + // If the port isn't a mapping, just use null + ports.add(portParts[0], new data.Null()); + } } containerContext.add(token.key.toString(), ports); } diff --git a/actions-languageservice/src/validate.expressions.test.ts b/actions-languageservice/src/validate.expressions.test.ts index 6cb674e..ace4d85 100644 --- a/actions-languageservice/src/validate.expressions.test.ts +++ b/actions-languageservice/src/validate.expressions.test.ts @@ -283,6 +283,38 @@ jobs: expect(result).toEqual([]); }); + + it("job.services.", async () => { + const input = ` +on: push + +jobs: + test: + runs-on: ubuntu-latest + container: + image: node:14.16 + steps: + - run: echo \${{ job.container.tupperware }} +`; + const result = await validate(createDocument("wf.yaml", input)); + + expect(result).toEqual([ + { + message: "Context access might be invalid: tupperware", + range: { + end: { + character: 49, + line: 9 + }, + start: { + character: 18, + line: 9 + } + }, + severity: DiagnosticSeverity.Warning + } + ]); + }); }); describe("strategy context", () => { From 365a8dfca21413cac4a622ddfb1ea058ffacd438 Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Fri, 16 Dec 2022 11:34:06 -0800 Subject: [PATCH 09/11] Add expression completion tests --- .../src/complete.expressions.test.ts | 63 +++++++++++++++++-- package-lock.json | 12 ++-- 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/actions-languageservice/src/complete.expressions.test.ts b/actions-languageservice/src/complete.expressions.test.ts index 55f2d60..d98c187 100644 --- a/actions-languageservice/src/complete.expressions.test.ts +++ b/actions-languageservice/src/complete.expressions.test.ts @@ -684,20 +684,73 @@ jobs: runs-on: ubuntu-latest container: image: node:14.16 + services: + nginx: + image: node:14.16 steps: - run: echo \${{ job.| }} `; const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig); - expect(result.map(x => x.label)).toContain("container"); - expect(result.map(x => x.label)).toContain("services"); - expect(result.map(x => x.label)).toContain("status"); + expect(result.map(x => x.label)).toEqual(["container", "services", "status"]); }); - it("job context is suggested within a job", async () => { + it("container context is suggested within a job container", async () => { + const input = ` +on: push + +jobs: + test: + runs-on: ubuntu-latest + container: + image: node:14.16 + steps: + - run: echo \${{ job.container.| }} +`; + + const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig); + expect(result.map(x => x.label)).toEqual(["id", "network"]); }); - it("job context is suggested within a job", async () => { + it("services are suggested within a job services list", async () => { + const input = ` +on: push + +jobs: + test: + runs-on: ubuntu-latest + services: + nginx: + image: node:14.16 + redis: + image: redis + steps: + - run: echo \${{ job.services.| }} +`; + + const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig); + expect(result.map(x => x.label)).toEqual(["nginx", "redis"]); + }); + + it("services context is suggested within a job service", async () => { + const input = ` +on: push + +jobs: + test: + runs-on: ubuntu-latest + services: + nginx: + image: node:14.16 + ports: + - 80:8080 + - 90 + steps: + - run: echo \${{ job.services.nginx.| }} +`; + + const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig); + expect(result.map(x => x.label)).toEqual(["id", "network", "ports"]); }); }); diff --git a/package-lock.json b/package-lock.json index fd48861..2fcc155 100644 --- a/package-lock.json +++ b/package-lock.json @@ -701,9 +701,9 @@ "link": true }, "node_modules/@github/actions-workflow-parser": { - "version": "0.0.35", - "resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.35/0e88d541486f7c8772c5640fc13fac630cfaeaa8", - "integrity": "sha512-OuNEuqUH4AOfWd2xb8VRl7KwJrtiZu1ic0b27lmadwvK8qgRhPHxvS+QICEc+JtFbbVWpAA2ymh8I1U35639mA==", + "version": "0.0.36", + "resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.36/f27b908936efa949a105d7192dd3787a15094d71", + "integrity": "sha512-ATResf0PCT/bftJgs2ROAo0gDMIKQ1X0HcB4GejuYTcyHxWfvC44EqvFZmMpurV4Camht20bqK3LSY4NoJ0aVQ==", "license": "MIT", "dependencies": { "@github/actions-expressions": "*", @@ -13539,9 +13539,9 @@ } }, "@github/actions-workflow-parser": { - "version": "0.0.35", - "resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.35/0e88d541486f7c8772c5640fc13fac630cfaeaa8", - "integrity": "sha512-OuNEuqUH4AOfWd2xb8VRl7KwJrtiZu1ic0b27lmadwvK8qgRhPHxvS+QICEc+JtFbbVWpAA2ymh8I1U35639mA==", + "version": "0.0.36", + "resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.36/f27b908936efa949a105d7192dd3787a15094d71", + "integrity": "sha512-ATResf0PCT/bftJgs2ROAo0gDMIKQ1X0HcB4GejuYTcyHxWfvC44EqvFZmMpurV4Camht20bqK3LSY4NoJ0aVQ==", "requires": { "@github/actions-expressions": "*", "yaml": "^2.0.0-8" From 65cdae08fc0f4b1c2b634698efcb086a7cd0023a Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Fri, 16 Dec 2022 11:50:03 -0800 Subject: [PATCH 10/11] Remove code for out-of-context fields --- .../src/context-providers/job.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/actions-languageservice/src/context-providers/job.ts b/actions-languageservice/src/context-providers/job.ts index 245e922..fcf42c2 100644 --- a/actions-languageservice/src/context-providers/job.ts +++ b/actions-languageservice/src/context-providers/job.ts @@ -41,12 +41,7 @@ export function getJobContext(workflowContext: WorkflowContext): data.Dictionary function createContainerContext(container: MappingToken, isServices: boolean): data.Dictionary { const containerContext = new data.Dictionary(); for (const token of container) { - if (isString(token.value)) { - // image and options - // containerContext.add(token.key.toString(), new data.StringData(token.value.toString())); - } - else if (isSequence(token.value)) { - // ports and volumes + if (isSequence(token.value)) { // service ports are the only thing that is part of the job context if (token.key.toString() !== "ports") { continue; @@ -66,14 +61,6 @@ function createContainerContext(container: MappingToken, isServices: boolean): d } containerContext.add(token.key.toString(), ports); } - else if (isMapping(token.value)) { - // credentials and env - // const dict = new data.Dictionary(); - // for (const item of token.value) { - // containerContext.add(item.key.toString(), new data.StringData(item.value.toString())); - // } - // containerContext.add(token.key.toString(), dict); - } } containerContext.add("id", new data.Null()); containerContext.add("network", new data.Null()); From b8545f2b99d7fe2c609ef52493b43cc94345c324 Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Fri, 16 Dec 2022 14:16:08 -0800 Subject: [PATCH 11/11] Address fedback --- .../src/complete.expressions.test.ts | 6 ++--- .../src/context-providers/job.ts | 26 +++++++++---------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/actions-languageservice/src/complete.expressions.test.ts b/actions-languageservice/src/complete.expressions.test.ts index d98c187..299ba69 100644 --- a/actions-languageservice/src/complete.expressions.test.ts +++ b/actions-languageservice/src/complete.expressions.test.ts @@ -707,7 +707,7 @@ jobs: steps: - run: echo \${{ job.container.| }} `; - + const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig); expect(result.map(x => x.label)).toEqual(["id", "network"]); }); @@ -727,7 +727,7 @@ jobs: steps: - run: echo \${{ job.services.| }} `; - + const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig); expect(result.map(x => x.label)).toEqual(["nginx", "redis"]); }); @@ -748,7 +748,7 @@ jobs: steps: - run: echo \${{ job.services.nginx.| }} `; - + const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig); expect(result.map(x => x.label)).toEqual(["id", "network", "ports"]); }); diff --git a/actions-languageservice/src/context-providers/job.ts b/actions-languageservice/src/context-providers/job.ts index fcf42c2..89d31c2 100644 --- a/actions-languageservice/src/context-providers/job.ts +++ b/actions-languageservice/src/context-providers/job.ts @@ -1,6 +1,6 @@ import {data} from "@github/actions-expressions"; -import {isMapping, isSequence, isString} from "@github/actions-workflow-parser"; -import { MappingToken } from "@github/actions-workflow-parser/templates/tokens/mapping-token"; +import {isMapping, isSequence} from "@github/actions-workflow-parser"; +import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token"; import {WorkflowContext} from "../context/workflow-context"; export function getJobContext(workflowContext: WorkflowContext): data.Dictionary { @@ -8,7 +8,7 @@ export function getJobContext(workflowContext: WorkflowContext): data.Dictionary const jobContext = new data.Dictionary(); const job = workflowContext.job; if (!job) { - return jobContext + return jobContext; } // Container @@ -24,7 +24,7 @@ export function getJobContext(workflowContext: WorkflowContext): data.Dictionary const servicesContext = new data.Dictionary(); for (const service of jobServices) { if (!isMapping(service.value)) { - continue + continue; } const serviceContext = createContainerContext(service.value, true); servicesContext.add(service.key.toString(), serviceContext); @@ -40,30 +40,28 @@ export function getJobContext(workflowContext: WorkflowContext): data.Dictionary function createContainerContext(container: MappingToken, isServices: boolean): data.Dictionary { const containerContext = new data.Dictionary(); - for (const token of container) { - if (isSequence(token.value)) { + for (const {key, value} of container) { + if (isSequence(value)) { // service ports are the only thing that is part of the job context - if (token.key.toString() !== "ports") { + if (key.toString() !== "ports") { continue; } - const ports = new data.Dictionary() - for (const item of token.value) { + const ports = new data.Dictionary(); + for (const item of value) { // We can determine the context mapping fully only if the port is defined // as a mapping (i.e. :), single ports are assigned randomly - const portParts = item.toString().split(":") + const portParts = item.toString().split(":"); if (isServices && portParts.length === 2) { ports.add(portParts[1], new data.StringData(portParts[0])); - } - else { + } else { // If the port isn't a mapping, just use null ports.add(portParts[0], new data.Null()); } } - containerContext.add(token.key.toString(), ports); + containerContext.add(key.toString(), ports); } } containerContext.add("id", new data.Null()); containerContext.add("network", new data.Null()); return containerContext; } -