Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ee228549a | |||
| 45a665690b | |||
| 0a9f420c96 |
+1
-1
@@ -1 +1 @@
|
|||||||
* @actions/actions-workflow-development-reviewers
|
* @actions/actions-experience
|
||||||
|
|||||||
+2
-10
@@ -8,8 +8,6 @@ Hi there! We're thrilled that you'd like to contribute to this project. Your hel
|
|||||||
|
|
||||||
We accept pull requests for bug fixes and features where we've discussed the approach in an issue and given the go-ahead for a community member to work on it. We'd also love to hear about ideas for new features as issues.
|
We accept pull requests for bug fixes and features where we've discussed the approach in an issue and given the go-ahead for a community member to work on it. We'd also love to hear about ideas for new features as issues.
|
||||||
|
|
||||||
We track issues on our project board [here](https://github.com/orgs/github/projects/9557/views/1).
|
|
||||||
|
|
||||||
Please do:
|
Please do:
|
||||||
|
|
||||||
* Check existing issues to verify that the [bug][bug issues] or [feature request][feature request issues] has not already been submitted.
|
* Check existing issues to verify that the [bug][bug issues] or [feature request][feature request issues] has not already been submitted.
|
||||||
@@ -23,7 +21,7 @@ Please avoid:
|
|||||||
|
|
||||||
* Opening pull requests for issues marked `needs-design`, `needs-investigation`, or `blocked`.
|
* Opening pull requests for issues marked `needs-design`, `needs-investigation`, or `blocked`.
|
||||||
|
|
||||||
Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE).
|
Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE.md).
|
||||||
|
|
||||||
Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms.
|
Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms.
|
||||||
|
|
||||||
@@ -62,10 +60,4 @@ Please also look at the `README.md` files for each package for additional notes
|
|||||||
|
|
||||||
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
|
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
|
||||||
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
|
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
|
||||||
- [GitHub Help](https://help.github.com)
|
- [GitHub Help](https://help.github.com)
|
||||||
|
|
||||||
|
|
||||||
[bug issues]: https://github.com/actions/languageservices/labels/bug
|
|
||||||
[feature request issues]: https://github.com/actions/languageservices/labels/enhancement
|
|
||||||
[hw]: https://github.com/actions/languageservices/labels/help%20wanted
|
|
||||||
[gfi]: https://github.com/actions/languageservices/labels/good%20first%20issue
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@actions/expressions",
|
"name": "@actions/expressions",
|
||||||
"version": "0.3.3",
|
"version": "0.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"source": "./src/index.ts",
|
"source": "./src/index.ts",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export class Evaluator implements ExprVisitor<data.ExpressionData> {
|
|||||||
return this.eval(this.n);
|
return this.eval(this.n);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected eval(n: Expr): data.ExpressionData {
|
private eval(n: Expr): data.ExpressionData {
|
||||||
return n.accept(this);
|
return n.accept(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@actions/languageserver",
|
"name": "@actions/languageserver",
|
||||||
"version": "0.3.3",
|
"version": "0.3.1",
|
||||||
"description": "Language server for GitHub Actions",
|
"description": "Language server for GitHub Actions",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -43,8 +43,8 @@
|
|||||||
"watch": "tsc --build tsconfig.build.json --watch"
|
"watch": "tsc --build tsconfig.build.json --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/languageservice": "^0.3.3",
|
"@actions/languageservice": "^0.3.1",
|
||||||
"@actions/workflow-parser": "^0.3.3",
|
"@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",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@actions/languageservice",
|
"name": "@actions/languageservice",
|
||||||
"version": "0.3.3",
|
"version": "0.3.1",
|
||||||
"description": "Language service for GitHub Actions",
|
"description": "Language service for GitHub Actions",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -44,8 +44,8 @@
|
|||||||
"watch": "tsc --build tsconfig.build.json --watch"
|
"watch": "tsc --build tsconfig.build.json --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/expressions": "^0.3.3",
|
"@actions/expressions": "^0.3.1",
|
||||||
"@actions/workflow-parser": "^0.3.3",
|
"@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",
|
||||||
|
|||||||
@@ -474,15 +474,4 @@ jobs:
|
|||||||
expect(result.filter(x => x.label === "concurrency").map(x => x.textEdit?.newText)).toEqual(["concurrency"]);
|
expect(result.filter(x => x.label === "concurrency").map(x => x.textEdit?.newText)).toEqual(["concurrency"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds a new line and indentation for mapping keys", async () => {
|
|
||||||
const input = "concurrency: |";
|
|
||||||
|
|
||||||
const result = await complete(...getPositionFromCursor(input));
|
|
||||||
|
|
||||||
expect(result.filter(x => x.label === "cancel-in-progress").map(x => x.textEdit?.newText)).toEqual([
|
|
||||||
"\n cancel-in-progress: "
|
|
||||||
]);
|
|
||||||
expect(result.filter(x => x.label === "group").map(x => x.textEdit?.newText)).toEqual(["\n group: "]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import {Evaluator, ExpressionEvaluationError, data} from "@actions/expressions";
|
|
||||||
import {Expr, Logical} from "@actions/expressions/ast";
|
|
||||||
import {ExpressionData} from "@actions/expressions/data/expressiondata";
|
|
||||||
import {TokenType} from "@actions/expressions/lexer";
|
|
||||||
import {falsy, truthy} from "@actions/expressions/result";
|
|
||||||
import {AccessError} from "./error-dictionary";
|
|
||||||
|
|
||||||
export type ValidationError = {
|
|
||||||
message: string;
|
|
||||||
severity: "error" | "warning";
|
|
||||||
};
|
|
||||||
|
|
||||||
export class ValidationEvaluator extends Evaluator {
|
|
||||||
public readonly errors: ValidationError[] = [];
|
|
||||||
|
|
||||||
public validate() {
|
|
||||||
super.evaluate();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override eval(n: Expr): ExpressionData {
|
|
||||||
try {
|
|
||||||
return super.eval(n);
|
|
||||||
} catch (e) {
|
|
||||||
// Record error
|
|
||||||
if (e instanceof AccessError) {
|
|
||||||
this.errors.push({
|
|
||||||
message: `Context access might be invalid: ${e.keyName}`,
|
|
||||||
severity: "warning"
|
|
||||||
});
|
|
||||||
} else if (e instanceof ExpressionEvaluationError) {
|
|
||||||
this.errors.push({
|
|
||||||
message: `Expression might be invalid: ${e.message}`,
|
|
||||||
severity: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return null but continue with the validation
|
|
||||||
return new data.Null();
|
|
||||||
}
|
|
||||||
|
|
||||||
override visitLogical(logical: Logical): ExpressionData {
|
|
||||||
let result: data.ExpressionData | undefined;
|
|
||||||
|
|
||||||
for (const arg of logical.args) {
|
|
||||||
const r = this.eval(arg);
|
|
||||||
|
|
||||||
// Simulate short-circuit behavior but continue to evalute all arguments for validation purposes
|
|
||||||
if (
|
|
||||||
!result &&
|
|
||||||
((logical.operator.type === TokenType.AND && falsy(r)) || (logical.operator.type === TokenType.OR && truthy(r)))
|
|
||||||
) {
|
|
||||||
result = r;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// result is always assigned before we return here
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
return result!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
@@ -46,52 +46,6 @@ jobs:
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("access invalid context field in short-circuited expression", async () => {
|
|
||||||
const result = await validate(
|
|
||||||
createDocument(
|
|
||||||
"wf.yaml",
|
|
||||||
`on: push
|
|
||||||
run-name: name-\${{ github.does-not-exist || github.does-not-exist2 }}
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- run: echo`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqual([
|
|
||||||
{
|
|
||||||
message: "Context access might be invalid: does-not-exist",
|
|
||||||
range: {
|
|
||||||
end: {
|
|
||||||
character: 69,
|
|
||||||
line: 1
|
|
||||||
},
|
|
||||||
start: {
|
|
||||||
character: 15,
|
|
||||||
line: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
severity: DiagnosticSeverity.Warning
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "Context access might be invalid: does-not-exist2",
|
|
||||||
range: {
|
|
||||||
end: {
|
|
||||||
character: 69,
|
|
||||||
line: 1
|
|
||||||
},
|
|
||||||
start: {
|
|
||||||
character: 15,
|
|
||||||
line: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
severity: DiagnosticSeverity.Warning
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("partial skip access invalid context on incomplete", async () => {
|
it("partial skip access invalid context on incomplete", async () => {
|
||||||
const contextProviderConfig: ContextProviderConfig = {
|
const contextProviderConfig: ContextProviderConfig = {
|
||||||
getContext: (context: string) => {
|
getContext: (context: string) => {
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ import {ActionMetadata, ActionReference} from "./action";
|
|||||||
import {ContextProviderConfig} from "./context-providers/config";
|
import {ContextProviderConfig} from "./context-providers/config";
|
||||||
import {Mode, getContext} from "./context-providers/default";
|
import {Mode, getContext} from "./context-providers/default";
|
||||||
import {WorkflowContext, getWorkflowContext} from "./context/workflow-context";
|
import {WorkflowContext, getWorkflowContext} from "./context/workflow-context";
|
||||||
import {wrapDictionary} from "./expression-validation/error-dictionary";
|
import {ValidationVisitor} from "./expression-validation/visitor";
|
||||||
import {ValidationEvaluator} from "./expression-validation/evaluator";
|
|
||||||
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";
|
||||||
@@ -205,7 +203,7 @@ async function validateExpression(
|
|||||||
|
|
||||||
const context = await getContext(namedContexts, contextProviderConfig, workflowContext, Mode.Validation);
|
const context = await getContext(namedContexts, contextProviderConfig, workflowContext, Mode.Validation);
|
||||||
|
|
||||||
const e = new ValidationEvaluator(expr, wrapDictionary(context), validatorFunctions);
|
const e = new ValidationVisitor(expr, context);
|
||||||
e.validate();
|
e.validate();
|
||||||
|
|
||||||
diagnostics.push(
|
diagnostics.push(
|
||||||
|
|||||||
@@ -71,10 +71,6 @@ function mappingValues(
|
|||||||
// No special insertText in this case
|
// No special insertText in this case
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DefinitionType.String:
|
|
||||||
case DefinitionType.Boolean:
|
|
||||||
insertText = `\n${indentation}${key}: `;
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
insertText = `${key}: `;
|
insertText = `${key}: `;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
## This script syncs all five repositories to the current state of main.
|
|
||||||
## It will stash changes on the current branch, switch to main, pull and remain on main.
|
|
||||||
|
|
||||||
echo "Syncing all repositories to main"
|
|
||||||
|
|
||||||
# for each folder in the above directory
|
|
||||||
cd ..
|
|
||||||
for d in */ ; do
|
|
||||||
cd $d
|
|
||||||
echo "Syncing $d"
|
|
||||||
echo "current branch: $(git rev-parse --abbrev-ref HEAD)"
|
|
||||||
git stash
|
|
||||||
git checkout main
|
|
||||||
git pull
|
|
||||||
cd ..
|
|
||||||
done
|
|
||||||
+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.3.3"
|
"version": "0.3.1"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+12
-13
@@ -135,7 +135,7 @@
|
|||||||
},
|
},
|
||||||
"expressions": {
|
"expressions": {
|
||||||
"name": "@actions/expressions",
|
"name": "@actions/expressions",
|
||||||
"version": "0.3.3",
|
"version": "0.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.0.3",
|
"@types/jest": "^29.0.3",
|
||||||
@@ -395,11 +395,11 @@
|
|||||||
},
|
},
|
||||||
"languageserver": {
|
"languageserver": {
|
||||||
"name": "@actions/languageserver",
|
"name": "@actions/languageserver",
|
||||||
"version": "0.3.3",
|
"version": "0.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/languageservice": "^0.3.3",
|
"@actions/languageservice": "^0.3.1",
|
||||||
"@actions/workflow-parser": "^0.3.3",
|
"@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",
|
||||||
@@ -678,11 +678,11 @@
|
|||||||
},
|
},
|
||||||
"languageservice": {
|
"languageservice": {
|
||||||
"name": "@actions/languageservice",
|
"name": "@actions/languageservice",
|
||||||
"version": "0.3.3",
|
"version": "0.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/expressions": "^0.3.3",
|
"@actions/expressions": "^0.3.1",
|
||||||
"@actions/workflow-parser": "^0.3.3",
|
"@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",
|
||||||
@@ -6652,10 +6652,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/http-cache-semantics": {
|
"node_modules/http-cache-semantics": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
"dev": true,
|
||||||
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
|
"license": "BSD-2-Clause"
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/http-proxy-agent": {
|
"node_modules/http-proxy-agent": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
@@ -11720,10 +11719,10 @@
|
|||||||
},
|
},
|
||||||
"workflow-parser": {
|
"workflow-parser": {
|
||||||
"name": "@actions/workflow-parser",
|
"name": "@actions/workflow-parser",
|
||||||
"version": "0.3.3",
|
"version": "0.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/expressions": "^0.3.3",
|
"@actions/expressions": "^0.3.1",
|
||||||
"cronstrue": "^2.21.0",
|
"cronstrue": "^2.21.0",
|
||||||
"yaml": "^2.0.0-8"
|
"yaml": "^2.0.0-8"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@actions/workflow-parser",
|
"name": "@actions/workflow-parser",
|
||||||
"version": "0.3.3",
|
"version": "0.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"source": "./src/index.ts",
|
"source": "./src/index.ts",
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
"watch": "tsc --build tsconfig.build.json --watch"
|
"watch": "tsc --build tsconfig.build.json --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/expressions": "^0.3.3",
|
"@actions/expressions": "^0.3.1",
|
||||||
"cronstrue": "^2.21.0",
|
"cronstrue": "^2.21.0",
|
||||||
"yaml": "^2.0.0-8"
|
"yaml": "^2.0.0-8"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -59,24 +59,17 @@ export function convertOn(context: TemplateContext, token: TemplateToken): Event
|
|||||||
|
|
||||||
// All other events are defined as mappings. During schema validation we already ensure that events
|
// All other events are defined as mappings. During schema validation we already ensure that events
|
||||||
// receive only known keys, so here we can focus on the values and whether they are valid.
|
// receive only known keys, so here we can focus on the values and whether they are valid.
|
||||||
|
|
||||||
const eventToken = item.value.assertMapping(`event ${eventName}`);
|
const eventToken = item.value.assertMapping(`event ${eventName}`);
|
||||||
if (eventName === "workflow_call") {
|
|
||||||
result.workflow_call = convertEventWorkflowCall(context, eventToken);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventName === "workflow_dispatch") {
|
|
||||||
result.workflow_dispatch = convertEventWorkflowDispatchInputs(context, eventToken);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
result[eventName] = {
|
result[eventName] = {
|
||||||
...convertPatternFilter("branches", eventToken),
|
...convertPatternFilter("branches", eventToken),
|
||||||
...convertPatternFilter("tags", eventToken),
|
...convertPatternFilter("tags", eventToken),
|
||||||
...convertPatternFilter("paths", eventToken),
|
...convertPatternFilter("paths", eventToken),
|
||||||
...convertFilter("types", eventToken),
|
...convertFilter("types", eventToken),
|
||||||
...convertFilter("workflows", eventToken)
|
...convertFilter("workflows", eventToken),
|
||||||
|
// workflow_call and workflow_dispatch share input parsing
|
||||||
|
...convertEventWorkflowDispatchInputs(context, eventToken),
|
||||||
|
...convertEventWorkflowCall(context, eventToken)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ function convertStep(context: TemplateContext, idBuilder: IdBuilder, step: Templ
|
|||||||
let id: StringToken | undefined;
|
let id: StringToken | undefined;
|
||||||
let name: ScalarToken | undefined;
|
let name: ScalarToken | undefined;
|
||||||
let uses: StringToken | undefined;
|
let uses: StringToken | undefined;
|
||||||
let continueOnError: boolean | ScalarToken | undefined;
|
let continueOnError: boolean | undefined;
|
||||||
let env: MappingToken | undefined;
|
let env: MappingToken | undefined;
|
||||||
const ifCondition = new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined);
|
const ifCondition = new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined);
|
||||||
for (const item of mapping) {
|
for (const item of mapping) {
|
||||||
@@ -78,11 +78,7 @@ function convertStep(context: TemplateContext, idBuilder: IdBuilder, step: Templ
|
|||||||
env = item.value.assertMapping("step env");
|
env = item.value.assertMapping("step env");
|
||||||
break;
|
break;
|
||||||
case "continue-on-error":
|
case "continue-on-error":
|
||||||
if (!item.value.isExpression) {
|
continueOnError = item.value.assertBoolean("steps item continue-on-error").value;
|
||||||
continueOnError = item.value.assertBoolean("steps item continue-on-error").value;
|
|
||||||
} else {
|
|
||||||
continueOnError = item.value.assertScalar("steps item continue-on-error");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import {TemplateContext} from "../../templates/template-context";
|
import {TemplateContext} from "../../templates/template-context";
|
||||||
import {MappingToken, TemplateToken} from "../../templates/tokens";
|
import {MappingToken, TemplateToken} from "../../templates/tokens";
|
||||||
import {isMapping} from "../../templates/tokens/type-guards";
|
import {isMapping} from "../../templates/tokens/type-guards";
|
||||||
import {SecretConfig, WorkflowCallConfig, InputConfig, InputType} from "../workflow-template";
|
import {SecretConfig, WorkflowCallConfig} from "../workflow-template";
|
||||||
import {convertStringList} from "./string-list";
|
|
||||||
import {ScalarToken} from "../../templates/tokens/scalar-token";
|
|
||||||
|
|
||||||
export function convertEventWorkflowCall(context: TemplateContext, token: MappingToken): WorkflowCallConfig {
|
export function convertEventWorkflowCall(context: TemplateContext, token: MappingToken): WorkflowCallConfig {
|
||||||
const result: WorkflowCallConfig = {};
|
const result: WorkflowCallConfig = {};
|
||||||
@@ -13,7 +11,7 @@ export function convertEventWorkflowCall(context: TemplateContext, token: Mappin
|
|||||||
|
|
||||||
switch (key.value) {
|
switch (key.value) {
|
||||||
case "inputs":
|
case "inputs":
|
||||||
result.inputs = convertWorkflowInputs(context, item.value.assertMapping("workflow dispatch inputs"));
|
// Ignore, these are handled by convertEventWorkflowDispatchInputs
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "secrets":
|
case "secrets":
|
||||||
@@ -29,94 +27,6 @@ export function convertEventWorkflowCall(context: TemplateContext, token: Mappin
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertWorkflowInputs(
|
|
||||||
context: TemplateContext,
|
|
||||||
token: MappingToken
|
|
||||||
): {
|
|
||||||
[inputName: string]: InputConfig;
|
|
||||||
} {
|
|
||||||
const result: {[inputName: string]: InputConfig} = {};
|
|
||||||
|
|
||||||
for (const item of token) {
|
|
||||||
const inputName = item.key.assertString("input name");
|
|
||||||
const inputMapping = item.value.assertMapping("input configuration");
|
|
||||||
|
|
||||||
result[inputName.value] = convertWorkflowInput(context, inputMapping);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function convertWorkflowInput(context: TemplateContext, token: MappingToken): InputConfig {
|
|
||||||
const result: InputConfig = {
|
|
||||||
type: InputType.string // Default to string
|
|
||||||
};
|
|
||||||
|
|
||||||
let defaultValue: undefined | ScalarToken;
|
|
||||||
|
|
||||||
for (const item of token) {
|
|
||||||
const key = item.key.assertString("workflow dispatch input key");
|
|
||||||
|
|
||||||
switch (key.value) {
|
|
||||||
case "description":
|
|
||||||
result.description = item.value.assertString("input description").value;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "required":
|
|
||||||
result.required = item.value.assertBoolean("input required").value;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "default":
|
|
||||||
defaultValue = item.value.assertScalar("input default");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "type":
|
|
||||||
result.type = InputType[item.value.assertString("input type").value as keyof typeof InputType];
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "options":
|
|
||||||
result.options = convertStringList("input options", item.value.assertSequence("input options"));
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
context.error(item.key, `Invalid key '${key.value}'`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate default value
|
|
||||||
if (defaultValue !== undefined && !defaultValue.isExpression) {
|
|
||||||
try {
|
|
||||||
switch (result.type) {
|
|
||||||
case InputType.boolean:
|
|
||||||
result.default = defaultValue.assertBoolean("input default").value;
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case InputType.string:
|
|
||||||
case InputType.choice:
|
|
||||||
case InputType.environment:
|
|
||||||
result.default = defaultValue.assertString("input default").value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
context.error(defaultValue, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate `options` for `choice` type
|
|
||||||
if (result.type === InputType.choice) {
|
|
||||||
if (result.options === undefined || result.options.length === 0) {
|
|
||||||
context.error(token, "Missing 'options' for choice input");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (result.options !== undefined) {
|
|
||||||
context.error(token, "Input type is not 'choice', but 'options' is defined");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertWorkflowCallSecrets(
|
function convertWorkflowCallSecrets(
|
||||||
context: TemplateContext,
|
context: TemplateContext,
|
||||||
token: MappingToken
|
token: MappingToken
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ type BaseStep = {
|
|||||||
id: string;
|
id: string;
|
||||||
name?: ScalarToken;
|
name?: ScalarToken;
|
||||||
if: BasicExpressionToken;
|
if: BasicExpressionToken;
|
||||||
"continue-on-error"?: boolean | ScalarToken;
|
"continue-on-error"?: boolean;
|
||||||
env?: MappingToken;
|
env?: MappingToken;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ export type WorkflowDispatchConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type WorkflowCallConfig = {
|
export type WorkflowCallConfig = {
|
||||||
inputs?: {[inputName: string]: InputConfig & {default?: string | boolean | number | ScalarToken}};
|
inputs?: {[inputName: string]: InputConfig};
|
||||||
secrets?: {[secretName: string]: SecretConfig};
|
secrets?: {[secretName: string]: SecretConfig};
|
||||||
// TODO - these are supported in C# and Go but not in TS yet
|
// TODO - these are supported in C# and Go but not in TS yet
|
||||||
// outputs: { [outputName: string]: OutputConfig }
|
// outputs: { [outputName: string]: OutputConfig }
|
||||||
|
|||||||
@@ -576,8 +576,7 @@
|
|||||||
"merge-group-mapping": {
|
"merge-group-mapping": {
|
||||||
"mapping": {
|
"mapping": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"types": "merge-group-activity",
|
"types": "merge-group-activity"
|
||||||
"branches": "event-branches"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-9
@@ -72,11 +72,7 @@ on:
|
|||||||
- edited
|
- edited
|
||||||
- deleted
|
- deleted
|
||||||
merge_group:
|
merge_group:
|
||||||
branches:
|
types: checks_requested
|
||||||
- master
|
|
||||||
- main
|
|
||||||
types:
|
|
||||||
- checks_requested
|
|
||||||
milestone:
|
milestone:
|
||||||
types:
|
types:
|
||||||
- created
|
- created
|
||||||
@@ -317,10 +313,6 @@ jobs:
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"merge_group": {
|
"merge_group": {
|
||||||
"branches": [
|
|
||||||
"master",
|
|
||||||
"main"
|
|
||||||
],
|
|
||||||
"types": [
|
"types": [
|
||||||
"checks_requested"
|
"checks_requested"
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user