Merge branch 'main' into thyeggman/job-output-steps-context
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@github/actions-languageserver",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.65",
|
||||
"description": "Language server for GitHub Actions",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -38,7 +38,7 @@
|
||||
"prettier-fix": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@github/actions-languageservice": "^0.1.61",
|
||||
"@github/actions-languageservice": "^0.1.65",
|
||||
"@github/actions-workflow-parser": "*",
|
||||
"@octokit/rest": "^19.0.5",
|
||||
"vscode-languageserver": "^8.0.2",
|
||||
@@ -54,6 +54,7 @@
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.0.3",
|
||||
"jest": "^29.0.3",
|
||||
"nock": "^13.2.9",
|
||||
"prettier": "^2.7.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^29.0.3",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import {data} from "@github/actions-expressions";
|
||||
import {ContextProviderConfig} from "@github/actions-languageservice/.";
|
||||
import {WorkflowContext} from "@github/actions-languageservice/context/workflow-context";
|
||||
import {Octokit} from "@octokit/rest";
|
||||
import {getSecrets} from "./context-providers/secrets";
|
||||
import {getStepsContext} from "./context-providers/steps";
|
||||
import {RepositoryContext} from "./initializationOptions";
|
||||
import {TTLCache} from "./utils/cache";
|
||||
|
||||
@@ -18,14 +20,22 @@ export function contextProviders(
|
||||
auth: sessionToken
|
||||
});
|
||||
|
||||
const getContext = async (name: string, defaultContext: data.Dictionary | undefined) => {
|
||||
const getContext = async (
|
||||
name: string,
|
||||
defaultContext: data.Dictionary | undefined,
|
||||
workflowContext: WorkflowContext
|
||||
) => {
|
||||
switch (name) {
|
||||
case "secrets":
|
||||
case "secrets": {
|
||||
const secrets = await getSecrets(octokit, cache, repo.owner, repo.name);
|
||||
|
||||
defaultContext = defaultContext || new data.Dictionary();
|
||||
secrets.forEach(secret => defaultContext!.add(secret.value, new data.StringData("***")));
|
||||
return defaultContext;
|
||||
}
|
||||
case "steps": {
|
||||
return await getStepsContext(octokit, cache, defaultContext, workflowContext);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import {ActionReference, ActionOutputs} from "@github/actions-languageservice/action";
|
||||
import {Octokit} from "@octokit/rest";
|
||||
import {fetchActionMetadata} from "../utils/action-metadata";
|
||||
import {TTLCache} from "../utils/cache";
|
||||
|
||||
export async function getActionOutputs(
|
||||
octokit: Octokit,
|
||||
cache: TTLCache,
|
||||
action: ActionReference
|
||||
): Promise<ActionOutputs | undefined> {
|
||||
return (await fetchActionMetadata(octokit, cache, action))?.outputs;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import {data} from "@github/actions-expressions";
|
||||
import {getStepsContext as getDefaultStepsContext} from "@github/actions-languageservice/context-providers/steps";
|
||||
import {Octokit} from "@octokit/rest";
|
||||
import nock from "nock";
|
||||
import {createWorkflowContext} from "../test-utils/workflow-context";
|
||||
import {TTLCache} from "../utils/cache";
|
||||
import {getStepsContext} from "./steps";
|
||||
|
||||
const workflow = `
|
||||
name: Caching Primes
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Cache Primes
|
||||
id: cache-primes
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: prime-numbers
|
||||
key: \${{ runner.os }}-primes
|
||||
|
||||
- name: Generate Prime Numbers
|
||||
if: steps.cache-primes.outputs.cache-hit != 'true'
|
||||
run: /generate-primes.sh -d prime-numbers
|
||||
|
||||
- name: Use Prime Numbers
|
||||
run: /primes.sh -d prime-numbers
|
||||
`;
|
||||
|
||||
// https://api.github.com/repos/actions/cache/contents/action.yml?ref=v3
|
||||
const actionMetadata = {
|
||||
name: "action.yml",
|
||||
path: "action.yml",
|
||||
sha: "3e158e3ee33f7e597ccf45c369a6e830dc8d43e3",
|
||||
size: 946,
|
||||
url: "https://api.github.com/repos/actions/cache/contents/action.yml?ref=v3",
|
||||
html_url: "https://github.com/actions/cache/blob/v3/action.yml",
|
||||
git_url: "https://api.github.com/repos/actions/cache/git/blobs/3e158e3ee33f7e597ccf45c369a6e830dc8d43e3",
|
||||
download_url: "https://raw.githubusercontent.com/actions/cache/v3/action.yml",
|
||||
type: "file",
|
||||
content:
|
||||
"bmFtZTogJ0NhY2hlJwpkZXNjcmlwdGlvbjogJ0NhY2hlIGFydGlmYWN0cyBs\naWtlIGRlcGVuZGVuY2llcyBhbmQgYnVpbGQgb3V0cHV0cyB0byBpbXByb3Zl\nIHdvcmtmbG93IGV4ZWN1dGlvbiB0aW1lJwphdXRob3I6ICdHaXRIdWInCmlu\ncHV0czoKICBwYXRoOgogICAgZGVzY3JpcHRpb246ICdBIGxpc3Qgb2YgZmls\nZXMsIGRpcmVjdG9yaWVzLCBhbmQgd2lsZGNhcmQgcGF0dGVybnMgdG8gY2Fj\naGUgYW5kIHJlc3RvcmUnCiAgICByZXF1aXJlZDogdHJ1ZQogIGtleToKICAg\nIGRlc2NyaXB0aW9uOiAnQW4gZXhwbGljaXQga2V5IGZvciByZXN0b3Jpbmcg\nYW5kIHNhdmluZyB0aGUgY2FjaGUnCiAgICByZXF1aXJlZDogdHJ1ZQogIHJl\nc3RvcmUta2V5czoKICAgIGRlc2NyaXB0aW9uOiAnQW4gb3JkZXJlZCBsaXN0\nIG9mIGtleXMgdG8gdXNlIGZvciByZXN0b3Jpbmcgc3RhbGUgY2FjaGUgaWYg\nbm8gY2FjaGUgaGl0IG9jY3VycmVkIGZvciBrZXkuIE5vdGUgYGNhY2hlLWhp\ndGAgcmV0dXJucyBmYWxzZSBpbiB0aGlzIGNhc2UuJwogICAgcmVxdWlyZWQ6\nIGZhbHNlCiAgdXBsb2FkLWNodW5rLXNpemU6CiAgICBkZXNjcmlwdGlvbjog\nJ1RoZSBjaHVuayBzaXplIHVzZWQgdG8gc3BsaXQgdXAgbGFyZ2UgZmlsZXMg\nZHVyaW5nIHVwbG9hZCwgaW4gYnl0ZXMnCiAgICByZXF1aXJlZDogZmFsc2UK\nb3V0cHV0czoKICBjYWNoZS1oaXQ6CiAgICBkZXNjcmlwdGlvbjogJ0EgYm9v\nbGVhbiB2YWx1ZSB0byBpbmRpY2F0ZSBhbiBleGFjdCBtYXRjaCB3YXMgZm91\nbmQgZm9yIHRoZSBwcmltYXJ5IGtleScKcnVuczoKICB1c2luZzogJ25vZGUx\nNicKICBtYWluOiAnZGlzdC9yZXN0b3JlL2luZGV4LmpzJwogIHBvc3Q6ICdk\naXN0L3NhdmUvaW5kZXguanMnCiAgcG9zdC1pZjogJ3N1Y2Nlc3MoKScKYnJh\nbmRpbmc6CiAgaWNvbjogJ2FyY2hpdmUnCiAgY29sb3I6ICdncmF5LWRhcmsn\nCg==\n",
|
||||
encoding: "base64",
|
||||
_links: {
|
||||
self: "https://api.github.com/repos/actions/cache/contents/action.yml?ref=v3",
|
||||
git: "https://api.github.com/repos/actions/cache/git/blobs/3e158e3ee33f7e597ccf45c369a6e830dc8d43e3",
|
||||
html: "https://github.com/actions/cache/blob/v3/action.yml"
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
nock.disableNetConnect();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
it("returns default context when job is undefined", async () => {
|
||||
const workflowContext = createWorkflowContext(workflow, undefined);
|
||||
const defaultContext = getDefaultStepsContext(workflowContext);
|
||||
|
||||
const stepsContext = await getStepsContext(new Octokit(), new TTLCache(), defaultContext, workflowContext);
|
||||
expect(stepsContext).toEqual(defaultContext);
|
||||
});
|
||||
|
||||
it("adds action outputs", async () => {
|
||||
nock("https://api.github.com").get("/repos/actions/cache/contents/action.yml?ref=v3").reply(200, actionMetadata);
|
||||
|
||||
const workflowContext = createWorkflowContext(workflow, "build");
|
||||
const defaultContext = getDefaultStepsContext(workflowContext);
|
||||
|
||||
const stepsContext = await getStepsContext(new Octokit(), new TTLCache(), defaultContext, workflowContext);
|
||||
expect(stepsContext).toBeDefined();
|
||||
|
||||
expect(stepsContext).toEqual(
|
||||
new data.Dictionary({
|
||||
key: "cache-primes",
|
||||
value: new data.Dictionary(
|
||||
{
|
||||
key: "outputs",
|
||||
value: new data.Dictionary({
|
||||
key: "cache-hit",
|
||||
value: new data.StringData("A boolean value to indicate an exact match was found for the primary key")
|
||||
})
|
||||
},
|
||||
{key: "conclusion", value: new data.Null()},
|
||||
{key: "outcome", value: new data.Null()}
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import {data} from "@github/actions-expressions";
|
||||
import {isDictionary} from "@github/actions-expressions/data/dictionary";
|
||||
import {parseActionReference} from "@github/actions-languageservice/action";
|
||||
import {WorkflowContext} from "@github/actions-languageservice/context/workflow-context";
|
||||
import {isActionStep} from "@github/actions-workflow-parser/model/type-guards";
|
||||
import {Octokit} from "@octokit/rest";
|
||||
import {TTLCache} from "../utils/cache";
|
||||
import {getActionOutputs} from "./action-outputs";
|
||||
|
||||
export async function getStepsContext(
|
||||
octokit: Octokit,
|
||||
cache: TTLCache,
|
||||
defaultContext: data.Dictionary | undefined,
|
||||
workflowContext: WorkflowContext
|
||||
): Promise<data.Dictionary | undefined> {
|
||||
if (!defaultContext || !workflowContext.job) {
|
||||
return defaultContext;
|
||||
}
|
||||
|
||||
// The default context includes the set of valid
|
||||
// step ids that can be used in expressions
|
||||
const contextSteps = new Set<string>();
|
||||
for (const {key} of defaultContext.pairs()) {
|
||||
contextSteps.add(key);
|
||||
}
|
||||
|
||||
// Copy the default context for each step
|
||||
// If the step is an action, add the action outputs to the context
|
||||
const stepsContext = new data.Dictionary();
|
||||
for (const step of workflowContext.job.steps) {
|
||||
if (!contextSteps.has(step.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const defaultStepContext = defaultContext.get(step.id);
|
||||
if (!defaultStepContext) {
|
||||
stepsContext.add(step.id, new data.Null());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isActionStep(step) || !isDictionary(defaultStepContext)) {
|
||||
stepsContext.add(step.id, defaultStepContext);
|
||||
continue;
|
||||
}
|
||||
|
||||
const action = parseActionReference(step.uses.value);
|
||||
if (!action) {
|
||||
stepsContext.add(step.id, defaultStepContext);
|
||||
continue;
|
||||
}
|
||||
|
||||
const stepContext = new data.Dictionary();
|
||||
for (const {key, value} of defaultStepContext.pairs()) {
|
||||
switch (key) {
|
||||
case "outputs":
|
||||
const outputs = await getActionOutputs(octokit, cache, action);
|
||||
if (!outputs) {
|
||||
stepContext.add(key, value);
|
||||
continue;
|
||||
}
|
||||
const outputsDict = new data.Dictionary();
|
||||
for (const [key, value] of Object.entries(outputs)) {
|
||||
outputsDict.add(key, new data.StringData(value.description));
|
||||
}
|
||||
stepContext.add("outputs", outputsDict);
|
||||
break;
|
||||
default:
|
||||
stepContext.add(key, value);
|
||||
}
|
||||
}
|
||||
stepsContext.add(step.id, stepContext);
|
||||
}
|
||||
|
||||
return stepsContext;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import {WorkflowContext} from "@github/actions-languageservice/context/workflow-context";
|
||||
import {convertWorkflowTemplate, parseWorkflow, TraceWriter} from "@github/actions-workflow-parser";
|
||||
|
||||
const nullTrace: TraceWriter = {
|
||||
info: x => {},
|
||||
verbose: x => {},
|
||||
error: x => {}
|
||||
};
|
||||
|
||||
export function createWorkflowContext(workflow: string, job?: string, stepIndex?: number): WorkflowContext {
|
||||
const parsed = parseWorkflow("test.yaml", [{name: "test.yaml", content: workflow}], nullTrace);
|
||||
if (!parsed.value) {
|
||||
throw new Error("Failed to parse workflow");
|
||||
}
|
||||
const template = convertWorkflowTemplate(parsed.context, parsed.value);
|
||||
const context: WorkflowContext = {uri: "test.yaml", template};
|
||||
|
||||
if (job) {
|
||||
context.job = template.jobs.find(j => j.id.value === job);
|
||||
}
|
||||
|
||||
if (stepIndex) {
|
||||
context.step = context.job?.steps[stepIndex];
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import {ActionReference, ActionInputs, ActionOutputs, actionIdentifier} from "@github/actions-languageservice/action";
|
||||
import {Octokit, RestEndpointMethodTypes} from "@octokit/rest";
|
||||
import {parse} from "yaml";
|
||||
import {TTLCache} from "./cache";
|
||||
|
||||
export type ActionMetadata = {
|
||||
inputs?: ActionInputs;
|
||||
outputs?: ActionOutputs;
|
||||
};
|
||||
|
||||
export async function fetchActionMetadata(
|
||||
client: Octokit,
|
||||
cache: TTLCache,
|
||||
action: ActionReference
|
||||
): Promise<ActionMetadata | undefined> {
|
||||
const metadata = await cache.get(`${actionIdentifier(action)}/action-metadata`, undefined, () =>
|
||||
getActionMetadata(client, action)
|
||||
);
|
||||
if (!metadata) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return parseActionMetadata(metadata);
|
||||
}
|
||||
|
||||
async function getActionMetadata(client: Octokit, action: ActionReference): Promise<string | undefined> {
|
||||
let resp: RestEndpointMethodTypes["repos"]["getContent"]["response"];
|
||||
try {
|
||||
resp = await client.repos.getContent({
|
||||
owner: action.owner,
|
||||
repo: action.name,
|
||||
ref: action.ref,
|
||||
path: action.path ? `${action.path}/action.yml` : "action.yml"
|
||||
});
|
||||
} catch (e: any) {
|
||||
// If action.yml doesn't exist, try action.yaml
|
||||
if (e.status === 404) {
|
||||
resp = await client.repos.getContent({
|
||||
owner: action.owner,
|
||||
repo: action.name,
|
||||
ref: action.ref,
|
||||
path: action.path ? `${action.path}/action.yaml` : "action.yaml"
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28
|
||||
// Ignore directories (array of files) and non-file content
|
||||
if (resp.data === undefined || Array.isArray(resp.data) || resp.data.type !== "file") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (resp.data.content === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const text = Buffer.from(resp.data.content, "base64").toString("utf8");
|
||||
// Remove any null bytes
|
||||
return text.replace(/\0/g, "");
|
||||
}
|
||||
|
||||
// https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions
|
||||
async function parseActionMetadata(content: string): Promise<ActionMetadata> {
|
||||
const metadata: ActionMetadata = parse(content);
|
||||
return metadata;
|
||||
}
|
||||
@@ -1,14 +1,9 @@
|
||||
import {
|
||||
actionIdentifier,
|
||||
ActionInputs,
|
||||
ActionReference,
|
||||
parseActionReference
|
||||
} from "@github/actions-languageservice/action";
|
||||
import {ActionInputs, ActionReference, parseActionReference} from "@github/actions-languageservice/action";
|
||||
import {WorkflowContext} from "@github/actions-languageservice/context/workflow-context";
|
||||
import {Value} from "@github/actions-languageservice/value-providers/config";
|
||||
import {isActionStep} from "@github/actions-workflow-parser/model/type-guards";
|
||||
import {Octokit, RestEndpointMethodTypes} from "@octokit/rest";
|
||||
import {parse} from "yaml";
|
||||
import {Octokit} from "@octokit/rest";
|
||||
import {fetchActionMetadata} from "../utils/action-metadata";
|
||||
import {TTLCache} from "../utils/cache";
|
||||
|
||||
export async function getActionInputs(
|
||||
@@ -16,11 +11,7 @@ export async function getActionInputs(
|
||||
cache: TTLCache,
|
||||
action: ActionReference
|
||||
): Promise<ActionInputs | undefined> {
|
||||
const inputs = await cache.get(`${actionIdentifier(action)}/action-inputs`, undefined, () =>
|
||||
fetchActionInputs(client, action)
|
||||
);
|
||||
|
||||
return inputs;
|
||||
return (await fetchActionMetadata(client, cache, action))?.inputs;
|
||||
}
|
||||
|
||||
export async function getActionInputValues(
|
||||
@@ -49,60 +40,3 @@ export async function getActionInputValues(
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchActionInputs(client: Octokit, action: ActionReference): Promise<ActionInputs | undefined> {
|
||||
const metadata = await getActionMetadata(client, action);
|
||||
if (!metadata) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return parseActionMetadata(metadata);
|
||||
}
|
||||
|
||||
async function getActionMetadata(client: Octokit, action: ActionReference): Promise<string | undefined> {
|
||||
let resp: RestEndpointMethodTypes["repos"]["getContent"]["response"];
|
||||
try {
|
||||
resp = await client.repos.getContent({
|
||||
owner: action.owner,
|
||||
repo: action.name,
|
||||
ref: action.ref,
|
||||
path: action.path ? `${action.path}/action.yml` : "action.yml"
|
||||
});
|
||||
} catch (e: any) {
|
||||
// If action.yml doesn't exist, try action.yaml
|
||||
if (e.status === 404) {
|
||||
resp = await client.repos.getContent({
|
||||
owner: action.owner,
|
||||
repo: action.name,
|
||||
ref: action.ref,
|
||||
path: action.path ? `${action.path}/action.yaml` : "action.yaml"
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28
|
||||
// Ignore directories (array of files) and non-file content
|
||||
if (resp.data === undefined || Array.isArray(resp.data) || resp.data.type !== "file") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (resp.data.content === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const text = Buffer.from(resp.data.content, "base64").toString("utf8");
|
||||
// Remove any null bytes
|
||||
return text.replace(/\0/g, "");
|
||||
}
|
||||
|
||||
type ActionMetadata = {
|
||||
inputs?: ActionInputs;
|
||||
};
|
||||
|
||||
// https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions
|
||||
async function parseActionMetadata(content: string): Promise<ActionInputs> {
|
||||
const metadata: ActionMetadata = parse(content);
|
||||
return metadata.inputs ?? {};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"exclude": ["./src/**/*.test.ts"],
|
||||
"exclude": ["./src/**/*.test.ts", "./src/test-utils"],
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@github/actions-languageservice",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.65",
|
||||
"description": "Language service for GitHub Actions",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -8,6 +8,14 @@ export type ActionInput = {
|
||||
|
||||
export type ActionInputs = Record<string, ActionInput>;
|
||||
|
||||
// https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#outputs-for-docker-container-and-javascript-actions
|
||||
export type ActionOutput = {
|
||||
description: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
export type ActionOutputs = Record<string, ActionOutput>;
|
||||
|
||||
export type ActionReference = {
|
||||
owner: string;
|
||||
name: string;
|
||||
|
||||
@@ -311,6 +311,35 @@ jobs:
|
||||
expect(result.map(x => x.label)).toEqual(["build_id"]);
|
||||
});
|
||||
|
||||
it("env steps context only for current step and job", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
env:
|
||||
envwf: workflow_env
|
||||
jobs:
|
||||
a:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
envjoba: job_a_env
|
||||
b:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
envjobb: job_b_env
|
||||
steps:
|
||||
- name: step a
|
||||
run: echo "hello"
|
||||
env:
|
||||
envstepa: step_a_env
|
||||
- name: step b
|
||||
run: echo "hello \${{ env.|
|
||||
env:
|
||||
envstepb: step_b_env
|
||||
`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["envjobb", "envstepb", "envwf"]);
|
||||
});
|
||||
|
||||
it("inputs", async () => {
|
||||
const input = `
|
||||
on:
|
||||
@@ -409,7 +438,7 @@ jobs:
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, undefined);
|
||||
|
||||
expect(result.map(x => x.label)).not.toContain("inputs");
|
||||
expect(result.map(x => x.label)).not.toContain("cron");
|
||||
expect(result.map(x => x.label)).not.toContain("schedule");
|
||||
});
|
||||
|
||||
it("includes cron schedules", async () => {
|
||||
@@ -428,7 +457,7 @@ jobs:
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, undefined);
|
||||
|
||||
expect(result.map(x => x.label)).toContain("cron");
|
||||
expect(result.map(x => x.label)).toContain("schedule");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import {data} from "@github/actions-expressions";
|
||||
import {Dictionary} from "@github/actions-expressions/data/dictionary";
|
||||
import {ExpressionData, Pair} from "@github/actions-expressions/data/expressiondata";
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
|
||||
export type ContextProviderConfig = {
|
||||
getContext: (name: string, defaultContext: data.Dictionary | undefined) => Promise<data.Dictionary | undefined>;
|
||||
getContext: (
|
||||
name: string,
|
||||
defaultContext: data.Dictionary | undefined,
|
||||
workflowContext: WorkflowContext
|
||||
) => Promise<data.Dictionary | undefined>;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@ import {data} from "@github/actions-expressions";
|
||||
import {Kind} from "@github/actions-expressions/data/expressiondata";
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
import {ContextProviderConfig} from "./config";
|
||||
import {getEnvContext} from "./env";
|
||||
import {getGithubContext} from "./github";
|
||||
import {getInputsContext} from "./inputs";
|
||||
import {getJobContext} from "./job";
|
||||
@@ -36,7 +37,7 @@ export async function getContext(
|
||||
continue;
|
||||
}
|
||||
|
||||
value = (await config?.getContext(contextName, value)) || value;
|
||||
value = (await config?.getContext(contextName, value, workflowContext)) || value;
|
||||
|
||||
context.add(contextName, value);
|
||||
}
|
||||
@@ -78,6 +79,9 @@ function getDefaultContext(name: string, workflowContext: WorkflowContext, mode:
|
||||
|
||||
case "job":
|
||||
return getJobContext(workflowContext);
|
||||
|
||||
case "env":
|
||||
return getEnvContext(workflowContext);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import {data} from "@github/actions-expressions";
|
||||
import {isScalar, isString} from "@github/actions-workflow-parser";
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token";
|
||||
|
||||
export function getEnvContext(workflowContext: WorkflowContext): data.Dictionary {
|
||||
const d = new data.Dictionary();
|
||||
|
||||
//step env
|
||||
if (workflowContext.step?.env) {
|
||||
envContext(workflowContext.step.env, d);
|
||||
}
|
||||
|
||||
//job env
|
||||
if (workflowContext.job && workflowContext.job.env) {
|
||||
envContext(workflowContext.job.env, d);
|
||||
}
|
||||
|
||||
//workflow env
|
||||
if (workflowContext.template && workflowContext.template.env) {
|
||||
const wfEnv = workflowContext.template.env.assertMapping("workflow env");
|
||||
envContext(wfEnv, d);
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
function envContext(envMap: MappingToken, d: data.Dictionary) {
|
||||
for (const env of envMap) {
|
||||
if (!isString(env.key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = isScalar(env.value) ? new data.StringData(env.value.toDisplayString()) : new data.Null();
|
||||
d.add(env.key.value, value);
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ function getEventContext(workflowContext: WorkflowContext): ExpressionData {
|
||||
if (schedule && schedule.length > 0) {
|
||||
const default_cron = schedule[0].cron;
|
||||
// For now, default to the first cron expression only
|
||||
d.add("cron", new data.StringData(default_cron));
|
||||
d.add("schedule", new data.StringData(default_cron));
|
||||
}
|
||||
|
||||
return d;
|
||||
|
||||
@@ -139,7 +139,7 @@ describe("find-token", () => {
|
||||
testFindToken(`on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [ubuntu-latest, self|`)
|
||||
runs-on: [ubuntu-latest, self|]`)
|
||||
).toEqual({
|
||||
path: [
|
||||
["workflow-root-strict", TokenType.Mapping],
|
||||
|
||||
@@ -350,6 +350,82 @@ jobs:
|
||||
});
|
||||
});
|
||||
|
||||
describe("env context", () => {
|
||||
it("references env within scope", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
a:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: step a
|
||||
env:
|
||||
step_env: job_a_env
|
||||
run: echo "hello \${{ env.step_env }}
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("inherits parent env", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
env:
|
||||
envwf: workflow_env
|
||||
jobs:
|
||||
a:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
envjoba: job_a_env
|
||||
steps:
|
||||
- name: step a
|
||||
run: echo "hello \${{ env.envwf }}
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("references env outside of scope", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
env:
|
||||
envwf: workflow_env
|
||||
jobs:
|
||||
a:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
envjoba: job_a_env
|
||||
steps:
|
||||
- name: step a
|
||||
run: echo "hello"
|
||||
env:
|
||||
envstepa: step_a_env
|
||||
- name: step b
|
||||
run: echo "hello \${{ env.envstepa }}
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Context access might be invalid: envstepa",
|
||||
range: {
|
||||
end: {
|
||||
character: 42,
|
||||
line: 15
|
||||
},
|
||||
start: {
|
||||
character: 23,
|
||||
line: 15
|
||||
}
|
||||
},
|
||||
severity: DiagnosticSeverity.Warning
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("strategy context", () => {
|
||||
it("reference within a matrix job", async () => {
|
||||
const input = `
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "browser-playground",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.65",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@github/actions-languageserver": "^0.1.61",
|
||||
"@github/actions-languageserver": "^0.1.65",
|
||||
"monaco-editor-webpack-plugin": "^7.0.1",
|
||||
"monaco-editor-workers": "^0.34.2",
|
||||
"monaco-languageclient": "^4.0.3",
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.1.61"
|
||||
"version": "0.1.65"
|
||||
}
|
||||
|
||||
Generated
+51
-7
@@ -16,10 +16,10 @@
|
||||
},
|
||||
"actions-languageserver": {
|
||||
"name": "@github/actions-languageserver",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.65",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-languageservice": "^0.1.61",
|
||||
"@github/actions-languageservice": "^0.1.65",
|
||||
"@github/actions-workflow-parser": "*",
|
||||
"@octokit/rest": "^19.0.5",
|
||||
"vscode-languageserver": "^8.0.2",
|
||||
@@ -29,6 +29,7 @@
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.0.3",
|
||||
"jest": "^29.0.3",
|
||||
"nock": "^13.2.9",
|
||||
"prettier": "^2.7.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^29.0.3",
|
||||
@@ -40,7 +41,7 @@
|
||||
},
|
||||
"actions-languageservice": {
|
||||
"name": "@github/actions-languageservice",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.65",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-expressions": "*",
|
||||
@@ -62,10 +63,10 @@
|
||||
}
|
||||
},
|
||||
"browser-playground": {
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.65",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-languageserver": "^0.1.61",
|
||||
"@github/actions-languageserver": "^0.1.65",
|
||||
"monaco-editor-webpack-plugin": "^7.0.1",
|
||||
"monaco-editor-workers": "^0.34.2",
|
||||
"monaco-languageclient": "^4.0.3",
|
||||
@@ -8814,6 +8815,21 @@
|
||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
|
||||
},
|
||||
"node_modules/nock": {
|
||||
"version": "13.2.9",
|
||||
"resolved": "https://registry.npmjs.org/nock/-/nock-13.2.9.tgz",
|
||||
"integrity": "sha512-1+XfJNYF1cjGB+TKMWi29eZ0b82QOvQs2YoLNzbpWGqFMtRQHTa57osqdGj4FrFPgkO4D4AZinzUJR9VvW3QUA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.1.0",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"propagate": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
|
||||
@@ -10271,6 +10287,15 @@
|
||||
"read": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/propagate": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz",
|
||||
"integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/proto-list": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
||||
@@ -13508,11 +13533,12 @@
|
||||
"@github/actions-languageserver": {
|
||||
"version": "file:actions-languageserver",
|
||||
"requires": {
|
||||
"@github/actions-languageservice": "^0.1.61",
|
||||
"@github/actions-languageservice": "^0.1.65",
|
||||
"@github/actions-workflow-parser": "*",
|
||||
"@octokit/rest": "^19.0.5",
|
||||
"@types/jest": "^29.0.3",
|
||||
"jest": "^29.0.3",
|
||||
"nock": "^13.2.9",
|
||||
"prettier": "^2.7.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^29.0.3",
|
||||
@@ -16294,7 +16320,7 @@
|
||||
"browser-playground": {
|
||||
"version": "file:browser-playground",
|
||||
"requires": {
|
||||
"@github/actions-languageserver": "^0.1.61",
|
||||
"@github/actions-languageserver": "^0.1.65",
|
||||
"css-loader": "^6.7.2",
|
||||
"monaco-editor-webpack-plugin": "^7.0.1",
|
||||
"monaco-editor-workers": "^0.34.2",
|
||||
@@ -19949,6 +19975,18 @@
|
||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
|
||||
},
|
||||
"nock": {
|
||||
"version": "13.2.9",
|
||||
"resolved": "https://registry.npmjs.org/nock/-/nock-13.2.9.tgz",
|
||||
"integrity": "sha512-1+XfJNYF1cjGB+TKMWi29eZ0b82QOvQs2YoLNzbpWGqFMtRQHTa57osqdGj4FrFPgkO4D4AZinzUJR9VvW3QUA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "^4.1.0",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"propagate": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node-addon-api": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
|
||||
@@ -21020,6 +21058,12 @@
|
||||
"read": "1"
|
||||
}
|
||||
},
|
||||
"propagate": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz",
|
||||
"integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==",
|
||||
"dev": true
|
||||
},
|
||||
"proto-list": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
||||
|
||||
Reference in New Issue
Block a user