Merge branch 'main' into joshmgross/for-of
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
name: Update @github dependencies
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
registry-url: 'https://npm.pkg.github.com'
|
||||
scope: '@github'
|
||||
|
||||
- run: npm ci
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- run: |
|
||||
npm update @github/actions-workflow-parser
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- run: |
|
||||
git config user.name github-actions
|
||||
git config user.email github-actions@github.com
|
||||
|
||||
- run: |
|
||||
git add .
|
||||
git commit -m "Update @github/actions-workflow-parser"
|
||||
git push
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@github/actions-languageserver",
|
||||
"version": "0.1.41",
|
||||
"version": "0.1.47",
|
||||
"description": "Language server for GitHub Actions",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -38,7 +38,7 @@
|
||||
"prettier-fix": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@github/actions-languageservice": "^0.1.41",
|
||||
"@github/actions-languageservice": "^0.1.47",
|
||||
"@octokit/rest": "^19.0.5",
|
||||
"vscode-languageserver": "^8.0.2",
|
||||
"vscode-languageserver-textdocument": "^1.0.7"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@github/actions-languageservice",
|
||||
"version": "0.1.41",
|
||||
"version": "0.1.47",
|
||||
"description": "Language service for GitHub Actions",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -88,6 +88,64 @@ describe("expressions", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
describe("multi-line strings", () => {
|
||||
it("indented |", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
steps:
|
||||
- run: |
|
||||
first line
|
||||
test \${{ github.| }}
|
||||
test2`;
|
||||
const result = await complete(...getPositionFromCursor(input, 1), undefined, contextProviderConfig);
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
|
||||
it("indented |+", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
steps:
|
||||
- run: |+
|
||||
first line
|
||||
test \${{ github.| }}
|
||||
test2`;
|
||||
const result = await complete(...getPositionFromCursor(input, 1), undefined, contextProviderConfig);
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
|
||||
it("indented >", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
steps:
|
||||
- run: >
|
||||
first line
|
||||
test \${{ github.| }}
|
||||
test2`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
|
||||
it("indented >+", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
steps:
|
||||
- run: >+
|
||||
first line
|
||||
test \${{ github.| }}
|
||||
test2`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
});
|
||||
|
||||
it("nested auto-complete", async () => {
|
||||
const input = "run-name: ${{ github.| }}";
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
@@ -532,7 +590,7 @@ jobs:
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
|
||||
expect(result.map(x => x.label)).toEqual([]);
|
||||
expect(result.map(x => x.label)).toEqual(["animal", "fruit"]);
|
||||
});
|
||||
|
||||
it("matrix with expression in property", async () => {
|
||||
|
||||
@@ -9,9 +9,9 @@ import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/map
|
||||
import {TokenType} from "@github/actions-workflow-parser/templates/tokens/types";
|
||||
import {File} from "@github/actions-workflow-parser/workflows/file";
|
||||
import {Position, TextDocument} from "vscode-languageserver-textdocument";
|
||||
import {CompletionItem, TextEdit, Range} from "vscode-languageserver-types";
|
||||
import {CompletionItem, Range, TextEdit} from "vscode-languageserver-types";
|
||||
import {ContextProviderConfig} from "./context-providers/config";
|
||||
import {getContext} from "./context-providers/default";
|
||||
import {getContext, Mode} from "./context-providers/default";
|
||||
import {getWorkflowContext, WorkflowContext} from "./context/workflow-context";
|
||||
import {nullTrace} from "./nulltrace";
|
||||
import {getAllowedContext} from "./utils/allowed-context";
|
||||
@@ -77,15 +77,23 @@ export async function complete(
|
||||
token.definition?.definitionType === DefinitionType.String && (token.definition as StringDefinition).isExpression;
|
||||
const containsExpression = isString(token) && token.value.indexOf(OPEN_EXPRESSION) >= 0;
|
||||
if (isString(token) && (isExpression || containsExpression)) {
|
||||
const currentInput = token.value;
|
||||
const currentInput = token.source || token.value;
|
||||
|
||||
// Transform the overall position into a node relative position
|
||||
const relCharPos = newPos.character - token.range!.start[1];
|
||||
let relCharPos: number = 0;
|
||||
const lineDiff = newPos.line - token.range!.start[0];
|
||||
if (token.range!.start[0] !== token.range!.end[0]) {
|
||||
const lines = currentInput.split("\n");
|
||||
const linesBeforeCusor = lines.slice(0, lineDiff);
|
||||
relCharPos = linesBeforeCusor.join("\n").length + newPos.character;
|
||||
} else {
|
||||
relCharPos = newPos.character - token.range!.start[1];
|
||||
}
|
||||
|
||||
const expressionInput = (getExpressionInput(currentInput, relCharPos) || "").trim();
|
||||
|
||||
const allowedContext = getAllowedContext(token, parent!);
|
||||
const context = await getContext(allowedContext, contextProviderConfig, workflowContext);
|
||||
const allowedContext = getAllowedContext(token, parent);
|
||||
const context = await getContext(allowedContext, contextProviderConfig, workflowContext, Mode.Completion);
|
||||
|
||||
return completeExpression(expressionInput, context, []);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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 {getInputsContext} from "./inputs";
|
||||
@@ -7,16 +8,31 @@ import {getNeedsContext} from "./needs";
|
||||
import {getStepsContext} from "./steps";
|
||||
import {getStrategyContext} from "./strategy";
|
||||
|
||||
// ContextValue is the type of the value returned by a context provider
|
||||
// Null indicates that the context provider doesn't have any value to provide
|
||||
export type ContextValue = data.Dictionary | data.Null;
|
||||
|
||||
export enum Mode {
|
||||
Completion,
|
||||
Validation,
|
||||
Hover
|
||||
}
|
||||
|
||||
export async function getContext(
|
||||
names: string[],
|
||||
config: ContextProviderConfig | undefined,
|
||||
workflowContext: WorkflowContext
|
||||
workflowContext: WorkflowContext,
|
||||
mode: Mode
|
||||
): Promise<data.Dictionary> {
|
||||
const context = new data.Dictionary();
|
||||
|
||||
const filteredNames = filterContextNames(names, workflowContext);
|
||||
for (const contextName of filteredNames) {
|
||||
let value = (await getDefaultContext(contextName, workflowContext)) || new data.Dictionary();
|
||||
let value = getDefaultContext(contextName, workflowContext, mode) || new data.Dictionary();
|
||||
if (value.kind === Kind.Null) {
|
||||
context.add(contextName, value);
|
||||
continue;
|
||||
}
|
||||
|
||||
value = (await config?.getContext(contextName, value)) || value;
|
||||
|
||||
@@ -26,7 +42,7 @@ export async function getContext(
|
||||
return context;
|
||||
}
|
||||
|
||||
async function getDefaultContext(name: string, workflowContext: WorkflowContext): Promise<data.Dictionary | undefined> {
|
||||
function getDefaultContext(name: string, workflowContext: WorkflowContext, mode: Mode): ContextValue | undefined {
|
||||
switch (name) {
|
||||
case "runner":
|
||||
return objectToDictionary({
|
||||
@@ -53,7 +69,7 @@ async function getDefaultContext(name: string, workflowContext: WorkflowContext)
|
||||
return getStrategyContext(workflowContext);
|
||||
|
||||
case "matrix":
|
||||
return getMatrixContext(workflowContext);
|
||||
return getMatrixContext(workflowContext, mode);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {SequenceToken} from "@github/actions-workflow-parser/templates/tokens/se
|
||||
import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token";
|
||||
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
import {Mode} from "./default";
|
||||
import {getMatrixContext} from "./matrix";
|
||||
|
||||
type MatrixMap = {
|
||||
@@ -62,7 +63,7 @@ describe("matrix context", () => {
|
||||
const workflowContext = {} as WorkflowContext;
|
||||
expect(workflowContext.job).toBeUndefined();
|
||||
|
||||
const context = getMatrixContext(workflowContext);
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
expect(context).toEqual(new data.Dictionary());
|
||||
});
|
||||
|
||||
@@ -71,7 +72,7 @@ describe("matrix context", () => {
|
||||
const workflowContext = {job} as WorkflowContext;
|
||||
expect(workflowContext.job!.strategy).toBeUndefined();
|
||||
|
||||
const context = getMatrixContext(workflowContext);
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
expect(context).toEqual(new data.Dictionary());
|
||||
});
|
||||
|
||||
@@ -79,7 +80,7 @@ describe("matrix context", () => {
|
||||
const workflowContext = contextFromStrategy(stringToToken("hello"));
|
||||
expect(workflowContext.job!.strategy).toBeDefined();
|
||||
|
||||
const context = getMatrixContext(workflowContext);
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
expect(context).toEqual(new data.Dictionary());
|
||||
});
|
||||
|
||||
@@ -87,8 +88,8 @@ describe("matrix context", () => {
|
||||
const strategy = new MappingToken(undefined, undefined, undefined);
|
||||
const workflowContext = contextFromStrategy(strategy);
|
||||
|
||||
const context = getMatrixContext(workflowContext);
|
||||
expect(context).toEqual(new data.Dictionary());
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
expect(context).toEqual(new data.Null());
|
||||
});
|
||||
|
||||
it("matrix is not a mapping token", () => {
|
||||
@@ -96,8 +97,8 @@ describe("matrix context", () => {
|
||||
strategy.add(stringToToken("matrix"), stringToToken("hello"));
|
||||
const workflowContext = contextFromStrategy(strategy);
|
||||
|
||||
const context = getMatrixContext(workflowContext);
|
||||
expect(context).toEqual(new data.Dictionary());
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
expect(context).toEqual(new data.Null());
|
||||
});
|
||||
|
||||
it("empty matrix", () => {
|
||||
@@ -105,7 +106,7 @@ describe("matrix context", () => {
|
||||
strategy.add(stringToToken("matrix"), new MappingToken(undefined, undefined, undefined));
|
||||
const workflowContext = contextFromStrategy(strategy);
|
||||
|
||||
const context = getMatrixContext(workflowContext);
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
expect(context).toEqual(new data.Dictionary());
|
||||
});
|
||||
});
|
||||
@@ -116,9 +117,9 @@ describe("matrix context", () => {
|
||||
strategy.add(stringToToken("matrix"), expressionToToken("${{ fromJSON(needs.job1.outputs.matrix) }}"));
|
||||
|
||||
const workflowContext = contextFromStrategy(strategy);
|
||||
const context = getMatrixContext(workflowContext);
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
|
||||
expect(context).toEqual(new data.Dictionary());
|
||||
expect(context).toEqual(new data.Null());
|
||||
});
|
||||
|
||||
it("matrix with include expression", () => {
|
||||
@@ -136,9 +137,35 @@ describe("matrix context", () => {
|
||||
strategy.add(stringToToken("matrix"), matrix);
|
||||
|
||||
const workflowContext = contextFromStrategy(strategy);
|
||||
const context = getMatrixContext(workflowContext);
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
|
||||
expect(context).toEqual(new data.Dictionary());
|
||||
expect(context).toEqual(new data.Null());
|
||||
});
|
||||
|
||||
it("matrix with include expression during completion", () => {
|
||||
const include = expressionToToken("${{ fromJSON(needs.job1.outputs.matrix) }}");
|
||||
|
||||
const nodeSequence = new SequenceToken(undefined, undefined, undefined);
|
||||
nodeSequence.add(stringToToken("12"));
|
||||
nodeSequence.add(stringToToken("14"));
|
||||
|
||||
const matrix = new MappingToken(undefined, undefined, undefined);
|
||||
matrix.add(stringToToken("node"), nodeSequence);
|
||||
matrix.add(stringToToken("include"), include);
|
||||
|
||||
const strategy = new MappingToken(undefined, undefined, undefined);
|
||||
strategy.add(stringToToken("matrix"), matrix);
|
||||
|
||||
const workflowContext = contextFromStrategy(strategy);
|
||||
|
||||
const context = getMatrixContext(workflowContext, Mode.Completion);
|
||||
|
||||
expect(context).toEqual(
|
||||
new data.Dictionary({
|
||||
key: "node",
|
||||
value: new data.Array(new data.StringData("12"), new data.StringData("14"))
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("matrix with expression within property", () => {
|
||||
@@ -151,7 +178,7 @@ describe("matrix context", () => {
|
||||
strategy.add(stringToToken("matrix"), matrix);
|
||||
|
||||
const workflowContext = contextFromStrategy(strategy);
|
||||
const context = getMatrixContext(workflowContext);
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
|
||||
expect(context).toEqual(
|
||||
new data.Dictionary({
|
||||
@@ -166,7 +193,7 @@ describe("matrix context", () => {
|
||||
it("basic matrix", () => {
|
||||
const workflowContext = createMatrix({os: ["ubuntu-latest", "windows-latest"]});
|
||||
|
||||
const context = getMatrixContext(workflowContext);
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
expect(context).toEqual(
|
||||
new data.Dictionary({
|
||||
key: "os",
|
||||
@@ -181,7 +208,7 @@ describe("matrix context", () => {
|
||||
node: ["12", "14"]
|
||||
});
|
||||
|
||||
const context = getMatrixContext(workflowContext);
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
expect(context).toEqual(
|
||||
new data.Dictionary(
|
||||
{
|
||||
@@ -208,7 +235,7 @@ describe("matrix context", () => {
|
||||
]
|
||||
});
|
||||
|
||||
const context = getMatrixContext(workflowContext);
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
|
||||
expect(context).toEqual(
|
||||
new data.Dictionary(
|
||||
@@ -242,7 +269,7 @@ describe("matrix context", () => {
|
||||
]
|
||||
});
|
||||
|
||||
const context = getMatrixContext(workflowContext);
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
|
||||
expect(context).toEqual(
|
||||
new data.Dictionary(
|
||||
@@ -276,7 +303,7 @@ describe("matrix context", () => {
|
||||
]
|
||||
});
|
||||
|
||||
const context = getMatrixContext(workflowContext);
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
|
||||
expect(context).toEqual(
|
||||
new data.Dictionary(
|
||||
@@ -311,7 +338,7 @@ describe("matrix context", () => {
|
||||
]
|
||||
});
|
||||
|
||||
const context = getMatrixContext(workflowContext);
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
|
||||
expect(context).toEqual(new data.Dictionary());
|
||||
});
|
||||
|
||||
@@ -4,8 +4,9 @@ import {KeyValuePair} from "@github/actions-workflow-parser/templates/tokens/key
|
||||
import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token";
|
||||
import {SequenceToken} from "@github/actions-workflow-parser/templates/tokens/sequence-token";
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
import {ContextValue, Mode} from "./default";
|
||||
|
||||
export function getMatrixContext(workflowContext: WorkflowContext): data.Dictionary {
|
||||
export function getMatrixContext(workflowContext: WorkflowContext, mode: Mode): ContextValue {
|
||||
// https://docs.github.com/en/actions/learn-github-actions/contexts#matrix-context
|
||||
const strategy = workflowContext.job?.strategy;
|
||||
if (!strategy || !isMapping(strategy)) {
|
||||
@@ -15,13 +16,13 @@ export function getMatrixContext(workflowContext: WorkflowContext): data.Diction
|
||||
const matrix = strategy.find("matrix");
|
||||
if (!matrix || !isMapping(matrix)) {
|
||||
// Matrix could be an expression, so there's no context we can provide
|
||||
return new data.Dictionary();
|
||||
return new data.Null();
|
||||
}
|
||||
|
||||
const properties = matrixProperties(matrix);
|
||||
const properties = matrixProperties(matrix, mode);
|
||||
if (!properties) {
|
||||
// Matrix included an expression, so there's no context we can provide
|
||||
return new data.Dictionary();
|
||||
return new data.Null();
|
||||
}
|
||||
|
||||
const d = new data.Dictionary();
|
||||
@@ -93,7 +94,7 @@ export function getMatrixContext(workflowContext: WorkflowContext): data.Diction
|
||||
*
|
||||
* Keys: os, version, environment
|
||||
*/
|
||||
function matrixProperties(matrix: MappingToken): Map<string, Set<string> | undefined> | undefined {
|
||||
function matrixProperties(matrix: MappingToken, mode: Mode): Map<string, Set<string> | undefined> | undefined {
|
||||
const properties = new Map<string, Set<string> | undefined>();
|
||||
|
||||
let include: SequenceToken | undefined;
|
||||
@@ -106,9 +107,14 @@ function matrixProperties(matrix: MappingToken): Map<string, Set<string> | undef
|
||||
const key = pair.key.value;
|
||||
switch (key) {
|
||||
case "include":
|
||||
// If "include" is an expression, we can't know the properties of the matrix
|
||||
// If "include" is an expression, we can't know the full properties of the matrix
|
||||
if (isBasicExpression(pair.value) || !isSequence(pair.value)) {
|
||||
return;
|
||||
// Without the full properties of the matrix, we shouldn't validate anything
|
||||
if (mode === Mode.Validation) {
|
||||
return;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
include = pair.value;
|
||||
break;
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
import {Position, TextDocument} from "vscode-languageserver-textdocument";
|
||||
import {createDocument} from "./document";
|
||||
|
||||
// Calculates the position of the cursor and the document without that cursor
|
||||
// Cursor is represented by a `|` character
|
||||
export function getPositionFromCursor(input: string): [TextDocument, Position] {
|
||||
/**
|
||||
* Calculates the position of the cursor and the document without that cursor
|
||||
* Cursor is represented by a `|` character
|
||||
* @param input Input string
|
||||
* @param skip Instances of `|` to skip
|
||||
*/
|
||||
export function getPositionFromCursor(input: string, skip = 0): [TextDocument, Position] {
|
||||
const doc = createDocument("test.yaml", input);
|
||||
|
||||
const cursorIndex = doc.getText().indexOf("|");
|
||||
let cursorIndex = doc.getText().indexOf("|");
|
||||
for (let i = 0; i < skip && cursorIndex !== -1; i++) {
|
||||
cursorIndex = doc.getText().indexOf("|", cursorIndex + 1);
|
||||
}
|
||||
|
||||
if (cursorIndex === -1) {
|
||||
throw new Error("No cursor found in document");
|
||||
}
|
||||
|
||||
// Replace only the last occurence of | in string
|
||||
let newText = doc.getText();
|
||||
newText = newText.substring(0, cursorIndex) + newText.substring(cursorIndex + 1);
|
||||
|
||||
const position = doc.positionAt(cursorIndex);
|
||||
const newDoc = TextDocument.create(doc.uri, doc.languageId, doc.version, doc.getText().replace("|", ""));
|
||||
const newDoc = TextDocument.create(doc.uri, doc.languageId, doc.version, newText);
|
||||
|
||||
return [newDoc, position];
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
|
||||
|
||||
export function getAllowedContext(token: TemplateToken, parent: TemplateToken): string[] {
|
||||
export function getAllowedContext(token: TemplateToken, parent: TemplateToken | null | undefined): string[] {
|
||||
// Workaround for https://github.com/github/c2c-actions-experience/issues/6876
|
||||
// Context is inherited from the parent
|
||||
const allowedContext = new Set<string>();
|
||||
for (const t of [token, parent]) {
|
||||
if (t.definition?.readerContext) {
|
||||
if (t?.definition?.readerContext) {
|
||||
for (const context of t.definition.readerContext) {
|
||||
allowedContext.add(context);
|
||||
}
|
||||
|
||||
@@ -305,4 +305,476 @@ jobs:
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("multi-line strings", () => {
|
||||
it("indented |", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
first line
|
||||
test \${{ github.does-not-exist }}
|
||||
test2`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Context access might be invalid: does-not-exist",
|
||||
range: {
|
||||
end: {
|
||||
character: 43,
|
||||
line: 7
|
||||
},
|
||||
start: {
|
||||
character: 15,
|
||||
line: 7
|
||||
}
|
||||
},
|
||||
severity: DiagnosticSeverity.Warning
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("indented |+", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |+
|
||||
first line
|
||||
test \${{ github.does-not-exist }}
|
||||
test2`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Context access might be invalid: does-not-exist",
|
||||
range: {
|
||||
end: {
|
||||
character: 43,
|
||||
line: 7
|
||||
},
|
||||
start: {
|
||||
character: 15,
|
||||
line: 7
|
||||
}
|
||||
},
|
||||
severity: DiagnosticSeverity.Warning
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("indented >", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: >
|
||||
first line
|
||||
test \${{ github.does-not-exist }}
|
||||
test2`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Context access might be invalid: does-not-exist",
|
||||
range: {
|
||||
end: {
|
||||
character: 43,
|
||||
line: 7
|
||||
},
|
||||
start: {
|
||||
character: 15,
|
||||
line: 7
|
||||
}
|
||||
},
|
||||
severity: DiagnosticSeverity.Warning
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("indented >+", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: >+
|
||||
first line
|
||||
test \${{ github.does-not-exist }}
|
||||
test2`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Context access might be invalid: does-not-exist",
|
||||
range: {
|
||||
end: {
|
||||
character: 43,
|
||||
line: 7
|
||||
},
|
||||
start: {
|
||||
character: 15,
|
||||
line: 7
|
||||
}
|
||||
},
|
||||
severity: DiagnosticSeverity.Warning
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("matrix context", () => {
|
||||
it("reference within a matrix job", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [14, 16]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: echo \${{ matrix.node }}
|
||||
`;
|
||||
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("reference outside of a matrix job", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: echo \${{ matrix.node }}
|
||||
`;
|
||||
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Context access might be invalid: matrix",
|
||||
range: {
|
||||
end: {
|
||||
character: 36,
|
||||
line: 8
|
||||
},
|
||||
start: {
|
||||
character: 18,
|
||||
line: 8
|
||||
}
|
||||
},
|
||||
severity: DiagnosticSeverity.Warning
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("basic matrix", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: \${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
node: [14, 16]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: echo \${{ matrix.node }}
|
||||
`;
|
||||
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("invalid property reference", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: \${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
node: [14, 16]
|
||||
include:
|
||||
- os: macos-latest
|
||||
node: 14
|
||||
exclude:
|
||||
- os: windows-latest
|
||||
node: 14
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: echo \${{ matrix.goversion }}
|
||||
`;
|
||||
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Context access might be invalid: goversion",
|
||||
range: {
|
||||
end: {
|
||||
character: 41,
|
||||
line: 18
|
||||
},
|
||||
start: {
|
||||
character: 18,
|
||||
line: 18
|
||||
}
|
||||
},
|
||||
severity: DiagnosticSeverity.Warning
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("matrix with include", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: \${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
node: [14, 16]
|
||||
include:
|
||||
- os: macos-latest
|
||||
node: 14
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: echo \${{ matrix.node }}
|
||||
`;
|
||||
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("matrix with only include", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-latest
|
||||
node: 14
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: echo \${{ matrix.os }}
|
||||
- run: echo \${{ matrix.node }}
|
||||
`;
|
||||
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("matrix with exclude", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: \${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
node: [14, 16]
|
||||
include:
|
||||
- os: macos-latest
|
||||
node: 14
|
||||
exclude:
|
||||
- os: windows-latest
|
||||
node: 14
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: \${{ matrix.node }}
|
||||
`;
|
||||
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("matrix with only exclude", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: \${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
exclude:
|
||||
- os: windows-latest
|
||||
node: 14
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: \${{ matrix.node }}
|
||||
`;
|
||||
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Context access might be invalid: os",
|
||||
range: {
|
||||
end: {
|
||||
character: 29,
|
||||
line: 5
|
||||
},
|
||||
start: {
|
||||
character: 13,
|
||||
line: 5
|
||||
}
|
||||
},
|
||||
severity: DiagnosticSeverity.Warning
|
||||
},
|
||||
{
|
||||
message: "Context access might be invalid: node",
|
||||
range: {
|
||||
end: {
|
||||
character: 42,
|
||||
line: 15
|
||||
},
|
||||
start: {
|
||||
character: 24,
|
||||
line: 15
|
||||
}
|
||||
},
|
||||
severity: DiagnosticSeverity.Warning
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("matrix from expression", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix: \${{ fromJSON('{"color":["green","blue"]}') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: \${{ matrix.ANYVALUE }}
|
||||
`;
|
||||
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("matrix with include expression", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
fruit: [apple, pear]
|
||||
animal: [cat, dog]
|
||||
include: \${{ fromJSON('{"color":"green"}') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: \${{ matrix.ANYVALUE }}
|
||||
`;
|
||||
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("matrix with property expression", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
color: \${{ fromJSON('["green","blue"]') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: \${{ matrix.color }}
|
||||
`;
|
||||
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("matrix with property expression and invalid property reference", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
color: \${{ fromJSON('["green","blue"]') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: \${{ matrix.shape }}
|
||||
`;
|
||||
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Context access might be invalid: shape",
|
||||
range: {
|
||||
end: {
|
||||
character: 43,
|
||||
line: 13
|
||||
},
|
||||
start: {
|
||||
character: 24,
|
||||
line: 13
|
||||
}
|
||||
},
|
||||
severity: DiagnosticSeverity.Warning
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
} from "@github/actions-workflow-parser";
|
||||
import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert";
|
||||
import {splitAllowedContext} from "@github/actions-workflow-parser/templates/allowed-context";
|
||||
import {Definition} from "@github/actions-workflow-parser/templates/schema/definition";
|
||||
import {BasicExpressionToken} from "@github/actions-workflow-parser/templates/tokens/basic-expression-token";
|
||||
import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token";
|
||||
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
|
||||
@@ -19,11 +18,12 @@ import {TextDocument} from "vscode-languageserver-textdocument";
|
||||
import {Diagnostic, DiagnosticSeverity, URI} from "vscode-languageserver-types";
|
||||
|
||||
import {ContextProviderConfig} from "./context-providers/config";
|
||||
import {getContext} from "./context-providers/default";
|
||||
import {getContext, Mode} from "./context-providers/default";
|
||||
import {getWorkflowContext, WorkflowContext} from "./context/workflow-context";
|
||||
import {AccessError, wrapDictionary} from "./expression-validation/error-dictionary";
|
||||
import {error} from "./log";
|
||||
import {nullTrace} from "./nulltrace";
|
||||
import {getAllowedContext} from "./utils/allowed-context";
|
||||
import {findToken} from "./utils/find-token";
|
||||
import {mapRange} from "./utils/range";
|
||||
import {ValueProviderConfig, ValueProviderKind} from "./value-providers/config";
|
||||
@@ -95,12 +95,14 @@ async function additionalValidations(
|
||||
const validationToken = key || parent || token;
|
||||
const validationDefinition = validationToken.definition;
|
||||
|
||||
const allowedContext = getAllowedContext(validationToken, parent);
|
||||
|
||||
// If this is an expression, validate it
|
||||
if (isBasicExpression(token)) {
|
||||
await validateExpression(
|
||||
diagnostics,
|
||||
token,
|
||||
validationDefinition,
|
||||
allowedContext,
|
||||
contextProviderConfig,
|
||||
getProviderContext(documentUri, template, root, token)
|
||||
);
|
||||
@@ -171,14 +173,13 @@ function getProviderContext(
|
||||
async function validateExpression(
|
||||
diagnostics: Diagnostic[],
|
||||
token: BasicExpressionToken,
|
||||
definition: Definition | undefined,
|
||||
allowedContext: string[],
|
||||
contextProviderConfig: ContextProviderConfig | undefined,
|
||||
workflowContext: WorkflowContext
|
||||
) {
|
||||
// Validate the expression
|
||||
for (const expression of token.originalExpressions || [token]) {
|
||||
const allowedContexts = definition?.readerContext || [];
|
||||
const {namedContexts, functions} = splitAllowedContext(allowedContexts);
|
||||
const {namedContexts, functions} = splitAllowedContext(allowedContext);
|
||||
|
||||
let expr: Expr | undefined;
|
||||
|
||||
@@ -194,7 +195,7 @@ async function validateExpression(
|
||||
}
|
||||
|
||||
try {
|
||||
const context = await getContext(namedContexts, contextProviderConfig, workflowContext);
|
||||
const context = await getContext(namedContexts, contextProviderConfig, workflowContext, Mode.Validation);
|
||||
|
||||
const e = new Evaluator(expr, wrapDictionary(context));
|
||||
e.evaluate();
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "browser-playground",
|
||||
"version": "0.1.41",
|
||||
"version": "0.1.47",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@github/actions-languageserver": "^0.1.41",
|
||||
"@github/actions-languageserver": "^0.1.47",
|
||||
"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.41"
|
||||
"version": "0.1.47"
|
||||
}
|
||||
|
||||
Generated
+13
-13
@@ -16,10 +16,10 @@
|
||||
},
|
||||
"actions-languageserver": {
|
||||
"name": "@github/actions-languageserver",
|
||||
"version": "0.1.41",
|
||||
"version": "0.1.47",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-languageservice": "^0.1.41",
|
||||
"@github/actions-languageservice": "^0.1.47",
|
||||
"@octokit/rest": "^19.0.5",
|
||||
"vscode-languageserver": "^8.0.2",
|
||||
"vscode-languageserver-textdocument": "^1.0.7"
|
||||
@@ -38,7 +38,7 @@
|
||||
},
|
||||
"actions-languageservice": {
|
||||
"name": "@github/actions-languageservice",
|
||||
"version": "0.1.41",
|
||||
"version": "0.1.47",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-workflow-parser": "*",
|
||||
@@ -59,10 +59,10 @@
|
||||
}
|
||||
},
|
||||
"browser-playground": {
|
||||
"version": "0.1.41",
|
||||
"version": "0.1.47",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-languageserver": "^0.1.41",
|
||||
"@github/actions-languageserver": "^0.1.47",
|
||||
"monaco-editor-webpack-plugin": "^7.0.1",
|
||||
"monaco-editor-workers": "^0.34.2",
|
||||
"monaco-languageclient": "^4.0.3",
|
||||
@@ -698,9 +698,9 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@github/actions-workflow-parser": {
|
||||
"version": "0.0.30",
|
||||
"resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.30/2c95f0614784e740bc544d2a8f7b1661e9526920",
|
||||
"integrity": "sha512-Gr/fDCh+dVZVCgDAo67gPVqV+cVrrhBPkVIaxnpyBVnPUtVftthY4+Klg3r37gNRSchxuORyxKQk/cPDrZwxoQ==",
|
||||
"version": "0.0.31",
|
||||
"resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.31/ebc46956b91ed1a8c45efd41a3256ae818722557",
|
||||
"integrity": "sha512-3k+MBWG7Gn86aHeLdJTiYUTGm53npEelzleCbq/8Ir51xVkRINsyNe4HuGJizXxZ2WoTxAkLeZ6qpO1oCucSIg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-expressions": "*",
|
||||
@@ -13505,7 +13505,7 @@
|
||||
"@github/actions-languageserver": {
|
||||
"version": "file:actions-languageserver",
|
||||
"requires": {
|
||||
"@github/actions-languageservice": "^0.1.41",
|
||||
"@github/actions-languageservice": "^0.1.47",
|
||||
"@octokit/rest": "^19.0.5",
|
||||
"@types/jest": "^29.0.3",
|
||||
"jest": "^29.0.3",
|
||||
@@ -13533,9 +13533,9 @@
|
||||
}
|
||||
},
|
||||
"@github/actions-workflow-parser": {
|
||||
"version": "0.0.30",
|
||||
"resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.30/2c95f0614784e740bc544d2a8f7b1661e9526920",
|
||||
"integrity": "sha512-Gr/fDCh+dVZVCgDAo67gPVqV+cVrrhBPkVIaxnpyBVnPUtVftthY4+Klg3r37gNRSchxuORyxKQk/cPDrZwxoQ==",
|
||||
"version": "0.0.31",
|
||||
"resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.31/ebc46956b91ed1a8c45efd41a3256ae818722557",
|
||||
"integrity": "sha512-3k+MBWG7Gn86aHeLdJTiYUTGm53npEelzleCbq/8Ir51xVkRINsyNe4HuGJizXxZ2WoTxAkLeZ6qpO1oCucSIg==",
|
||||
"requires": {
|
||||
"@github/actions-expressions": "*",
|
||||
"yaml": "^2.0.0-8"
|
||||
@@ -16288,7 +16288,7 @@
|
||||
"browser-playground": {
|
||||
"version": "file:browser-playground",
|
||||
"requires": {
|
||||
"@github/actions-languageserver": "^0.1.41",
|
||||
"@github/actions-languageserver": "^0.1.47",
|
||||
"css-loader": "^6.7.2",
|
||||
"monaco-editor-webpack-plugin": "^7.0.1",
|
||||
"monaco-editor-workers": "^0.34.2",
|
||||
|
||||
Reference in New Issue
Block a user