Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ee228549a | ||
|
|
45a665690b | ||
|
|
0a9f420c96 | ||
|
|
fb962e6c47 | ||
|
|
e3db56d6ed | ||
|
|
872c873c9d | ||
|
|
29e676fad1 | ||
|
|
e8b3c20aca | ||
|
|
50f9c04a91 | ||
|
|
9c4b8a4c1c | ||
|
|
6b1e4ff115 | ||
|
|
b79d0e8e47 | ||
|
|
c538615cc7 | ||
|
|
39fa2c1e14 | ||
|
|
e229500fea | ||
|
|
172f450745 | ||
|
|
266bebde2b | ||
|
|
895f62ba7e | ||
|
|
d86a0ca08f | ||
|
|
6e5149bc79 | ||
|
|
99dd997d88 | ||
|
|
3efd7934b4 | ||
|
|
130f22d937 | ||
|
|
019717a8cd | ||
|
|
a0eb7ac824 | ||
|
|
d670d73091 | ||
|
|
7f3d6f7382 | ||
|
|
ee2e82a4bd | ||
|
|
d65c105d83 | ||
|
|
bc1fb6e6d6 |
+1
-1
@@ -1 +1 @@
|
|||||||
* @github/c2c-actions-experience-parser-reviewers
|
* @actions/actions-experience
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- package.json
|
- lerna.json
|
||||||
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
uses: actions/github-script@v6
|
uses: actions/github-script@v6
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const version = '${{ inputs.version }}' || require('./package.json').version;
|
const version = '${{ inputs.version }}' || require('./lerna.json').version;
|
||||||
// Find a release for that version
|
// Find a release for that version
|
||||||
const release = await github.rest.repos.getReleaseByTag({
|
const release = await github.rest.repos.getReleaseByTag({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
@@ -59,12 +59,10 @@ jobs:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
EXT_VERSION: "" # will be set in the workflow
|
PKG_VERSION: "" # will be set in the workflow
|
||||||
|
|
||||||
outputs:
|
|
||||||
version: ${{ env.EXT_VERSION }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -73,21 +71,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 16.x
|
node-version: 16.x
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
registry-url: "https://npm.pkg.github.com"
|
scope: '@actions'
|
||||||
|
|
||||||
- name: Parse version from package.json
|
- name: Parse version from lerna.json
|
||||||
run: |
|
run: |
|
||||||
echo "EXT_VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
echo "PKG_VERSION=$(node -p -e "require('./lerna.json').version")" >> $GITHUB_ENV
|
||||||
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Publish packages
|
- name: Create release
|
||||||
run: |
|
|
||||||
lerna publish from-git
|
|
||||||
|
|
||||||
- name: Create release and upload release asset
|
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@v6
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
@@ -96,11 +88,22 @@ jobs:
|
|||||||
const release = await github.rest.repos.createRelease({
|
const release = await github.rest.repos.createRelease({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
tag_name: "release-v${{ env.EXT_VERSION }}",
|
tag_name: "release-v${{ env.PKG_VERSION }}",
|
||||||
name: "v${{ env.EXT_VERSION }}",
|
name: "v${{ env.PKG_VERSION }}",
|
||||||
draft: false,
|
draft: false,
|
||||||
prerelease: false
|
prerelease: false
|
||||||
});
|
});
|
||||||
|
|
||||||
core.summary.addLink(`Release v${{ env.EXT_VERSION }}`, release.data.html_url);
|
core.summary.addLink(`Release v${{ env.PKG_VERSION }}`, release.data.html_url);
|
||||||
await core.summary.write();
|
await core.summary.write();
|
||||||
|
|
||||||
|
- name: setup authentication
|
||||||
|
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> .npmrc
|
||||||
|
env:
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
|
- name: Publish packages
|
||||||
|
run: |
|
||||||
|
lerna publish ${{ env.PKG_VERSION }} --yes --no-git-reset --no-git-tag-version
|
||||||
|
env:
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "browser-playground",
|
"name": "browser-playground",
|
||||||
"version": "0.1.2",
|
"version": "0.2.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/languageserver": "^0.1.2",
|
"@actions/languageserver": "^0.2.0",
|
||||||
"monaco-editor-webpack-plugin": "^7.0.1",
|
"monaco-editor-webpack-plugin": "^7.0.1",
|
||||||
"monaco-editor-workers": "^0.34.2",
|
"monaco-editor-workers": "^0.34.2",
|
||||||
"monaco-languageclient": "^4.0.3",
|
"monaco-languageclient": "^4.0.3",
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@actions/expressions",
|
"name": "@actions/expressions",
|
||||||
"version": "0.1.2",
|
"version": "0.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"source": "./src/index.ts",
|
"source": "./src/index.ts",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"import": "./dist/index.js"
|
"import": "./dist/index.js"
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "@actions/languageserver",
|
"name": "@actions/languageserver",
|
||||||
"version": "0.1.2",
|
"version": "0.3.1",
|
||||||
"description": "Language server for GitHub Actions",
|
"description": "Language server for GitHub Actions",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"source": "./src/index.ts",
|
"source": "./src/index.ts",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"import": "./dist/index.js"
|
"import": "./dist/index.js"
|
||||||
@@ -40,8 +43,8 @@
|
|||||||
"watch": "tsc --build tsconfig.build.json --watch"
|
"watch": "tsc --build tsconfig.build.json --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/languageservice": "^0.1.2",
|
"@actions/languageservice": "^0.3.1",
|
||||||
"@actions/workflow-parser": "^0.1.2",
|
"@actions/workflow-parser": "^0.3.1",
|
||||||
"@octokit/rest": "^19.0.7",
|
"@octokit/rest": "^19.0.7",
|
||||||
"@octokit/types": "^9.0.0",
|
"@octokit/types": "^9.0.0",
|
||||||
"vscode-languageserver": "^8.0.2",
|
"vscode-languageserver": "^8.0.2",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {getFileProvider} from "./file-provider";
|
|||||||
import {InitializationOptions, RepositoryContext} from "./initializationOptions";
|
import {InitializationOptions, RepositoryContext} from "./initializationOptions";
|
||||||
import {onCompletion} from "./on-completion";
|
import {onCompletion} from "./on-completion";
|
||||||
import {ReadFileRequest, Requests} from "./request";
|
import {ReadFileRequest, Requests} from "./request";
|
||||||
import {fetchActionMetadata} from "./utils/action-metadata";
|
import {getActionsMetadataProvider} from "./utils/action-metadata";
|
||||||
import {TTLCache} from "./utils/cache";
|
import {TTLCache} from "./utils/cache";
|
||||||
import {timeOperation} from "./utils/timer";
|
import {timeOperation} from "./utils/timer";
|
||||||
import {valueProviders} from "./value-providers";
|
import {valueProviders} from "./value-providers";
|
||||||
@@ -108,13 +108,7 @@ export function initConnection(connection: Connection) {
|
|||||||
const config: ValidationConfig = {
|
const config: ValidationConfig = {
|
||||||
valueProviderConfig: valueProviders(client, repoContext, cache),
|
valueProviderConfig: valueProviders(client, repoContext, cache),
|
||||||
contextProviderConfig: contextProviders(client, repoContext, cache),
|
contextProviderConfig: contextProviders(client, repoContext, cache),
|
||||||
fetchActionMetadata: async action => {
|
actionsMetadataProvider: getActionsMetadataProvider(client, cache),
|
||||||
if (client) {
|
|
||||||
return await fetchActionMetadata(client, cache, action);
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
fileProvider: getFileProvider(client, cache, repoContext?.workspaceUri, async path => {
|
fileProvider: getFileProvider(client, cache, repoContext?.workspaceUri, async path => {
|
||||||
return await connection.sendRequest(Requests.ReadFile, {path} satisfies ReadFileRequest);
|
return await connection.sendRequest(Requests.ReadFile, {path} satisfies ReadFileRequest);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
import {actionIdentifier, ActionMetadata, ActionReference} from "@actions/languageservice/action";
|
import {actionIdentifier, ActionMetadata, ActionReference} from "@actions/languageservice/action";
|
||||||
|
import {ActionsMetadataProvider} from "@actions/languageservice";
|
||||||
import {error} from "@actions/languageservice/log";
|
import {error} from "@actions/languageservice/log";
|
||||||
import {Octokit, RestEndpointMethodTypes} from "@octokit/rest";
|
import {Octokit, RestEndpointMethodTypes} from "@octokit/rest";
|
||||||
import {parse} from "yaml";
|
import {parse} from "yaml";
|
||||||
import {TTLCache} from "./cache";
|
import {TTLCache} from "./cache";
|
||||||
import {errorMessage, errorStatus} from "./error";
|
import {errorMessage, errorStatus} from "./error";
|
||||||
|
|
||||||
|
export function getActionsMetadataProvider(
|
||||||
|
client: Octokit | undefined,
|
||||||
|
cache: TTLCache
|
||||||
|
): ActionsMetadataProvider | undefined {
|
||||||
|
if (!client) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fetchActionMetadata: async action => fetchActionMetadata(client, cache, action)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchActionMetadata(
|
export async function fetchActionMetadata(
|
||||||
client: Octokit,
|
client: Octokit,
|
||||||
cache: TTLCache,
|
cache: TTLCache,
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "@actions/languageservice",
|
"name": "@actions/languageservice",
|
||||||
"version": "0.1.2",
|
"version": "0.3.1",
|
||||||
"description": "Language service for GitHub Actions",
|
"description": "Language service for GitHub Actions",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"source": "./src/index.ts",
|
"source": "./src/index.ts",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"import": "./dist/index.js"
|
"import": "./dist/index.js"
|
||||||
@@ -41,8 +44,8 @@
|
|||||||
"watch": "tsc --build tsconfig.build.json --watch"
|
"watch": "tsc --build tsconfig.build.json --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/expressions": "^0.1.2",
|
"@actions/expressions": "^0.3.1",
|
||||||
"@actions/workflow-parser": "^0.1.2",
|
"@actions/workflow-parser": "^0.3.1",
|
||||||
"vscode-languageserver-textdocument": "^1.0.7",
|
"vscode-languageserver-textdocument": "^1.0.7",
|
||||||
"vscode-languageserver-types": "^3.17.2",
|
"vscode-languageserver-types": "^3.17.2",
|
||||||
"vscode-uri": "^3.0.7",
|
"vscode-uri": "^3.0.7",
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
import {data, isDescriptionDictionary} from "@actions/expressions";
|
|
||||||
import {isDictionary} from "@actions/expressions/data/dictionary";
|
|
||||||
import {ExpressionData, Pair} from "@actions/expressions/data/expressiondata";
|
|
||||||
|
|
||||||
export class AccessError extends Error {
|
|
||||||
constructor(message: string, public readonly keyName: string) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ErrorDictionary extends data.Dictionary {
|
|
||||||
constructor(...pairs: Pair[]) {
|
|
||||||
super(...pairs);
|
|
||||||
}
|
|
||||||
public complete = true;
|
|
||||||
|
|
||||||
get(key: string): ExpressionData | undefined {
|
|
||||||
const value = super.get(key);
|
|
||||||
if (value) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.complete) {
|
|
||||||
throw new AccessError(`Invalid context access: ${key}`, key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function wrapDictionary(d: data.Dictionary): ErrorDictionary {
|
|
||||||
const e = new ErrorDictionary();
|
|
||||||
if (isDescriptionDictionary(d)) {
|
|
||||||
e.complete = d.complete;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const {key, value} of d.pairs()) {
|
|
||||||
if (isDictionary(value)) {
|
|
||||||
e.add(key, wrapDictionary(value));
|
|
||||||
} else {
|
|
||||||
e.add(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import {Lexer, Parser} from "@actions/expressions";
|
||||||
|
import {Dictionary} from "@actions/expressions/data/dictionary";
|
||||||
|
import {StringData} from "@actions/expressions/data/string";
|
||||||
|
import {splitAllowedContext} from "@actions/workflow-parser/templates/allowed-context";
|
||||||
|
import {ValidationVisitor} from "./visitor";
|
||||||
|
|
||||||
|
const testContext = new Dictionary({
|
||||||
|
key: "github",
|
||||||
|
value: new Dictionary(
|
||||||
|
{
|
||||||
|
key: "event",
|
||||||
|
value: new StringData("push")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "repo",
|
||||||
|
value: new Dictionary({
|
||||||
|
key: "name",
|
||||||
|
value: new StringData("test")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
function useVisitor(expression: string, allowedContext: string[]): any[] {
|
||||||
|
const {namedContexts, functions} = splitAllowedContext(allowedContext);
|
||||||
|
|
||||||
|
const l = new Lexer(expression);
|
||||||
|
const lr = l.lex();
|
||||||
|
|
||||||
|
const p = new Parser(lr.tokens, namedContexts, functions);
|
||||||
|
const expr = p.parse();
|
||||||
|
|
||||||
|
const e = new ValidationVisitor(expr, testContext);
|
||||||
|
e.validate();
|
||||||
|
|
||||||
|
return e.errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("validation visitor", () => {
|
||||||
|
it("invalid context access", () => {
|
||||||
|
expect(useVisitor("github.foo", ["github"])).toEqual([
|
||||||
|
{
|
||||||
|
message: "Context access might be invalid: foo",
|
||||||
|
range: {
|
||||||
|
end: {
|
||||||
|
column: 10,
|
||||||
|
line: 0
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
column: 0,
|
||||||
|
line: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
severity: "warning"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("invalid context access as index", () => {
|
||||||
|
expect(useVisitor("github[github.foo]", ["github"])).toEqual([
|
||||||
|
{
|
||||||
|
message: "Context access might be invalid: foo",
|
||||||
|
range: {
|
||||||
|
end: {
|
||||||
|
column: 17,
|
||||||
|
line: 0
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
column: 7,
|
||||||
|
line: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
severity: "warning"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("invalid nested context access", () => {
|
||||||
|
expect(useVisitor("github.repo.name", ["github"])).toEqual([
|
||||||
|
{
|
||||||
|
message: "Context access might be invalid: name",
|
||||||
|
range: {
|
||||||
|
end: {
|
||||||
|
column: 16,
|
||||||
|
line: 0
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
column: 0,
|
||||||
|
line: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
severity: "warning"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("invalid context accesses", () => {
|
||||||
|
expect(useVisitor("github.foo || github.foo.bar", ["github"])).toEqual([
|
||||||
|
{
|
||||||
|
message: "Context access might be invalid: foo",
|
||||||
|
range: {
|
||||||
|
end: {
|
||||||
|
column: 10,
|
||||||
|
line: 0
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
column: 0,
|
||||||
|
line: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
severity: "warning"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Context access might be invalid: bar",
|
||||||
|
range: {
|
||||||
|
end: {
|
||||||
|
column: 28,
|
||||||
|
line: 0
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
column: 14,
|
||||||
|
line: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
severity: "warning"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import {DescriptionDictionary} from "@actions/expressions";
|
||||||
|
import {
|
||||||
|
Binary,
|
||||||
|
ContextAccess,
|
||||||
|
Expr,
|
||||||
|
ExprVisitor,
|
||||||
|
FunctionCall,
|
||||||
|
Grouping,
|
||||||
|
IndexAccess,
|
||||||
|
Literal,
|
||||||
|
Logical,
|
||||||
|
Unary
|
||||||
|
} from "@actions/expressions/ast";
|
||||||
|
import {Dictionary} from "@actions/expressions/data/dictionary";
|
||||||
|
import {ExpressionData} from "@actions/expressions/data/expressiondata";
|
||||||
|
import {Range} from "@actions/expressions/lexer";
|
||||||
|
|
||||||
|
export type ValidationError = {
|
||||||
|
range: Range;
|
||||||
|
message: string;
|
||||||
|
severity: "error" | "warning";
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ValidationVisitor implements ExprVisitor<void> {
|
||||||
|
public readonly errors: ValidationError[] = [];
|
||||||
|
|
||||||
|
constructor(private expr: Expr, private context: Dictionary) {}
|
||||||
|
|
||||||
|
validate(): void {
|
||||||
|
this._validate(this.expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _validate(expr: Expr) {
|
||||||
|
expr.accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
visitLiteral() {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
visitUnary(unary: Unary) {
|
||||||
|
this._validate(unary.expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
visitBinary(binary: Binary) {
|
||||||
|
this._validate(binary.left);
|
||||||
|
this._validate(binary.right);
|
||||||
|
}
|
||||||
|
|
||||||
|
visitLogical(logical: Logical) {
|
||||||
|
for (const arg of logical.args) {
|
||||||
|
this._validate(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visitGrouping(grouping: Grouping) {
|
||||||
|
this._validate(grouping.group);
|
||||||
|
}
|
||||||
|
|
||||||
|
visitContextAccess(contextAccess: ContextAccess) {
|
||||||
|
const contextName = contextAccess.name.lexeme;
|
||||||
|
if (this.context.get(contextName) === undefined) {
|
||||||
|
this.errors.push({
|
||||||
|
message: `Context access might be invalid: ${contextName}`,
|
||||||
|
range: contextAccess.name.range,
|
||||||
|
severity: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visitIndexAccess(indexAccess: IndexAccess) {
|
||||||
|
let contextAccess: ContextAccess | undefined;
|
||||||
|
|
||||||
|
const s: ExpressionData[] = [];
|
||||||
|
let i: Expr = indexAccess;
|
||||||
|
while (i) {
|
||||||
|
if (i instanceof IndexAccess) {
|
||||||
|
if (!(i.index instanceof Literal)) {
|
||||||
|
// Not a literal, validate independently
|
||||||
|
this._validate(i.index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s.push(i.index.literal);
|
||||||
|
i = i.expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i instanceof ContextAccess) {
|
||||||
|
contextAccess = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!contextAccess) {
|
||||||
|
// Context not found, should not happen, ignore in this case
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contextName = contextAccess.name.lexeme;
|
||||||
|
let contextValue = this.context.get(contextName);
|
||||||
|
if (contextValue === undefined || !(contextValue instanceof Dictionary)) {
|
||||||
|
const contextName = contextAccess.name.lexeme;
|
||||||
|
if (this.context.get(contextName) === undefined) {
|
||||||
|
this.errors.push({
|
||||||
|
message: `Context access might be invalid: ${contextName}`,
|
||||||
|
range: contextAccess.name.range,
|
||||||
|
severity: "warning"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (s.length > 0) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const idx = s.pop()!;
|
||||||
|
|
||||||
|
const key = idx.coerceString();
|
||||||
|
const v: ExpressionData | undefined = contextValue.get(key);
|
||||||
|
if (v === undefined) {
|
||||||
|
if (contextValue instanceof DescriptionDictionary && !contextValue.complete) {
|
||||||
|
// If the context dictionary is not complete, we cannot validate the expression
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.errors.push({
|
||||||
|
range: {
|
||||||
|
start: contextAccess.name.range.start,
|
||||||
|
end: (indexAccess.index as Literal).token.range.end
|
||||||
|
},
|
||||||
|
message: `Context access might be invalid: ${key}`,
|
||||||
|
severity: "warning"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(v instanceof Dictionary)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
contextValue = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visitFunctionCall(functionCall: FunctionCall) {
|
||||||
|
for (const arg of functionCall.args) {
|
||||||
|
this._validate(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,5 +3,5 @@ export {ContextProviderConfig} from "./context-providers/config";
|
|||||||
export {documentLinks} from "./document-links";
|
export {documentLinks} from "./document-links";
|
||||||
export {hover} from "./hover";
|
export {hover} from "./hover";
|
||||||
export {Logger, LogLevel, registerLogger, setLogLevel} from "./log";
|
export {Logger, LogLevel, registerLogger, setLogLevel} from "./log";
|
||||||
export {validate, ValidationConfig} from "./validate";
|
export {validate, ValidationConfig, ActionsMetadataProvider} from "./validate";
|
||||||
export {ValueProviderConfig, ValueProviderKind} from "./value-providers/config";
|
export {ValueProviderConfig, ValueProviderKind} from "./value-providers/config";
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export async function validateAction(
|
|||||||
step: Step | undefined,
|
step: Step | undefined,
|
||||||
config: ValidationConfig | undefined
|
config: ValidationConfig | undefined
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!isMapping(stepToken) || !step || !isActionStep(step) || !config?.fetchActionMetadata) {
|
if (!isMapping(stepToken) || !step || !isActionStep(step) || !config?.actionsMetadataProvider) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ export async function validateAction(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionMetadata = await config.fetchActionMetadata(action);
|
const actionMetadata = await config.actionsMetadataProvider.fetchActionMetadata(action);
|
||||||
if (actionMetadata === undefined) {
|
if (actionMetadata === undefined) {
|
||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
severity: DiagnosticSeverity.Error,
|
severity: DiagnosticSeverity.Error,
|
||||||
|
|||||||
@@ -14,75 +14,77 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const validationConfig: ValidationConfig = {
|
const validationConfig: ValidationConfig = {
|
||||||
fetchActionMetadata: (ref: ActionReference) => {
|
actionsMetadataProvider: {
|
||||||
let metadata: ActionMetadata | undefined = undefined;
|
fetchActionMetadata: (ref: ActionReference) => {
|
||||||
switch (ref.owner + "/" + ref.name + "@" + ref.ref) {
|
let metadata: ActionMetadata | undefined = undefined;
|
||||||
case "actions/checkout@v3":
|
switch (ref.owner + "/" + ref.name + "@" + ref.ref) {
|
||||||
metadata = {
|
case "actions/checkout@v3":
|
||||||
name: "Checkout",
|
metadata = {
|
||||||
description: "Checkout a Git repository at a particular version",
|
name: "Checkout",
|
||||||
inputs: {
|
description: "Checkout a Git repository at a particular version",
|
||||||
repository: {
|
inputs: {
|
||||||
description: "Repository name with owner",
|
repository: {
|
||||||
default: "${{ github.repository }}"
|
description: "Repository name with owner",
|
||||||
|
default: "${{ github.repository }}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
break;
|
||||||
break;
|
case "actions/setup-node@v1":
|
||||||
case "actions/setup-node@v1":
|
metadata = {
|
||||||
metadata = {
|
name: "Setup Node.js environment",
|
||||||
name: "Setup Node.js environment",
|
description:
|
||||||
description:
|
"Setup a Node.js environment by adding problem matchers and optionally downloading and adding it to the PATH.",
|
||||||
"Setup a Node.js environment by adding problem matchers and optionally downloading and adding it to the PATH.",
|
inputs: {
|
||||||
inputs: {
|
version: {
|
||||||
version: {
|
description: "Deprecated. Use node-version instead. Will not be supported after October 1, 2019",
|
||||||
description: "Deprecated. Use node-version instead. Will not be supported after October 1, 2019",
|
deprecationMessage:
|
||||||
deprecationMessage:
|
"The version property will not be supported after October 1, 2019. Use node-version instead"
|
||||||
"The version property will not be supported after October 1, 2019. Use node-version instead"
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
break;
|
||||||
break;
|
case "actions/deploy-pages@main":
|
||||||
case "actions/deploy-pages@main":
|
metadata = {
|
||||||
metadata = {
|
name: "Deploy GitHub Pages site",
|
||||||
name: "Deploy GitHub Pages site",
|
description: "A GitHub Action to deploy an artifact as a GitHub Pages site",
|
||||||
description: "A GitHub Action to deploy an artifact as a GitHub Pages site",
|
inputs: {
|
||||||
inputs: {
|
token: {
|
||||||
token: {
|
required: true,
|
||||||
required: true,
|
description: "token to use",
|
||||||
description: "token to use",
|
default: "${{ github.token }}"
|
||||||
default: "${{ github.token }}"
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
break;
|
||||||
break;
|
case "actions/cache@v1":
|
||||||
case "actions/cache@v1":
|
metadata = {
|
||||||
metadata = {
|
name: "Cache",
|
||||||
name: "Cache",
|
description: "Cache artifacts like dependencies and build outputs to improve workflow execution time",
|
||||||
description: "Cache artifacts like dependencies and build outputs to improve workflow execution time",
|
inputs: {
|
||||||
inputs: {
|
path: {
|
||||||
path: {
|
description: "A directory to store and save the cache",
|
||||||
description: "A directory to store and save the cache",
|
required: true
|
||||||
required: true
|
},
|
||||||
},
|
key: {
|
||||||
key: {
|
description: "An explicit key for restoring and saving the cache",
|
||||||
description: "An explicit key for restoring and saving the cache",
|
required: true
|
||||||
required: true
|
},
|
||||||
},
|
"restore-keys": {
|
||||||
"restore-keys": {
|
description: "An ordered list of keys to use for restoring the cache if no cache hit occurred for key",
|
||||||
description: "An ordered list of keys to use for restoring the cache if no cache hit occurred for key",
|
required: false
|
||||||
required: false
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
break;
|
||||||
break;
|
case "actions/action-no-input@v1":
|
||||||
case "actions/action-no-input@v1":
|
metadata = {
|
||||||
metadata = {
|
name: "Action with no inputs",
|
||||||
name: "Action with no inputs",
|
description: "An action with no inputs"
|
||||||
description: "An action with no inputs"
|
};
|
||||||
};
|
}
|
||||||
|
return Promise.resolve(metadata);
|
||||||
}
|
}
|
||||||
return Promise.resolve(metadata);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -101,6 +103,21 @@ jobs:
|
|||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("no actionsMetadataProvider", async () => {
|
||||||
|
const input = `
|
||||||
|
on: push
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/does-not-exist@v3
|
||||||
|
`;
|
||||||
|
const config: ValidationConfig = {};
|
||||||
|
const result = await validate(createDocument("wf.yaml", input), config);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it("action does not exist", async () => {
|
it("action does not exist", async () => {
|
||||||
const input = `
|
const input = `
|
||||||
on: push
|
on: push
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {DescriptionDictionary} from "@actions/expressions/.";
|
import {DescriptionDictionary} from "@actions/expressions";
|
||||||
import {DiagnosticSeverity} from "vscode-languageserver-types";
|
import {DiagnosticSeverity} from "vscode-languageserver-types";
|
||||||
import {ContextProviderConfig} from "./context-providers/config";
|
import {ContextProviderConfig} from "./context-providers/config";
|
||||||
import {registerLogger} from "./log";
|
import {registerLogger} from "./log";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {Evaluator, ExpressionEvaluationError, Lexer, Parser} from "@actions/expressions";
|
import {Lexer, Parser} from "@actions/expressions";
|
||||||
import {Expr} from "@actions/expressions/ast";
|
import {Expr} from "@actions/expressions/ast";
|
||||||
import {isBasicExpression, isString, ParseWorkflowResult, WorkflowTemplate} from "@actions/workflow-parser";
|
import {ParseWorkflowResult, WorkflowTemplate, isBasicExpression, isString} from "@actions/workflow-parser";
|
||||||
import {ErrorPolicy} from "@actions/workflow-parser/model/convert";
|
import {ErrorPolicy} from "@actions/workflow-parser/model/convert";
|
||||||
import {splitAllowedContext} from "@actions/workflow-parser/templates/allowed-context";
|
import {splitAllowedContext} from "@actions/workflow-parser/templates/allowed-context";
|
||||||
import {BasicExpressionToken} from "@actions/workflow-parser/templates/tokens/basic-expression-token";
|
import {BasicExpressionToken} from "@actions/workflow-parser/templates/tokens/basic-expression-token";
|
||||||
@@ -13,10 +13,9 @@ import {TextDocument} from "vscode-languageserver-textdocument";
|
|||||||
import {Diagnostic, DiagnosticSeverity, URI} from "vscode-languageserver-types";
|
import {Diagnostic, DiagnosticSeverity, URI} from "vscode-languageserver-types";
|
||||||
import {ActionMetadata, ActionReference} from "./action";
|
import {ActionMetadata, ActionReference} from "./action";
|
||||||
import {ContextProviderConfig} from "./context-providers/config";
|
import {ContextProviderConfig} from "./context-providers/config";
|
||||||
import {getContext, Mode} from "./context-providers/default";
|
import {Mode, getContext} from "./context-providers/default";
|
||||||
import {getWorkflowContext, WorkflowContext} from "./context/workflow-context";
|
import {WorkflowContext, getWorkflowContext} from "./context/workflow-context";
|
||||||
import {AccessError, wrapDictionary} from "./expression-validation/error-dictionary";
|
import {ValidationVisitor} from "./expression-validation/visitor";
|
||||||
import {validatorFunctions} from "./expression-validation/functions";
|
|
||||||
import {error} from "./log";
|
import {error} from "./log";
|
||||||
import {findToken} from "./utils/find-token";
|
import {findToken} from "./utils/find-token";
|
||||||
import {mapRange} from "./utils/range";
|
import {mapRange} from "./utils/range";
|
||||||
@@ -28,10 +27,14 @@ import {defaultValueProviders} from "./value-providers/default";
|
|||||||
export type ValidationConfig = {
|
export type ValidationConfig = {
|
||||||
valueProviderConfig?: ValueProviderConfig;
|
valueProviderConfig?: ValueProviderConfig;
|
||||||
contextProviderConfig?: ContextProviderConfig;
|
contextProviderConfig?: ContextProviderConfig;
|
||||||
fetchActionMetadata?(action: ActionReference): Promise<ActionMetadata | undefined>;
|
actionsMetadataProvider?: ActionsMetadataProvider;
|
||||||
fileProvider?: FileProvider;
|
fileProvider?: FileProvider;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ActionsMetadataProvider = {
|
||||||
|
fetchActionMetadata(action: ActionReference): Promise<ActionMetadata | undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates a workflow file
|
* Validates a workflow file
|
||||||
*
|
*
|
||||||
@@ -198,28 +201,17 @@ async function validateExpression(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const context = await getContext(namedContexts, contextProviderConfig, workflowContext, Mode.Validation);
|
||||||
const context = await getContext(namedContexts, contextProviderConfig, workflowContext, Mode.Validation);
|
|
||||||
|
|
||||||
const e = new Evaluator(expr, wrapDictionary(context), validatorFunctions);
|
const e = new ValidationVisitor(expr, context);
|
||||||
e.evaluate();
|
e.validate();
|
||||||
|
|
||||||
// Any invalid context access would've thrown an error via the `ErrorDictionary`, for now we don't have to check the actual
|
diagnostics.push(
|
||||||
// result of the evaluation.
|
...e.errors.map(e => ({
|
||||||
} catch (e) {
|
message: e.message,
|
||||||
if (e instanceof AccessError) {
|
range: mapRange(expression.range),
|
||||||
diagnostics.push({
|
severity: e.severity === "error" ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning
|
||||||
message: `Context access might be invalid: ${e.keyName}`,
|
}))
|
||||||
severity: DiagnosticSeverity.Warning,
|
);
|
||||||
range: mapRange(expression.range)
|
|
||||||
});
|
|
||||||
} else if (e instanceof ExpressionEvaluationError) {
|
|
||||||
diagnostics.push({
|
|
||||||
message: `Expression might be invalid: ${e.message}`,
|
|
||||||
severity: DiagnosticSeverity.Error,
|
|
||||||
range: mapRange(expression.range)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||||
"useWorkspaces": true,
|
"useWorkspaces": true,
|
||||||
"version": "0.1.2"
|
"version": "0.3.1"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+54
-2292
File diff suppressed because it is too large
Load Diff
+1
-2
@@ -5,8 +5,7 @@
|
|||||||
"./expressions",
|
"./expressions",
|
||||||
"./workflow-parser",
|
"./workflow-parser",
|
||||||
"./languageservice",
|
"./languageservice",
|
||||||
"./languageserver",
|
"./languageserver"
|
||||||
"./browser-playground"
|
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"lerna": "^6.0.3"
|
"lerna": "^6.0.3"
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@actions/workflow-parser",
|
"name": "@actions/workflow-parser",
|
||||||
"version": "0.1.2",
|
"version": "0.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"source": "./src/index.ts",
|
"source": "./src/index.ts",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"import": "./dist/index.js"
|
"import": "./dist/index.js"
|
||||||
@@ -40,7 +43,7 @@
|
|||||||
"watch": "tsc --build tsconfig.build.json --watch"
|
"watch": "tsc --build tsconfig.build.json --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/expressions": "^0.1.2",
|
"@actions/expressions": "^0.3.1",
|
||||||
"cronstrue": "^2.21.0",
|
"cronstrue": "^2.21.0",
|
||||||
"yaml": "^2.0.0-8"
|
"yaml": "^2.0.0-8"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user