Merge branch 'main' into elbrenn/expression-complete
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
dist
|
||||
*.md
|
||||
*.js
|
||||
*.json
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@github/actions-expressions",
|
||||
"version": "0.1.113",
|
||||
"version": "0.1.126",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"source": "./src/index.ts",
|
||||
|
||||
@@ -17,7 +17,7 @@ export abstract class Expr {
|
||||
}
|
||||
|
||||
export class Literal extends Expr {
|
||||
constructor(public literal: ExpressionData) {
|
||||
constructor(public literal: ExpressionData, public token: Token) {
|
||||
super();
|
||||
}
|
||||
|
||||
|
||||
@@ -34,4 +34,8 @@ export class DescriptionDictionary extends Dictionary {
|
||||
const pairs = super.pairs();
|
||||
return pairs.map(p => ({...p, description: this.descriptions.get(p.key)}));
|
||||
}
|
||||
|
||||
getDescription(key: string): string | undefined {
|
||||
return this.descriptions.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export class ExpressionError extends Error {
|
||||
constructor(private typ: ErrorType, private tok: Token) {
|
||||
super(`${errorDescription(typ)}: '${tokenString(tok)}'`);
|
||||
|
||||
this.pos = this.tok.pos;
|
||||
this.pos = this.tok.range.start;
|
||||
}
|
||||
|
||||
public pos: Pos;
|
||||
|
||||
@@ -3,32 +3,32 @@ import {Lexer, Token, TokenType} from "./lexer";
|
||||
describe("lexer", () => {
|
||||
const tests: {
|
||||
input: string;
|
||||
tokenType: TokenType[];
|
||||
token?: Token;
|
||||
tokenTypes: TokenType[];
|
||||
tokens?: Token[];
|
||||
}[] = [
|
||||
{input: "<", tokenType: [TokenType.LESS]},
|
||||
{input: ">", tokenType: [TokenType.GREATER]},
|
||||
{input: "<", tokenTypes: [TokenType.LESS]},
|
||||
{input: ">", tokenTypes: [TokenType.GREATER]},
|
||||
|
||||
{input: "!=", tokenType: [TokenType.BANG_EQUAL]},
|
||||
{input: "==", tokenType: [TokenType.EQUAL_EQUAL]},
|
||||
{input: "<=", tokenType: [TokenType.LESS_EQUAL]},
|
||||
{input: ">=", tokenType: [TokenType.GREATER_EQUAL]},
|
||||
{input: "!=", tokenTypes: [TokenType.BANG_EQUAL]},
|
||||
{input: "==", tokenTypes: [TokenType.EQUAL_EQUAL]},
|
||||
{input: "<=", tokenTypes: [TokenType.LESS_EQUAL]},
|
||||
{input: ">=", tokenTypes: [TokenType.GREATER_EQUAL]},
|
||||
|
||||
{input: "&&", tokenType: [TokenType.AND]},
|
||||
{input: "||", tokenType: [TokenType.OR]},
|
||||
{input: "&&", tokenTypes: [TokenType.AND]},
|
||||
{input: "||", tokenTypes: [TokenType.OR]},
|
||||
|
||||
// Numbers
|
||||
{input: "12", tokenType: [TokenType.NUMBER]},
|
||||
{input: "12.0", tokenType: [TokenType.NUMBER]},
|
||||
{input: "0", tokenType: [TokenType.NUMBER]},
|
||||
{input: "-0", tokenType: [TokenType.NUMBER]},
|
||||
{input: "-12.0", tokenType: [TokenType.NUMBER]},
|
||||
{input: "12", tokenTypes: [TokenType.NUMBER]},
|
||||
{input: "12.0", tokenTypes: [TokenType.NUMBER]},
|
||||
{input: "0", tokenTypes: [TokenType.NUMBER]},
|
||||
{input: "-0", tokenTypes: [TokenType.NUMBER]},
|
||||
{input: "-12.0", tokenTypes: [TokenType.NUMBER]},
|
||||
|
||||
// Strings
|
||||
{input: "'It''s okay'", tokenType: [TokenType.STRING]},
|
||||
{input: "'It''s okay'", tokenTypes: [TokenType.STRING]},
|
||||
{
|
||||
input: "format('{0} == ''queued''', needs)",
|
||||
tokenType: [
|
||||
tokenTypes: [
|
||||
TokenType.IDENTIFIER,
|
||||
TokenType.LEFT_PAREN,
|
||||
TokenType.STRING,
|
||||
@@ -41,87 +41,161 @@ describe("lexer", () => {
|
||||
// Arrays
|
||||
{
|
||||
input: "[1,2]",
|
||||
tokenType: [TokenType.LEFT_BRACKET, TokenType.NUMBER, TokenType.COMMA, TokenType.NUMBER, TokenType.RIGHT_BRACKET]
|
||||
tokenTypes: [TokenType.LEFT_BRACKET, TokenType.NUMBER, TokenType.COMMA, TokenType.NUMBER, TokenType.RIGHT_BRACKET]
|
||||
},
|
||||
|
||||
// Simple expressions
|
||||
{
|
||||
input: "1 == 2",
|
||||
tokenType: [TokenType.NUMBER, TokenType.EQUAL_EQUAL, TokenType.NUMBER]
|
||||
tokenTypes: [TokenType.NUMBER, TokenType.EQUAL_EQUAL, TokenType.NUMBER]
|
||||
},
|
||||
{
|
||||
input: "1== 1",
|
||||
tokenType: [TokenType.NUMBER, TokenType.EQUAL_EQUAL, TokenType.NUMBER]
|
||||
tokenTypes: [TokenType.NUMBER, TokenType.EQUAL_EQUAL, TokenType.NUMBER]
|
||||
},
|
||||
{
|
||||
input: "1< 1",
|
||||
tokenType: [TokenType.NUMBER, TokenType.LESS, TokenType.NUMBER]
|
||||
tokenTypes: [TokenType.NUMBER, TokenType.LESS, TokenType.NUMBER]
|
||||
},
|
||||
|
||||
// Identifiers
|
||||
{
|
||||
input: "github",
|
||||
tokenType: [TokenType.IDENTIFIER],
|
||||
token: {
|
||||
type: TokenType.IDENTIFIER,
|
||||
lexeme: "github",
|
||||
pos: {
|
||||
line: 0,
|
||||
column: 0
|
||||
tokenTypes: [TokenType.IDENTIFIER],
|
||||
tokens: [
|
||||
{
|
||||
type: TokenType.IDENTIFIER,
|
||||
lexeme: "github",
|
||||
range: {
|
||||
start: {
|
||||
line: 0,
|
||||
column: 0
|
||||
},
|
||||
end: {
|
||||
line: 0,
|
||||
column: 6
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// Keywords
|
||||
{
|
||||
input: "true",
|
||||
tokenType: [TokenType.TRUE],
|
||||
token: {
|
||||
type: TokenType.TRUE,
|
||||
lexeme: "true",
|
||||
pos: {
|
||||
line: 0,
|
||||
column: 0
|
||||
tokenTypes: [TokenType.TRUE],
|
||||
tokens: [
|
||||
{
|
||||
type: TokenType.TRUE,
|
||||
lexeme: "true",
|
||||
range: {
|
||||
start: {
|
||||
line: 0,
|
||||
column: 0
|
||||
},
|
||||
end: {
|
||||
line: 0,
|
||||
column: 4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
input: "false",
|
||||
tokenType: [TokenType.FALSE],
|
||||
token: {
|
||||
type: TokenType.FALSE,
|
||||
lexeme: "false",
|
||||
pos: {
|
||||
line: 0,
|
||||
column: 0
|
||||
tokenTypes: [TokenType.FALSE],
|
||||
tokens: [
|
||||
{
|
||||
type: TokenType.FALSE,
|
||||
lexeme: "false",
|
||||
range: {
|
||||
start: {
|
||||
line: 0,
|
||||
column: 0
|
||||
},
|
||||
end: {
|
||||
line: 0,
|
||||
column: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
input: "null",
|
||||
tokenType: [TokenType.NULL],
|
||||
token: {
|
||||
type: TokenType.NULL,
|
||||
lexeme: "null",
|
||||
pos: {
|
||||
line: 0,
|
||||
column: 0
|
||||
tokenTypes: [TokenType.NULL],
|
||||
tokens: [
|
||||
{
|
||||
type: TokenType.NULL,
|
||||
lexeme: "null",
|
||||
range: {
|
||||
start: {
|
||||
line: 0,
|
||||
column: 0
|
||||
},
|
||||
end: {
|
||||
line: 0,
|
||||
column: 4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
input: "github\n ==",
|
||||
tokenTypes: [TokenType.IDENTIFIER, TokenType.EQUAL_EQUAL],
|
||||
tokens: [
|
||||
{
|
||||
type: TokenType.IDENTIFIER,
|
||||
lexeme: "github",
|
||||
range: {
|
||||
start: {
|
||||
line: 0,
|
||||
column: 0
|
||||
},
|
||||
end: {
|
||||
line: 0,
|
||||
column: 6
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: TokenType.EQUAL_EQUAL,
|
||||
lexeme: "==",
|
||||
range: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 1
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 3
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
test.each(tests)("$input", ({input, tokenType, token}: {input: string; tokenType: TokenType[]; token?: Token}) => {
|
||||
const l = new Lexer(input);
|
||||
test.each(tests)(
|
||||
"$input",
|
||||
({input, tokenTypes, tokens}: {input: string; tokenTypes: TokenType[]; tokens?: Token[]}) => {
|
||||
const l = new Lexer(input);
|
||||
|
||||
const r = l.lex();
|
||||
const r = l.lex();
|
||||
|
||||
const want = r.tokens.map(t => t.type);
|
||||
const got = r.tokens.map(t => t.type);
|
||||
|
||||
tokenType.push(TokenType.EOF);
|
||||
tokenTypes.push(TokenType.EOF);
|
||||
expect(got).toEqual(tokenTypes);
|
||||
|
||||
expect(want).toEqual(tokenType);
|
||||
});
|
||||
if (tokens) {
|
||||
// Ignore the last EOF token
|
||||
expect(r.tokens.slice(0, r.tokens.length - 1)).toEqual(tokens);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -36,13 +36,18 @@ export type Pos = {
|
||||
column: number;
|
||||
};
|
||||
|
||||
export type Range = {
|
||||
start: Pos;
|
||||
end: Pos;
|
||||
};
|
||||
|
||||
export type Token = {
|
||||
type: TokenType;
|
||||
|
||||
lexeme: string;
|
||||
value?: string | number | boolean;
|
||||
|
||||
pos: Pos;
|
||||
range: Range;
|
||||
};
|
||||
|
||||
export function tokenString(tok: Token): string {
|
||||
@@ -192,7 +197,7 @@ export class Lexer {
|
||||
this.tokens.push({
|
||||
type: TokenType.EOF,
|
||||
lexeme: "",
|
||||
pos: this.pos()
|
||||
range: this.range()
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -207,6 +212,20 @@ export class Lexer {
|
||||
};
|
||||
}
|
||||
|
||||
private endPos(): Pos {
|
||||
return {
|
||||
line: this.line,
|
||||
column: this.offset - this.lastLineOffset
|
||||
};
|
||||
}
|
||||
|
||||
private range(): Range {
|
||||
return {
|
||||
start: this.pos(),
|
||||
end: this.endPos()
|
||||
};
|
||||
}
|
||||
|
||||
private atEnd(): boolean {
|
||||
return this.offset >= this.input.length;
|
||||
}
|
||||
@@ -240,10 +259,6 @@ export class Lexer {
|
||||
return this.input[this.offset++];
|
||||
}
|
||||
|
||||
private reverse(): string {
|
||||
return this.input[--this.offset];
|
||||
}
|
||||
|
||||
private match(expected: string): boolean {
|
||||
if (this.atEnd()) {
|
||||
return false;
|
||||
@@ -260,7 +275,7 @@ export class Lexer {
|
||||
this.tokens.push({
|
||||
type,
|
||||
lexeme: this.input.substring(this.start, this.offset),
|
||||
pos: this.pos(),
|
||||
range: this.range(),
|
||||
value
|
||||
});
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ export class Parser {
|
||||
|
||||
if (this.match(TokenType.IDENTIFIER)) {
|
||||
let property = this.previous();
|
||||
expr = new IndexAccess(expr, new Literal(new data.StringData(property.lexeme)));
|
||||
expr = new IndexAccess(expr, new Literal(new data.StringData(property.lexeme), property));
|
||||
} else if (this.match(TokenType.STAR)) {
|
||||
expr = new IndexAccess(expr, new Star());
|
||||
} else {
|
||||
@@ -254,19 +254,19 @@ export class Parser {
|
||||
private primary(): Expr {
|
||||
switch (true) {
|
||||
case this.match(TokenType.FALSE):
|
||||
return new Literal(new data.BooleanData(false));
|
||||
return new Literal(new data.BooleanData(false), this.previous());
|
||||
|
||||
case this.match(TokenType.TRUE):
|
||||
return new Literal(new data.BooleanData(true));
|
||||
return new Literal(new data.BooleanData(true), this.previous());
|
||||
|
||||
case this.match(TokenType.NULL):
|
||||
return new Literal(new data.Null());
|
||||
return new Literal(new data.Null(), this.previous());
|
||||
|
||||
case this.match(TokenType.NUMBER):
|
||||
return new Literal(new data.NumberData(this.previous().value as number));
|
||||
return new Literal(new data.NumberData(this.previous().value as number), this.previous());
|
||||
|
||||
case this.match(TokenType.STRING):
|
||||
return new Literal(new data.StringData(this.previous().value as string));
|
||||
return new Literal(new data.StringData(this.previous().value as string), this.previous());
|
||||
|
||||
case this.match(TokenType.LEFT_PAREN):
|
||||
const expr = this.expression();
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
dist
|
||||
*.md
|
||||
*.js
|
||||
*.json
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@github/actions-languageserver",
|
||||
"version": "0.1.113",
|
||||
"version": "0.1.126",
|
||||
"description": "Language server for GitHub Actions",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -38,8 +38,8 @@
|
||||
"watch": "tsc --build tsconfig.build.json --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@github/actions-languageservice": "^0.1.113",
|
||||
"@github/actions-workflow-parser": "^0.1.113",
|
||||
"@github/actions-languageservice": "^0.1.126",
|
||||
"@github/actions-workflow-parser": "^0.1.126",
|
||||
"@octokit/rest": "^19.0.7",
|
||||
"vscode-languageserver": "^8.0.2",
|
||||
"vscode-languageserver-textdocument": "^1.0.7",
|
||||
|
||||
@@ -20,6 +20,7 @@ import {getClient} from "./client";
|
||||
import {Commands} from "./commands";
|
||||
import {contextProviders} from "./context-providers";
|
||||
import {descriptionProvider} from "./description-provider";
|
||||
import {getFileProvider} from "./file-provider";
|
||||
import {InitializationOptions, RepositoryContext} from "./initializationOptions";
|
||||
import {onCompletion} from "./on-completion";
|
||||
import {TTLCache} from "./utils/cache";
|
||||
@@ -106,7 +107,10 @@ export function initConnection(connection: Connection) {
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
fileProvider: getFileProvider(client, cache, repoContext?.workspaceUri, async path => {
|
||||
return await connection.sendRequest("actions/readFile", {path});
|
||||
})
|
||||
};
|
||||
const result = await validate(textDocument, config);
|
||||
|
||||
@@ -124,8 +128,11 @@ export function initConnection(connection: Connection) {
|
||||
});
|
||||
|
||||
connection.onHover(async ({position, textDocument}: HoverParams): Promise<Hover | null> => {
|
||||
const repoContext = repos.find(repo => textDocument.uri.startsWith(repo.workspaceUri));
|
||||
|
||||
return hover(documents.get(textDocument.uri)!, position, {
|
||||
descriptionProvider: descriptionProvider(client, cache)
|
||||
descriptionProvider: descriptionProvider(client, cache),
|
||||
contextProviderConfig: repoContext && contextProviders(client, repoContext, cache)
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ const actionMetadata = {
|
||||
};
|
||||
|
||||
it("returns default context when job is undefined", async () => {
|
||||
const workflowContext = createWorkflowContext(workflow, undefined);
|
||||
const workflowContext = await createWorkflowContext(workflow, undefined);
|
||||
const defaultContext = getDefaultStepsContext(workflowContext);
|
||||
|
||||
const stepsContext = await getStepsContext(new Octokit(), new TTLCache(), defaultContext, workflowContext);
|
||||
@@ -68,7 +68,7 @@ it("adds action outputs", async () => {
|
||||
.sandbox()
|
||||
.getOnce("https://api.github.com/repos/actions/cache/contents/action.yml?ref=v3", actionMetadata);
|
||||
|
||||
const workflowContext = createWorkflowContext(workflow, "build");
|
||||
const workflowContext = await createWorkflowContext(workflow, "build");
|
||||
const defaultContext = getDefaultStepsContext(workflowContext);
|
||||
|
||||
const stepsContext = await getStepsContext(
|
||||
|
||||
@@ -60,7 +60,7 @@ const actionMetadata = {
|
||||
};
|
||||
|
||||
async function getDescription(input: string, mock: fetchMock.FetchMockSandbox) {
|
||||
const workflowContext = createWorkflowContext(workflow, "build", 0);
|
||||
const workflowContext = await createWorkflowContext(workflow, "build", 0);
|
||||
|
||||
return await getActionInputDescription(
|
||||
new Octokit({
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import {File} from "@github/actions-workflow-parser/workflows/file";
|
||||
import {FileProvider} from "@github/actions-workflow-parser/workflows/file-provider";
|
||||
import {fileIdentifier} from "@github/actions-workflow-parser/workflows/file-reference";
|
||||
import {Octokit} from "@octokit/rest";
|
||||
import path from "path";
|
||||
import {TTLCache} from "./utils/cache";
|
||||
|
||||
export function getFileProvider(
|
||||
client: Octokit | undefined,
|
||||
cache: TTLCache,
|
||||
workspace: string | undefined,
|
||||
readFile: (path: string) => Promise<string>
|
||||
): FileProvider | undefined {
|
||||
if (!client && !workspace) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
getFileContent: async (ref): Promise<File> => {
|
||||
if ("repository" in ref) {
|
||||
if (!client) {
|
||||
throw new Error("Remote file references are not supported with this configuration");
|
||||
}
|
||||
|
||||
return await cache.get(`file-content-${fileIdentifier(ref)}`, undefined, () =>
|
||||
fetchWorkflowFile(client, ref.owner, ref.repository, ref.path, ref.version)
|
||||
);
|
||||
}
|
||||
|
||||
if (!workspace) {
|
||||
throw new Error("Local file references are not supported with this configuration");
|
||||
}
|
||||
|
||||
const file = await readFile(path.join(workspace, ref.path));
|
||||
if (!file) {
|
||||
throw new Error(`File not found: ${ref.path}`);
|
||||
}
|
||||
return {
|
||||
name: ref.path,
|
||||
content: file
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchWorkflowFile(
|
||||
client: Octokit,
|
||||
owner: string,
|
||||
repo: string,
|
||||
path: string,
|
||||
version: string
|
||||
): Promise<File> {
|
||||
const resp = await client.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path,
|
||||
ref: version
|
||||
});
|
||||
|
||||
// https://docs.github.com/rest/repos/contents?apiVersion=2022-11-28
|
||||
// Ignore directories (array of files) and non-file content
|
||||
if (
|
||||
resp.data === undefined ||
|
||||
Array.isArray(resp.data) ||
|
||||
resp.data.type !== "file" ||
|
||||
resp.data.content === undefined
|
||||
) {
|
||||
throw new Error("Not a file");
|
||||
}
|
||||
|
||||
return {
|
||||
name: path,
|
||||
content: Buffer.from(resp.data.content, "base64").toString("utf8")
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import {WorkflowContext} from "@github/actions-languageservice/context/workflow-context";
|
||||
import {convertWorkflowTemplate, parseWorkflow, TraceWriter} from "@github/actions-workflow-parser";
|
||||
import {isJob} from "@github/actions-workflow-parser/model/type-guards";
|
||||
|
||||
const nullTrace: TraceWriter = {
|
||||
info: x => {},
|
||||
@@ -7,16 +8,27 @@ const nullTrace: TraceWriter = {
|
||||
error: x => {}
|
||||
};
|
||||
|
||||
export function createWorkflowContext(workflow: string, job?: string, stepIndex?: number): WorkflowContext {
|
||||
const parsed = parseWorkflow("test.yaml", [{name: "test.yaml", content: workflow}], nullTrace);
|
||||
export async function createWorkflowContext(
|
||||
workflow: string,
|
||||
job?: string,
|
||||
stepIndex?: number
|
||||
): Promise<WorkflowContext> {
|
||||
const parsed = parseWorkflow({name: "test.yaml", content: workflow}, nullTrace);
|
||||
if (!parsed.value) {
|
||||
throw new Error("Failed to parse workflow");
|
||||
}
|
||||
const template = convertWorkflowTemplate(parsed.context, parsed.value);
|
||||
const template = await convertWorkflowTemplate(parsed.context, parsed.value);
|
||||
const context: WorkflowContext = {uri: "test.yaml", template};
|
||||
|
||||
if (job) {
|
||||
context.job = template.jobs.find(j => j.id.value === job);
|
||||
const workflowJob = template.jobs.find(j => j.id.value === job);
|
||||
if (workflowJob) {
|
||||
if (isJob(workflowJob)) {
|
||||
context.job = workflowJob;
|
||||
} else {
|
||||
context.reusableWorkflowJob = workflowJob;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stepIndex !== undefined) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@github/actions-languageservice",
|
||||
"version": "0.1.113",
|
||||
"version": "0.1.126",
|
||||
"description": "Language service for GitHub Actions",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -38,8 +38,8 @@
|
||||
"watch": "tsc --build tsconfig.build.json --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@github/actions-expressions": "^0.1.113",
|
||||
"@github/actions-workflow-parser": "^0.1.113",
|
||||
"@github/actions-expressions": "^0.1.126",
|
||||
"@github/actions-workflow-parser": "^0.1.126",
|
||||
"vscode-languageserver-textdocument": "^1.0.7",
|
||||
"vscode-languageserver-types": "^3.17.2",
|
||||
"yaml": "^2.1.1"
|
||||
|
||||
@@ -153,6 +153,24 @@ describe("expressions", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("multiple regions - first region", async () => {
|
||||
const input = "run-name: test-${{ git| == 1 }}-${{ github.event }}";
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
|
||||
expect(result.map(x => x.label)).toEqual([
|
||||
"github",
|
||||
"inputs",
|
||||
"vars",
|
||||
"contains",
|
||||
"endsWith",
|
||||
"format",
|
||||
"fromJson",
|
||||
"join",
|
||||
"startsWith",
|
||||
"toJson"
|
||||
]);
|
||||
});
|
||||
|
||||
it("multiple regions", async () => {
|
||||
const input = "run-name: test-${{ github }}-${{ | }}";
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {DescriptionDictionary, complete as completeExpression} from "@github/actions-expressions";
|
||||
import {complete as completeExpression, DescriptionDictionary} from "@github/actions-expressions";
|
||||
import {CompletionItem as ExpressionCompletionItem} from "@github/actions-expressions/completion";
|
||||
import {
|
||||
convertWorkflowTemplate,
|
||||
@@ -8,8 +8,6 @@ import {
|
||||
parseWorkflow
|
||||
} from "@github/actions-workflow-parser";
|
||||
import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert";
|
||||
import {DefinitionType} from "@github/actions-workflow-parser/templates/schema/definition-type";
|
||||
import {StringDefinition} from "@github/actions-workflow-parser/templates/schema/string-definition";
|
||||
import {OPEN_EXPRESSION} from "@github/actions-workflow-parser/templates/template-constants";
|
||||
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/index";
|
||||
import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token";
|
||||
@@ -23,14 +21,15 @@ import {getWorkflowContext, WorkflowContext} from "./context/workflow-context";
|
||||
import {validatorFunctions} from "./expression-validation/functions";
|
||||
import {error} from "./log";
|
||||
import {nullTrace} from "./nulltrace";
|
||||
import {isPotentiallyExpression} from "./utils/expression-detection";
|
||||
import {findToken} from "./utils/find-token";
|
||||
import {guessIndentation} from "./utils/indentation-guesser";
|
||||
import {mapRange} from "./utils/range";
|
||||
import {getRelCharOffset} from "./utils/rel-char-pos";
|
||||
import {transform} from "./utils/transform";
|
||||
import {Value, ValueProviderConfig} from "./value-providers/config";
|
||||
import {defaultValueProviders} from "./value-providers/default";
|
||||
import {definitionValues} from "./value-providers/definition";
|
||||
import {TokenRange} from "@github/actions-workflow-parser/templates/tokens/token-range";
|
||||
|
||||
export function getExpressionInput(input: string, pos: number): string {
|
||||
// Find start marker around the cursor position
|
||||
@@ -66,22 +65,19 @@ export async function complete(
|
||||
name: textDocument.uri,
|
||||
content: newDoc.getText()
|
||||
};
|
||||
const result = parseWorkflow(file.name, [file], nullTrace);
|
||||
const result = parseWorkflow(file, nullTrace);
|
||||
if (!result.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const {token, keyToken, parent, path} = findToken(newPos, result.value);
|
||||
const template = convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
|
||||
const workflowContext = getWorkflowContext(textDocument.uri, template, path);
|
||||
|
||||
// If we are inside an expression, take a different code-path. The workflow parser does not correctly create
|
||||
// expression nodes for invalid expressions and during editing expressions are invalid most of the time.
|
||||
if (token) {
|
||||
const isStringExpressionToken = isStringExpression(token);
|
||||
const isBasicExpressionToken = isBasicExpression(token) && token.isExpression;
|
||||
|
||||
if (isStringExpressionToken || isBasicExpressionToken) {
|
||||
if (isBasicExpression(token) || isPotentiallyExpression(token)) {
|
||||
const allowedContext = token.definitionInfo?.allowedContext || [];
|
||||
const context = await getContext(allowedContext, contextProviderConfig, workflowContext, Mode.Completion);
|
||||
|
||||
@@ -93,12 +89,14 @@ export async function complete(
|
||||
const indentString = " ".repeat(indentation.tabSize);
|
||||
|
||||
const values = await getValues(token, keyToken, parent, valueProviderConfig, workflowContext, indentString);
|
||||
|
||||
let replaceRange: Range | undefined;
|
||||
if (token?.range) {
|
||||
replaceRange = mapRange(token.range);
|
||||
} else if (!token) {
|
||||
// Not a valid token, create a range from the current position
|
||||
const line = newDoc.getText({start: {line: position.line, character: 0}, end: position});
|
||||
|
||||
// Get the length of the current word
|
||||
const val = line.match(/[\w_-]*$/)?.[0].length || 0;
|
||||
replaceRange = Range.create({line: position.line, character: position.character - val}, position);
|
||||
@@ -214,12 +212,12 @@ function getExpressionCompletionItems(
|
||||
currentInput = stringToken.source || stringToken.value;
|
||||
}
|
||||
|
||||
const relCharPos = getRelCharPos(token.range!, currentInput, pos);
|
||||
const expressionInput = (getExpressionInput(currentInput, relCharPos) || "").trim();
|
||||
const relCharOffset = getRelCharOffset(token.range!, currentInput, pos);
|
||||
const expressionInput = (getExpressionInput(currentInput, relCharOffset) || "").trim();
|
||||
|
||||
try {
|
||||
return completeExpression(expressionInput, context, [], validatorFunctions).map(item =>
|
||||
mapExpressionCompletionItem(item, currentInput[relCharPos])
|
||||
mapExpressionCompletionItem(item, currentInput[relCharOffset])
|
||||
);
|
||||
} catch (e: any) {
|
||||
error(`Error while completing expression: '${e?.message || "<no details>"}'`);
|
||||
@@ -250,23 +248,3 @@ function mapExpressionCompletionItem(item: ExpressionCompletionItem, charAfterPo
|
||||
kind: item.function ? CompletionItemKind.Function : CompletionItemKind.Variable
|
||||
};
|
||||
}
|
||||
|
||||
function getRelCharPos(tokenRange: TokenRange, currentInput: string, pos: Position): number {
|
||||
// Transform the overall position into a node relative position
|
||||
const range = mapRange(tokenRange);
|
||||
if (range.start.line !== range.end.line) {
|
||||
const lines = currentInput.split("\n");
|
||||
const lineDiff = pos.line - range.start.line - 1;
|
||||
const linesBeforeCusor = lines.slice(0, lineDiff);
|
||||
return linesBeforeCusor.join("\n").length + pos.character + 1;
|
||||
} else {
|
||||
return pos.character - range.start.character;
|
||||
}
|
||||
}
|
||||
|
||||
function isStringExpression(token: TemplateToken): boolean {
|
||||
const isExpression =
|
||||
token.definition?.definitionType === DefinitionType.String && (token.definition as StringDefinition).isExpression;
|
||||
const containsExpression = isString(token) && token.value.indexOf(OPEN_EXPRESSION) >= 0;
|
||||
return isExpression || containsExpression;
|
||||
}
|
||||
|
||||
@@ -182,5 +182,39 @@
|
||||
"outcome": {
|
||||
"description": "The result of a completed step before `continue-on-error` is applied. Possible values are `success`, `failure`, `cancelled`, or `skipped`. When a `continue-on-error` step fails, the `outcome` is `failure`, but the final conclusion is `success`."
|
||||
}
|
||||
},
|
||||
"runner": {
|
||||
"name": {
|
||||
"description": "The name of the runner executing the job."
|
||||
},
|
||||
"os": {
|
||||
"description": "The operating system of the runner executing the job. Possible values are `Linux`, `Windows`, or `macOS`."
|
||||
},
|
||||
"arch": {
|
||||
"description": "The architecture of the runner executing the job. Possible values are `X86`, `X64`, `ARM`, or `ARM64`."
|
||||
},
|
||||
"temp": {
|
||||
"description": "The path to a temporary directory on the runner. This directory is emptied at the beginning and end of each job. Note that files will not be removed if the runner's user account does not have permission to delete them."
|
||||
},
|
||||
"tool_cache": {
|
||||
"description": "The path to the directory containing preinstalled tools for GitHub-hosted runners. For more information, see \"[About GitHub-hosted runners](https://docs.github.com/actions/reference/specifications-for-github-hosted-runners/#supported-software)\"."
|
||||
},
|
||||
"debug": {
|
||||
"description": "This is set only if [debug logging](https://docs.github.com/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging) is enabled, and always has the value of 1. It can be useful as an indicator to enable additional debugging or verbose logging in your own job steps."
|
||||
}
|
||||
},
|
||||
"strategy": {
|
||||
"fail-fast": {
|
||||
"description": "The `fail-fast` setting for the job. Possible values are `true` or `false`. For more information, see [Workflow syntax for GitHub Actions: `jobs.<job_id>.strategy.fail-fast`](https://docs.github.com/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstrategyfail-fast)."
|
||||
},
|
||||
"max-parallel": {
|
||||
"description": "The `max-parallel` setting for the job. For more information, see [Workflow syntax for GitHub Actions: `jobs.<job_id>.strategy.max-parallel`](https://docs.github.com/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstrategymax-parallel)."
|
||||
},
|
||||
"job-index": {
|
||||
"description": "The index of the current job in the matrix. **Note:** This number is a zero-based number. The first job's index in the matrix is `0`."
|
||||
},
|
||||
"job-total": {
|
||||
"description": "The total number of jobs in the matrix. **Note:** This number **is not** a zero-based number. For example, for a matrix with four jobs, the value of `job-total` is `4`."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ function stringToToken(value: string) {
|
||||
}
|
||||
|
||||
function expressionToToken(expr: string) {
|
||||
return new BasicExpressionToken(undefined, undefined, expr, undefined, undefined, expr);
|
||||
return new BasicExpressionToken(undefined, undefined, expr, undefined, undefined, undefined);
|
||||
}
|
||||
|
||||
function contextFromStrategy(strategy?: TemplateToken) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {data, DescriptionDictionary} from "@github/actions-expressions";
|
||||
import {isScalar, isString} from "@github/actions-workflow-parser";
|
||||
import {Job} from "@github/actions-workflow-parser/model/workflow-template";
|
||||
import {isJob} from "@github/actions-workflow-parser/model/type-guards";
|
||||
import {Job, WorkflowJob} from "@github/actions-workflow-parser/model/workflow-template";
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
|
||||
export function getNeedsContext(workflowContext: WorkflowContext): DescriptionDictionary {
|
||||
@@ -17,11 +18,13 @@ export function getNeedsContext(workflowContext: WorkflowContext): DescriptionDi
|
||||
return d;
|
||||
}
|
||||
|
||||
function needsJobContext(job?: Job): DescriptionDictionary {
|
||||
function needsJobContext(job?: WorkflowJob): DescriptionDictionary {
|
||||
// https://docs.github.com/en/actions/learn-github-actions/contexts#needs-context
|
||||
const d = new DescriptionDictionary();
|
||||
|
||||
d.add("outputs", jobOutputs(job));
|
||||
if (job && isJob(job)) {
|
||||
d.add("outputs", jobOutputs(job));
|
||||
}
|
||||
|
||||
// Can be "success", "failure", "cancelled", or "skipped"
|
||||
d.add("result", new data.Null());
|
||||
|
||||
@@ -5,23 +5,20 @@ import {getPositionFromCursor} from "../test-utils/cursor-position";
|
||||
import {findToken} from "../utils/find-token";
|
||||
import {getWorkflowContext, WorkflowContext} from "./workflow-context";
|
||||
|
||||
function testGetWorkflowContext(input: string): WorkflowContext {
|
||||
async function testGetWorkflowContext(input: string): Promise<WorkflowContext> {
|
||||
const [textDocument, pos] = getPositionFromCursor(input);
|
||||
const result = parseWorkflow(
|
||||
"wf.yaml",
|
||||
[
|
||||
{
|
||||
content: textDocument.getText(),
|
||||
name: "wf.yaml"
|
||||
}
|
||||
],
|
||||
{
|
||||
content: textDocument.getText(),
|
||||
name: "wf.yaml"
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
let template: WorkflowTemplate | undefined;
|
||||
|
||||
if (result.value) {
|
||||
template = convertWorkflowTemplate(result.context, result.value);
|
||||
template = await convertWorkflowTemplate(result.context, result.value);
|
||||
}
|
||||
|
||||
const {path} = findToken(pos, result.value);
|
||||
@@ -30,8 +27,8 @@ function testGetWorkflowContext(input: string): WorkflowContext {
|
||||
}
|
||||
|
||||
describe("getWorkflowContext", () => {
|
||||
it("context for workflow", () => {
|
||||
const context = testGetWorkflowContext(`on: push
|
||||
it("context for workflow", async () => {
|
||||
const context = await testGetWorkflowContext(`on: push
|
||||
name: te|st
|
||||
jobs:
|
||||
build:
|
||||
@@ -44,8 +41,8 @@ jobs:
|
||||
expect(context.step).toBeUndefined();
|
||||
});
|
||||
|
||||
it("context for workflow job", () => {
|
||||
const context = testGetWorkflowContext(`on: push
|
||||
it("context for workflow job", async () => {
|
||||
const context = await testGetWorkflowContext(`on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-lat|est
|
||||
@@ -57,8 +54,8 @@ jobs:
|
||||
expect(context.step).toBeUndefined();
|
||||
});
|
||||
|
||||
it("context for workflow run step", () => {
|
||||
const context = testGetWorkflowContext(`on: push
|
||||
it("context for workflow run step", async () => {
|
||||
const context = await testGetWorkflowContext(`on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -74,8 +71,8 @@ jobs:
|
||||
expect(step.run.toDisplayString()).toBe("echo Hello");
|
||||
});
|
||||
|
||||
it("context for workflow uses step", () => {
|
||||
const context = testGetWorkflowContext(`on: push
|
||||
it("context for workflow uses step", async () => {
|
||||
const context = await testGetWorkflowContext(`on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {isMapping, isSequence, WorkflowTemplate} from "@github/actions-workflow-parser";
|
||||
import {Job, Step} from "@github/actions-workflow-parser/model/workflow-template";
|
||||
import {isJob, isReusableWorkflowJob} from "@github/actions-workflow-parser/model/type-guards";
|
||||
import {Step, Job, ReusableWorkflowJob} from "@github/actions-workflow-parser/model/workflow-template";
|
||||
import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token";
|
||||
import {SequenceToken} from "@github/actions-workflow-parser/templates/tokens/sequence-token";
|
||||
import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token";
|
||||
@@ -10,9 +11,12 @@ export interface WorkflowContext {
|
||||
|
||||
template: WorkflowTemplate | undefined;
|
||||
|
||||
/** If the context is for a position within a job, this will be the job */
|
||||
/** If the context is for a position within a regular job, this will be the job */
|
||||
job?: Job;
|
||||
|
||||
/** If the context is for a position within a reusable workflow job, this will be the reusable workflow job */
|
||||
reusableWorkflowJob?: ReusableWorkflowJob;
|
||||
|
||||
/** If the context is for a position within a step, this will be the step */
|
||||
step?: Step;
|
||||
}
|
||||
@@ -35,7 +39,15 @@ export function getWorkflowContext(
|
||||
switch (token.definition?.key) {
|
||||
case "job": {
|
||||
const jobID = (token as StringToken).value;
|
||||
context.job = template.jobs.find(job => job.id.value === jobID);
|
||||
const job = template.jobs.find(job => job.id.value === jobID);
|
||||
if (!job) {
|
||||
break;
|
||||
}
|
||||
if (isJob(job)) {
|
||||
context.job = job;
|
||||
} else if (isReusableWorkflowJob(job)) {
|
||||
context.reusableWorkflowJob = job;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "steps": {
|
||||
@@ -54,7 +66,10 @@ export function getWorkflowContext(
|
||||
}
|
||||
}
|
||||
|
||||
context.step = findStep(context.job?.steps, stepsSequence, stepToken);
|
||||
if (context.job && isJob(context.job)) {
|
||||
context.step = findStep(context.job.steps, stepsSequence, stepToken);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {convertWorkflowTemplate, parseWorkflow} from "@github/actions-workflow-parser";
|
||||
import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert";
|
||||
import {isJob} from "@github/actions-workflow-parser/model/type-guards";
|
||||
import {File} from "@github/actions-workflow-parser/workflows/file";
|
||||
import {TextDocument} from "vscode-languageserver-textdocument";
|
||||
import {DocumentLink} from "vscode-languageserver-types";
|
||||
@@ -13,12 +14,12 @@ export async function documentLinks(document: TextDocument): Promise<DocumentLin
|
||||
content: document.getText()
|
||||
};
|
||||
|
||||
const result = parseWorkflow(file.name, [file], nullTrace);
|
||||
const result = parseWorkflow(file, nullTrace);
|
||||
if (!result.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const template = convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
|
||||
// Add links to referenced actions
|
||||
const actionLinks: DocumentLink[] = [];
|
||||
@@ -27,7 +28,10 @@ export async function documentLinks(document: TextDocument): Promise<DocumentLin
|
||||
const gitHubBaseUri = "https://www.github.com/";
|
||||
|
||||
for (const job of template?.jobs || []) {
|
||||
for (const step of job?.steps || []) {
|
||||
if (!job || !isJob(job)) {
|
||||
continue;
|
||||
}
|
||||
for (const step of job.steps || []) {
|
||||
if ("uses" in step) {
|
||||
const actionRef = parseActionReference(step.uses.value);
|
||||
if (!actionRef) {
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import {parseWorkflow} from "@github/actions-workflow-parser/.";
|
||||
import {File} from "@github/actions-workflow-parser/workflows/file";
|
||||
import {nullTrace} from "../nulltrace";
|
||||
import {getPositionFromCursor} from "../test-utils/cursor-position";
|
||||
import {findToken} from "../utils/find-token";
|
||||
import {ExpressionPos, mapToExpressionPos} from "./expression-pos";
|
||||
|
||||
describe("mapToExpressionPos", () => {
|
||||
it("simple expression", () => {
|
||||
expect(
|
||||
testMapToExpressionPos(`on: push
|
||||
run-name: \${{ git|hub.event }}`)
|
||||
).toEqual<ExpressionPos>({
|
||||
expression: "github.event",
|
||||
position: {line: 0, column: 3},
|
||||
documentRange: {
|
||||
start: {line: 1, character: 14},
|
||||
end: {line: 1, character: 26}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("implicit format expression", () => {
|
||||
expect(
|
||||
testMapToExpressionPos(`on: push
|
||||
run-name: hello \${{ git|hub.event }}`)
|
||||
).toEqual<ExpressionPos>({
|
||||
expression: "github.event",
|
||||
position: {line: 0, column: 3},
|
||||
documentRange: {
|
||||
start: {line: 1, character: 20},
|
||||
end: {line: 1, character: 32}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("implicit complex format expression", () => {
|
||||
expect(
|
||||
testMapToExpressionPos(`on: push
|
||||
run-name: hello \${{ github.test }}-\${{ git|hub.event }}`)
|
||||
).toEqual<ExpressionPos>({
|
||||
expression: "github.event",
|
||||
position: {line: 0, column: 3},
|
||||
documentRange: {
|
||||
start: {line: 1, character: 39},
|
||||
end: {line: 1, character: 51}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("multi-line expression", () => {
|
||||
expect(
|
||||
testMapToExpressionPos(`on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted]
|
||||
steps:
|
||||
- run: >
|
||||
echo 'hello'
|
||||
echo '\${{ github.event.te|st }}
|
||||
echo 'world'
|
||||
echo '\${{ github.event.test }}`)
|
||||
).toEqual<ExpressionPos>({
|
||||
expression: "github.event.test",
|
||||
position: {line: 0, column: 15},
|
||||
documentRange: {
|
||||
start: {line: 7, character: 18},
|
||||
end: {line: 7, character: 35}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function testMapToExpressionPos(input: string) {
|
||||
const [td, pos] = getPositionFromCursor(input);
|
||||
|
||||
const file: File = {
|
||||
name: td.uri,
|
||||
content: td.getText()
|
||||
};
|
||||
const result = parseWorkflow(file, nullTrace);
|
||||
if (!result.value) {
|
||||
throw new Error("Invalid workflow");
|
||||
}
|
||||
|
||||
const {token} = findToken(pos, result.value);
|
||||
if (!token) {
|
||||
throw new Error("No token found");
|
||||
}
|
||||
|
||||
return mapToExpressionPos(token, pos);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import {Pos} from "@github/actions-expressions/lexer";
|
||||
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
|
||||
import {isBasicExpression} from "@github/actions-workflow-parser/templates/tokens/type-guards";
|
||||
import {Position, Range as LSPRange} from "vscode-languageserver-textdocument";
|
||||
import {mapRange} from "../utils/range";
|
||||
import {posWithinRange} from "./pos-range";
|
||||
|
||||
export type ExpressionPos = {
|
||||
/** The expression that includes the position */
|
||||
expression: string;
|
||||
|
||||
/** Adjusted position, pointing into the expression */
|
||||
position: Pos;
|
||||
|
||||
/** Range of the expression in the document */
|
||||
documentRange: LSPRange;
|
||||
};
|
||||
|
||||
export function mapToExpressionPos(token: TemplateToken, position: Position): ExpressionPos | undefined {
|
||||
const pos: Pos = {
|
||||
line: position.line + 1,
|
||||
column: position.character + 1
|
||||
};
|
||||
|
||||
if (!isBasicExpression(token)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (token.originalExpressions?.length) {
|
||||
for (const originalExp of token.originalExpressions) {
|
||||
// Find the original expression that contains the position
|
||||
if (posWithinRange(pos, originalExp.expressionRange!)) {
|
||||
const exprRange = mapRange(originalExp.expressionRange);
|
||||
|
||||
return {
|
||||
expression: originalExp.expression,
|
||||
// Adjust the position to point into the expression
|
||||
position: {
|
||||
line: pos.line - exprRange.start.line - 1,
|
||||
column: pos.column - exprRange.start.character - 1
|
||||
},
|
||||
documentRange: exprRange
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const exprRange = mapRange(token.expressionRange!);
|
||||
return {
|
||||
expression: token.expression,
|
||||
// Adjust the position to point into the expression
|
||||
position: {
|
||||
line: pos.line - exprRange.start.line - 1,
|
||||
column: pos.column - exprRange.start.character - 1
|
||||
},
|
||||
documentRange: exprRange
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import {Pos, Range} from "@github/actions-expressions/lexer";
|
||||
|
||||
export function posWithinRange(pos: Pos, range: Range): boolean {
|
||||
return (
|
||||
pos.line >= range.start.line &&
|
||||
pos.line <= range.end.line &&
|
||||
pos.column >= range.start.column &&
|
||||
pos.column <= range.end.column
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import {data, DescriptionDictionary, Lexer, Parser} from "@github/actions-expressions";
|
||||
import {convertWorkflowTemplate, parseWorkflow} from "@github/actions-workflow-parser";
|
||||
import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert";
|
||||
import {File} from "@github/actions-workflow-parser/workflows/file";
|
||||
import {ContextProviderConfig} from "../context-providers/config";
|
||||
import {getContext, Mode} from "../context-providers/default";
|
||||
import {getWorkflowContext} from "../context/workflow-context";
|
||||
import {validatorFunctions} from "../expression-validation/functions";
|
||||
import {nullTrace} from "../nulltrace";
|
||||
import {getPositionFromCursor} from "../test-utils/cursor-position";
|
||||
import {HoverVisitor} from "./visitor";
|
||||
|
||||
const contextProviderConfig: ContextProviderConfig = {
|
||||
getContext: async (context: string) => {
|
||||
switch (context) {
|
||||
case "github":
|
||||
return new DescriptionDictionary(
|
||||
{
|
||||
key: "event",
|
||||
value: new data.StringData("push"),
|
||||
description: "The event that triggered the workflow"
|
||||
},
|
||||
{
|
||||
key: "test",
|
||||
value: new DescriptionDictionary({
|
||||
key: "name",
|
||||
value: new data.StringData("push"),
|
||||
description: "Name for the test"
|
||||
}),
|
||||
description: "Test dictionary"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
describe("visitor", () => {
|
||||
describe("unsupported hover positions", () => {
|
||||
["1 =|= 2", "12|3", "1 == |(2)", "'ab|c'"].forEach(x =>
|
||||
it(x, async () => expect(await hoverExpression(x)).toBeUndefined())
|
||||
);
|
||||
});
|
||||
|
||||
it("top-level context access", async () => {
|
||||
expect(await hoverExpression("githu|b")).toEqual({
|
||||
label: "github",
|
||||
description:
|
||||
"Information about the workflow run. For more information, see [`github` context](https://docs.github.com/actions/learn-github-actions/contexts#github-context).",
|
||||
function: false,
|
||||
range: {
|
||||
start: {line: 0, column: 0},
|
||||
end: {line: 0, column: 6}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("nested context access", async () => {
|
||||
expect(await hoverExpression("github.test.na|me")).toEqual({
|
||||
label: "name",
|
||||
description: "Name for the test",
|
||||
function: false,
|
||||
range: {
|
||||
start: {line: 0, column: 0},
|
||||
end: {line: 0, column: 16}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("nested context access with string key", async () => {
|
||||
expect(await hoverExpression("github['te|st']")).toEqual({
|
||||
label: "test",
|
||||
description: "Test dictionary",
|
||||
function: false,
|
||||
range: {
|
||||
start: {line: 0, column: 0},
|
||||
end: {line: 0, column: 13}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("function call", async () => {
|
||||
expect(await hoverExpression("cont|ains(github, 'github')")).toEqual({
|
||||
label: "contains",
|
||||
description:
|
||||
"`contains( search, item )`\n\nReturns `true` if `search` contains `item`. If `search`" +
|
||||
" is an array, this function returns `true` if the `item` is an element in the array. If `search`" +
|
||||
" is a string, this function returns `true` if the `item` is a substring of `search`. This function" +
|
||||
" is not case sensitive. Casts values to a string.",
|
||||
function: true,
|
||||
range: {
|
||||
start: {line: 0, column: 0},
|
||||
end: {line: 0, column: 8}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function hoverExpression(input: string) {
|
||||
const [td, pos] = getPositionFromCursor(input);
|
||||
const allowedContext = ["github"];
|
||||
|
||||
const file: File = {
|
||||
name: td.uri,
|
||||
content: td.getText()
|
||||
};
|
||||
const result = parseWorkflow(file, nullTrace);
|
||||
if (!result.value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const template = await convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
|
||||
const workflowContext = getWorkflowContext(td.uri, template, []);
|
||||
const context = await getContext(allowedContext, contextProviderConfig, workflowContext, Mode.Completion);
|
||||
|
||||
const l = new Lexer(td.getText());
|
||||
const lr = l.lex();
|
||||
|
||||
const p = new Parser(lr.tokens, ["github"], []);
|
||||
const expr = p.parse();
|
||||
|
||||
const hv = new HoverVisitor(
|
||||
{
|
||||
line: pos.line,
|
||||
column: pos.character
|
||||
},
|
||||
context,
|
||||
[],
|
||||
validatorFunctions
|
||||
);
|
||||
return hv.hover(expr);
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import {
|
||||
DescriptionDictionary,
|
||||
Evaluator,
|
||||
isDescriptionDictionary,
|
||||
wellKnownFunctions
|
||||
} from "@github/actions-expressions";
|
||||
import {
|
||||
Binary,
|
||||
ContextAccess,
|
||||
Expr,
|
||||
ExprVisitor,
|
||||
FunctionCall,
|
||||
Grouping,
|
||||
IndexAccess,
|
||||
Literal,
|
||||
Logical,
|
||||
Unary
|
||||
} from "@github/actions-expressions/ast";
|
||||
import {FunctionDefinition, FunctionInfo} from "@github/actions-expressions/funcs/info";
|
||||
import {Pos, Range} from "@github/actions-expressions/lexer";
|
||||
import {posWithinRange} from "./pos-range";
|
||||
|
||||
export type HoverResult =
|
||||
| undefined
|
||||
| {
|
||||
label: string;
|
||||
description?: string;
|
||||
function: boolean;
|
||||
range: Range;
|
||||
};
|
||||
|
||||
export class HoverVisitor implements ExprVisitor<HoverResult> {
|
||||
private ignorePosCheck = false;
|
||||
|
||||
constructor(
|
||||
private pos: Pos,
|
||||
private context: DescriptionDictionary,
|
||||
private extensionFunctions: FunctionInfo[],
|
||||
private functions: Map<string, FunctionDefinition>
|
||||
) {}
|
||||
|
||||
hover(n: Expr): HoverResult {
|
||||
return n.accept(this);
|
||||
}
|
||||
|
||||
visitLiteral(literal: Literal): HoverResult {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
visitUnary(unary: Unary): HoverResult {
|
||||
return this.hover(unary.expr);
|
||||
}
|
||||
|
||||
visitBinary(binary: Binary): HoverResult {
|
||||
return this.hover(binary.left) || this.hover(binary.right);
|
||||
}
|
||||
|
||||
visitLogical(logical: Logical): HoverResult {
|
||||
for (const arg of logical.args) {
|
||||
const result = this.hover(arg);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
visitGrouping(grouping: Grouping): HoverResult {
|
||||
return this.hover(grouping.group);
|
||||
}
|
||||
|
||||
visitContextAccess(contextAccess: ContextAccess): HoverResult {
|
||||
if (this.ignorePosCheck || posWithinRange(this.pos, contextAccess.name.range)) {
|
||||
const contextName = contextAccess.name.lexeme;
|
||||
|
||||
return {
|
||||
label: contextName,
|
||||
description: this.context.getDescription(contextName),
|
||||
function: false,
|
||||
range: contextAccess.name.range
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
visitIndexAccess(indexAccess: IndexAccess): HoverResult {
|
||||
// Is the position within the index, so for example:
|
||||
// github.event.test
|
||||
// ^ - pos
|
||||
if (!(indexAccess.index instanceof Literal)) {
|
||||
// No support for context access of the form github[github.event]
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!posWithinRange(this.pos, indexAccess.index.token.range)) {
|
||||
// Try to get hover from the rest of the expression
|
||||
return this.hover(indexAccess.expr);
|
||||
}
|
||||
|
||||
const ev = new Evaluator(indexAccess.expr, this.context, this.functions);
|
||||
const result = ev.evaluate();
|
||||
|
||||
if (!isDescriptionDictionary(result)) {
|
||||
// No description to show
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const key = indexAccess.index.literal.coerceString();
|
||||
const description = result.getDescription(key);
|
||||
if (!description) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Calculate context access range for whole expression. For example:
|
||||
// github.event.test
|
||||
// ^ - pos
|
||||
// should return the range:
|
||||
// github.event.test
|
||||
// ^^^^^^^^^^^^
|
||||
this.ignorePosCheck = true;
|
||||
|
||||
try {
|
||||
const contextHover = this.hover(indexAccess.expr);
|
||||
if (!contextHover) {
|
||||
throw new Error("Expected context hover to be defined");
|
||||
}
|
||||
|
||||
return {
|
||||
label: key,
|
||||
description: description,
|
||||
function: false,
|
||||
range: {
|
||||
start: contextHover.range.start,
|
||||
end: indexAccess.index.token.range.end
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
this.ignorePosCheck = false;
|
||||
}
|
||||
}
|
||||
|
||||
visitFunctionCall(functionCall: FunctionCall): HoverResult {
|
||||
if (posWithinRange(this.pos, functionCall.functionName.range)) {
|
||||
const functionName = functionCall.functionName.lexeme.toLowerCase();
|
||||
const f = this.functions.get(functionName) || wellKnownFunctions[functionName];
|
||||
|
||||
return {
|
||||
label: f.name,
|
||||
description: f.description,
|
||||
function: true,
|
||||
range: functionCall.functionName.range
|
||||
};
|
||||
}
|
||||
|
||||
for (const args of functionCall.args) {
|
||||
const result = this.hover(args);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import {data, DescriptionDictionary} from "@github/actions-expressions";
|
||||
import {Hover} from "vscode-languageserver-types";
|
||||
import {ContextProviderConfig} from "./context-providers/config";
|
||||
import {hover} from "./hover";
|
||||
import {registerLogger} from "./log";
|
||||
import {getPositionFromCursor} from "./test-utils/cursor-position";
|
||||
import {TestLogger} from "./test-utils/logger";
|
||||
|
||||
const contextProviderConfig: ContextProviderConfig = {
|
||||
getContext: async (context: string) => {
|
||||
switch (context) {
|
||||
case "github":
|
||||
return new DescriptionDictionary(
|
||||
{
|
||||
key: "event",
|
||||
value: new data.StringData("push"),
|
||||
description: "The event that triggered the workflow"
|
||||
},
|
||||
{
|
||||
key: "test",
|
||||
value: new DescriptionDictionary({
|
||||
key: "name",
|
||||
value: new data.StringData("push"),
|
||||
description: "Name for the test"
|
||||
}),
|
||||
description: "Test dictionary"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
registerLogger(new TestLogger());
|
||||
|
||||
describe("hover.expressions", () => {
|
||||
it("context access", async () => {
|
||||
const input = `on: push
|
||||
run-name: \${{ github.even|t }}
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted]`;
|
||||
const result = await hover(...getPositionFromCursor(input), {
|
||||
contextProviderConfig
|
||||
});
|
||||
expect(result).toEqual<Hover>({
|
||||
contents: "The event that triggered the workflow",
|
||||
range: {
|
||||
start: {line: 1, character: 14},
|
||||
end: {line: 1, character: 26}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("context", async () => {
|
||||
const input = `on: push
|
||||
run-name: \${{ git|hub.event }}
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted]`;
|
||||
const result = await hover(...getPositionFromCursor(input), {
|
||||
contextProviderConfig
|
||||
});
|
||||
expect(result).toEqual<Hover>({
|
||||
contents:
|
||||
"Information about the workflow run. For more information, see [`github` context](https://docs.github.com/actions/learn-github-actions/contexts#github-context).",
|
||||
range: {
|
||||
start: {line: 1, character: 14},
|
||||
end: {line: 1, character: 20}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("multiple expressions", async () => {
|
||||
const input = `on: push
|
||||
run-name: \${{ git|hub.event }}-\${{ github.event }}
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted]`;
|
||||
const result = await hover(...getPositionFromCursor(input), {
|
||||
contextProviderConfig
|
||||
});
|
||||
expect(result).toEqual<Hover>({
|
||||
contents:
|
||||
"Information about the workflow run. For more information, see [`github` context](https://docs.github.com/actions/learn-github-actions/contexts#github-context).",
|
||||
range: {
|
||||
start: {line: 1, character: 14},
|
||||
end: {line: 1, character: 20}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("multi-line expression", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted]
|
||||
steps:
|
||||
- run: |
|
||||
echo 'hello'
|
||||
echo '\${{ github.test.na|me }}
|
||||
echo 'world'
|
||||
echo '\${{ github.event.test }}`;
|
||||
const result = await hover(...getPositionFromCursor(input, 1), {
|
||||
contextProviderConfig
|
||||
});
|
||||
expect(result).toEqual<Hover>({
|
||||
contents: "Name for the test",
|
||||
range: {
|
||||
start: {line: 7, character: 18},
|
||||
end: {line: 7, character: 34}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import {StringToken} from "@github/actions-workflow-parser/templates/tokens/stri
|
||||
import {DescriptionProvider, hover, HoverConfig} from "./hover";
|
||||
import {getPositionFromCursor} from "./test-utils/cursor-position";
|
||||
|
||||
function testHoverConfig(tokenValue: string, tokenKey: string, description?: string) {
|
||||
export function testHoverConfig(tokenValue: string, tokenKey: string, description?: string) {
|
||||
return {
|
||||
descriptionProvider: {
|
||||
getDescription: async (_, token, __) => {
|
||||
|
||||
@@ -1,37 +1,69 @@
|
||||
import {DescriptionDictionary, Parser} from "@github/actions-expressions";
|
||||
import {FunctionInfo} from "@github/actions-expressions/funcs/info";
|
||||
import {Lexer} from "@github/actions-expressions/lexer";
|
||||
import {convertWorkflowTemplate, parseWorkflow, ParseWorkflowResult} from "@github/actions-workflow-parser";
|
||||
import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert";
|
||||
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
|
||||
import {TokenResult} from "./utils/find-token";
|
||||
import {File} from "@github/actions-workflow-parser/workflows/file";
|
||||
import {isString} from "@github/actions-workflow-parser/templates/tokens/type-guards";
|
||||
import {getCronDescription} from "@github/actions-workflow-parser/model/converter/cron";
|
||||
import {splitAllowedContext} from "@github/actions-workflow-parser/templates/allowed-context";
|
||||
import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token";
|
||||
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
|
||||
import {isBasicExpression, isString} from "@github/actions-workflow-parser/templates/tokens/type-guards";
|
||||
import {File} from "@github/actions-workflow-parser/workflows/file";
|
||||
import {Position, TextDocument} from "vscode-languageserver-textdocument";
|
||||
import {Hover} from "vscode-languageserver-types";
|
||||
import {ContextProviderConfig} from "./context-providers/config";
|
||||
import {getContext, Mode} from "./context-providers/default";
|
||||
import {getWorkflowContext, WorkflowContext} from "./context/workflow-context";
|
||||
import {ExpressionPos, mapToExpressionPos} from "./expression-hover/expression-pos";
|
||||
import {HoverVisitor} from "./expression-hover/visitor";
|
||||
import {validatorFunctions} from "./expression-validation/functions";
|
||||
import {info} from "./log";
|
||||
import {nullTrace} from "./nulltrace";
|
||||
import {findToken} from "./utils/find-token";
|
||||
import {isPotentiallyExpression} from "./utils/expression-detection";
|
||||
import {findToken, TokenResult} from "./utils/find-token";
|
||||
import {mapRange} from "./utils/range";
|
||||
import {getCronDescription} from "@github/actions-workflow-parser/model/converter/cron";
|
||||
|
||||
export type HoverConfig = {
|
||||
descriptionProvider?: DescriptionProvider;
|
||||
contextProviderConfig?: ContextProviderConfig;
|
||||
};
|
||||
|
||||
export type DescriptionProvider = {
|
||||
getDescription(context: WorkflowContext, token: TemplateToken, path: TemplateToken[]): Promise<string | undefined>;
|
||||
};
|
||||
|
||||
export type HoverConfig = {
|
||||
descriptionProvider?: DescriptionProvider;
|
||||
};
|
||||
|
||||
// Render value description and Context when hovering over a key in a MappingToken
|
||||
export async function hover(document: TextDocument, position: Position, config?: HoverConfig): Promise<Hover | null> {
|
||||
const file: File = {
|
||||
name: document.uri,
|
||||
content: document.getText()
|
||||
};
|
||||
const result = parseWorkflow(file.name, [file], nullTrace);
|
||||
const result = parseWorkflow(file, nullTrace);
|
||||
if (!result.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokenResult = findToken(position, result.value);
|
||||
const token = tokenResult.token;
|
||||
const {token, keyToken, parent} = tokenResult;
|
||||
|
||||
const tokenDefinitionInfo = (keyToken || parent || token)?.definitionInfo;
|
||||
if (token && tokenDefinitionInfo) {
|
||||
if (isBasicExpression(token) || isPotentiallyExpression(token)) {
|
||||
info(`Calculating expression hover for token with definition ${tokenDefinitionInfo.definition.key}`);
|
||||
|
||||
const allowedContext = tokenDefinitionInfo.allowedContext || [];
|
||||
const {namedContexts, functions} = splitAllowedContext(allowedContext);
|
||||
|
||||
const template = await convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
|
||||
const workflowContext = getWorkflowContext(document.uri, template, tokenResult.path);
|
||||
const context = await getContext(namedContexts, config?.contextProviderConfig, workflowContext, Mode.Completion);
|
||||
|
||||
const exprPos = mapToExpressionPos(token, position);
|
||||
if (exprPos) {
|
||||
return expressionHover(exprPos, context, namedContexts, functions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!token?.definition) {
|
||||
return null;
|
||||
}
|
||||
@@ -40,7 +72,7 @@ export async function hover(document: TextDocument, position: Position, config?:
|
||||
|
||||
if (tokenResult.parent && isCronMappingValue(tokenResult)) {
|
||||
const tokenValue = (token as StringToken).value;
|
||||
let description = getCronDescription(tokenValue);
|
||||
const description = getCronDescription(tokenValue);
|
||||
if (description) {
|
||||
return {
|
||||
contents: description,
|
||||
@@ -75,7 +107,7 @@ async function getDescription(
|
||||
return defaultDescription;
|
||||
}
|
||||
|
||||
const template = convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
|
||||
const workflowContext = getWorkflowContext(document.uri, template, path);
|
||||
const description = await config.descriptionProvider.getDescription(workflowContext, token, path);
|
||||
return description || defaultDescription;
|
||||
@@ -88,3 +120,47 @@ function isCronMappingValue(tokenResult: TokenResult): boolean {
|
||||
tokenResult.token.value !== "cron"
|
||||
);
|
||||
}
|
||||
|
||||
function expressionHover(
|
||||
exprPos: ExpressionPos,
|
||||
context: DescriptionDictionary,
|
||||
namedContexts: string[],
|
||||
functions: FunctionInfo[]
|
||||
): Hover | null {
|
||||
const {expression, position, documentRange} = exprPos;
|
||||
|
||||
try {
|
||||
const l = new Lexer(expression);
|
||||
const lr = l.lex();
|
||||
|
||||
const p = new Parser(lr.tokens, namedContexts, functions);
|
||||
const expr = p.parse();
|
||||
|
||||
const hv = new HoverVisitor(position, context, [], validatorFunctions);
|
||||
const hoverResult = hv.hover(expr);
|
||||
if (!hoverResult) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const exprRange = hoverResult.range;
|
||||
|
||||
return {
|
||||
contents: hoverResult?.description || hoverResult?.label,
|
||||
// Map the expression range back to a document range
|
||||
range: {
|
||||
start: {
|
||||
line: documentRange.start.line + exprRange.start.line,
|
||||
character: documentRange.start.character + exprRange.start.column
|
||||
},
|
||||
end: {
|
||||
line: documentRange.start.line + exprRange.end.line,
|
||||
character: documentRange.start.character + exprRange.end.column
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
// Hovering over an invalid expression should not cause an error here
|
||||
info(`Encountered error trying to calculate expression hover: ${e}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import {isString} from "@github/actions-workflow-parser";
|
||||
import {DefinitionType} from "@github/actions-workflow-parser/templates/schema/definition-type";
|
||||
import {StringDefinition} from "@github/actions-workflow-parser/templates/schema/string-definition";
|
||||
import {OPEN_EXPRESSION} from "@github/actions-workflow-parser/templates/template-constants";
|
||||
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/index";
|
||||
|
||||
export function isPotentiallyExpression(token: TemplateToken): boolean {
|
||||
const isAlwaysExpression =
|
||||
token.definition?.definitionType === DefinitionType.String && (token.definition as StringDefinition).isExpression;
|
||||
const containsExpression = isString(token) && token.value.indexOf(OPEN_EXPRESSION) >= 0;
|
||||
return isAlwaysExpression || containsExpression;
|
||||
}
|
||||
@@ -27,13 +27,10 @@ function testFindToken(input: string): {
|
||||
} {
|
||||
const [textDocument, pos] = getPositionFromCursor(input);
|
||||
const result = parseWorkflow(
|
||||
"wf.yaml",
|
||||
[
|
||||
{
|
||||
content: textDocument.getText(),
|
||||
name: "wf.yaml"
|
||||
}
|
||||
],
|
||||
{
|
||||
content: textDocument.getText(),
|
||||
name: "wf.yaml"
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import {TokenRange} from "@github/actions-workflow-parser/templates/tokens/token-range";
|
||||
import {Position} from "vscode-languageserver-textdocument";
|
||||
import {mapRange} from "./range";
|
||||
|
||||
export function getRelCharOffset(tokenRange: TokenRange, currentInput: string, pos: Position): number {
|
||||
const range = mapRange(tokenRange);
|
||||
if (range.start.line !== range.end.line) {
|
||||
const lines = currentInput.split("\n");
|
||||
const lineDiff = pos.line - range.start.line - 1;
|
||||
const linesBeforeCusor = lines.slice(0, lineDiff);
|
||||
return linesBeforeCusor.join("\n").length + pos.character + 1;
|
||||
} else {
|
||||
return pos.character - range.start.character;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {BasicExpressionToken} from "@github/actions-workflow-parser/templates/to
|
||||
import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token";
|
||||
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
|
||||
import {File} from "@github/actions-workflow-parser/workflows/file";
|
||||
import {FileProvider} from "@github/actions-workflow-parser/workflows/file-provider";
|
||||
import {TextDocument} from "vscode-languageserver-textdocument";
|
||||
import {Diagnostic, DiagnosticSeverity, URI} from "vscode-languageserver-types";
|
||||
import {ActionInputs, ActionReference} from "./action";
|
||||
@@ -35,6 +36,7 @@ export type ValidationConfig = {
|
||||
valueProviderConfig?: ValueProviderConfig;
|
||||
contextProviderConfig?: ContextProviderConfig;
|
||||
getActionInputs?(action: ActionReference): Promise<ActionInputs | undefined>;
|
||||
fileProvider?: FileProvider;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -43,11 +45,7 @@ export type ValidationConfig = {
|
||||
* @param textDocument Document to validate
|
||||
* @returns Array of diagnostics
|
||||
*/
|
||||
export async function validate(
|
||||
textDocument: TextDocument,
|
||||
config?: ValidationConfig
|
||||
// TODO: Support multiple files, context for API calls
|
||||
): Promise<Diagnostic[]> {
|
||||
export async function validate(textDocument: TextDocument, config?: ValidationConfig): Promise<Diagnostic[]> {
|
||||
const file: File = {
|
||||
name: textDocument.uri,
|
||||
content: textDocument.getText()
|
||||
@@ -56,10 +54,18 @@ export async function validate(
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
|
||||
try {
|
||||
const result: ParseWorkflowResult = parseWorkflow(file.name, [file], nullTrace);
|
||||
const result: ParseWorkflowResult = parseWorkflow(file, nullTrace);
|
||||
if (result.value) {
|
||||
// Errors will be updated in the context. Attempt to do the conversion anyway in order to give the user more information
|
||||
const template = convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(
|
||||
result.context,
|
||||
result.value,
|
||||
ErrorPolicy.TryConversion,
|
||||
config?.fileProvider,
|
||||
{
|
||||
fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0
|
||||
}
|
||||
);
|
||||
|
||||
// Validate expressions and value providers
|
||||
await additionalValidations(diagnostics, textDocument.uri, template, result.value, config);
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import {FileProvider} from "@github/actions-workflow-parser/workflows/file-provider";
|
||||
import {fileIdentifier} from "@github/actions-workflow-parser/workflows/file-reference";
|
||||
import {createDocument} from "./test-utils/document";
|
||||
import {validate} from "./validate";
|
||||
|
||||
const testFileProvider: FileProvider = {
|
||||
getFileContent: async ref => {
|
||||
switch (fileIdentifier(ref)) {
|
||||
case "monalisa/octocat/workflow.yaml@main":
|
||||
return {
|
||||
name: "monalisa/octocat/workflow.yaml",
|
||||
content: `
|
||||
on: workflow_call
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
`
|
||||
};
|
||||
|
||||
case "monalisa/octocat/.github/workflows/non-reusable-workflow.yaml@main":
|
||||
return {
|
||||
name: "monalisa/octocat/.github/workflows/non-reusable-workflow.yaml",
|
||||
content: `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
`
|
||||
};
|
||||
|
||||
case "./reusable-workflow.yaml":
|
||||
return {
|
||||
name: "reusable-workflow.yaml",
|
||||
content: `
|
||||
on: workflow_call
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
`
|
||||
};
|
||||
|
||||
case "./reusable-workflow-with-inputs.yaml":
|
||||
return {
|
||||
name: "reusable-workflow-with-inputs.yaml",
|
||||
content: `
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
username:
|
||||
description: 'A username passed from the caller workflow'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
`
|
||||
};
|
||||
|
||||
default:
|
||||
throw new Error("File not found");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe("workflow references validation", () => {
|
||||
it("invalid workflow reference", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: monalisa/octocat/.github/workflows/non-reusable-workflow.yaml@main
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input), {
|
||||
fileProvider: testFileProvider
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "workflow_call key is not defined in the referenced workflow.",
|
||||
range: {
|
||||
start: {
|
||||
character: 10,
|
||||
line: 5
|
||||
},
|
||||
end: {
|
||||
character: 76,
|
||||
line: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("reference to a non-reusable workflow", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: monalisa/octocat/workflow.yaml@not-a-branch
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input), {
|
||||
fileProvider: testFileProvider
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Unable to find reusable workflow",
|
||||
range: {
|
||||
start: {
|
||||
character: 10,
|
||||
line: 5
|
||||
},
|
||||
end: {
|
||||
character: 53,
|
||||
line: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("valid reference to a reusable workflow", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: monalisa/octocat/workflow.yaml@main
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input), {
|
||||
fileProvider: testFileProvider
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("valid reference to a local workflow", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./reusable-workflow.yaml
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input), {
|
||||
fileProvider: testFileProvider
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("workflow reference without required inputs", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./reusable-workflow-with-inputs.yaml
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input), {
|
||||
fileProvider: testFileProvider
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Input username is required, but not provided while calling.",
|
||||
range: {
|
||||
start: {
|
||||
character: 10,
|
||||
line: 5
|
||||
},
|
||||
end: {
|
||||
character: 46,
|
||||
line: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("workflow reference with required inputs", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./reusable-workflow-with-inputs.yaml
|
||||
with:
|
||||
username: monalisa
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input), {
|
||||
fileProvider: testFileProvider
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
dist
|
||||
*.md
|
||||
*.js
|
||||
*.json
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@github/actions-workflow-parser",
|
||||
"version": "0.1.113",
|
||||
"version": "0.1.126",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"source": "./src/index.ts",
|
||||
@@ -40,7 +40,7 @@
|
||||
"watch": "tsc --build tsconfig.build.json --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@github/actions-expressions": "^0.1.113",
|
||||
"@github/actions-expressions": "^0.1.126",
|
||||
"cronstrue": "^2.21.0",
|
||||
"yaml": "^2.0.0-8"
|
||||
},
|
||||
|
||||
@@ -6,19 +6,16 @@ import {parseWorkflow} from "./workflows/workflow-parser";
|
||||
describe("Workflow Expression Parsing", () => {
|
||||
it("preserves original expressions when building format", () => {
|
||||
const result = parseWorkflow(
|
||||
"test.yaml",
|
||||
[
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
run-name: Test \${{ github.event_name }} \${{ github.ref }}
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'hello'`
|
||||
}
|
||||
],
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
@@ -44,20 +41,17 @@ jobs:
|
||||
|
||||
it("preserves original expressions when building format for multi-line strings", () => {
|
||||
const result = parseWorkflow(
|
||||
"test.yaml",
|
||||
[
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo \${{ github.event_name }}
|
||||
echo 'hello' \${{ github.ref }}`
|
||||
}
|
||||
],
|
||||
echo 'hello' \${{github.ref }}`
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
@@ -81,19 +75,27 @@ jobs:
|
||||
}
|
||||
|
||||
expect(stepRun.originalExpressions).toHaveLength(2);
|
||||
expect(stepRun.originalExpressions!.map(x => [x.toDisplayString(), x.range])).toEqual([
|
||||
expect(stepRun.originalExpressions!.map(x => [x.toDisplayString(), x.range, x.expressionRange])).toEqual([
|
||||
[
|
||||
"${{ github.event_name }}",
|
||||
{
|
||||
start: {line: 7, column: 16},
|
||||
end: {line: 7, column: 40}
|
||||
},
|
||||
{
|
||||
start: {line: 7, column: 20},
|
||||
end: {line: 7, column: 37}
|
||||
}
|
||||
],
|
||||
[
|
||||
"${{ github.ref }}",
|
||||
{
|
||||
start: {line: 8, column: 24},
|
||||
end: {line: 8, column: 41}
|
||||
end: {line: 8, column: 40}
|
||||
},
|
||||
{
|
||||
start: {line: 8, column: 27},
|
||||
end: {line: 8, column: 37}
|
||||
}
|
||||
]
|
||||
]);
|
||||
@@ -101,11 +103,9 @@ jobs:
|
||||
|
||||
it("return errors and string token with preserved expressions for (multiple) expression errors", () => {
|
||||
const result = parseWorkflow(
|
||||
"test.yaml",
|
||||
[
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -113,8 +113,7 @@ jobs:
|
||||
- run: |
|
||||
echo \${{ abc }}
|
||||
echo 'hello' \${{ gith }}`
|
||||
}
|
||||
],
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
@@ -139,11 +138,9 @@ jobs:
|
||||
|
||||
it("reports all errors for multi-line expressions at the correct locations", () => {
|
||||
const result = parseWorkflow(
|
||||
"test.yaml",
|
||||
[
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -151,8 +148,7 @@ jobs:
|
||||
- run: |
|
||||
echo \${{ fromJSON2('test') }}
|
||||
echo 'hello' \${{ toJSON2(inputs.test) }}`
|
||||
}
|
||||
],
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
@@ -178,19 +174,16 @@ jobs:
|
||||
|
||||
it("parses isExpression strings into expression tokens", () => {
|
||||
const result = parseWorkflow(
|
||||
"test.yaml",
|
||||
[
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push'
|
||||
steps:
|
||||
- run: echo 'hello'`
|
||||
}
|
||||
],
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
|
||||
@@ -5,13 +5,10 @@ import {parseWorkflow} from "./workflows/workflow-parser";
|
||||
describe("parseWorkflow", () => {
|
||||
it("parses valid workflow", () => {
|
||||
const result = parseWorkflow(
|
||||
"test.yaml",
|
||||
[
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: "on: push\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - run: echo 'hello'"
|
||||
}
|
||||
],
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: "on: push\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - run: echo 'hello'"
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
@@ -20,13 +17,10 @@ describe("parseWorkflow", () => {
|
||||
|
||||
it("contains range for error", () => {
|
||||
const result = parseWorkflow(
|
||||
"test.yaml",
|
||||
[
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: "on: push\njobs:\n build:\n steps:\n - run: echo 'hello'"
|
||||
}
|
||||
],
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: "on: push\njobs:\n build:\n steps:\n - run: echo 'hello'"
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
@@ -40,19 +34,16 @@ describe("parseWorkflow", () => {
|
||||
|
||||
it("error range for expression is constrained to scalar node", () => {
|
||||
const result = parseWorkflow(
|
||||
"test.yaml",
|
||||
[
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: \${{ github.event = 12 }}
|
||||
run: echo 'hello'`
|
||||
}
|
||||
],
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
@@ -71,14 +62,11 @@ jobs:
|
||||
|
||||
it("tokens contain descriptions", () => {
|
||||
const result = parseWorkflow(
|
||||
"test.yaml",
|
||||
[
|
||||
{
|
||||
name: "test.yaml",
|
||||
content:
|
||||
"on: push\nname: hello\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - run: echo 'hello'"
|
||||
}
|
||||
],
|
||||
{
|
||||
name: "test.yaml",
|
||||
content:
|
||||
"on: push\nname: hello\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - run: echo 'hello'"
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
|
||||
@@ -7,22 +7,19 @@ function serializeTemplate(template: unknown): unknown {
|
||||
}
|
||||
|
||||
describe("convertWorkflowTemplate", () => {
|
||||
it("converts workflow with one job", () => {
|
||||
it("converts workflow with one job", async () => {
|
||||
const result = parseWorkflow(
|
||||
"wf.yaml",
|
||||
[
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: `on: push
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest`
|
||||
}
|
||||
],
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
const template = convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
|
||||
expect(serializeTemplate(template)).toEqual({
|
||||
events: {
|
||||
@@ -46,13 +43,11 @@ jobs:
|
||||
});
|
||||
});
|
||||
|
||||
it("converts workflow if expressions", () => {
|
||||
it("converts workflow if expressions", async () => {
|
||||
const result = parseWorkflow(
|
||||
"wf.yaml",
|
||||
[
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: `on: push
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
if: \${{ true }}
|
||||
@@ -60,12 +55,11 @@ jobs:
|
||||
deploy:
|
||||
if: true
|
||||
runs-on: ubuntu-latest`
|
||||
}
|
||||
],
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
const template = convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
|
||||
expect(serializeTemplate(template)).toEqual({
|
||||
events: {
|
||||
@@ -98,23 +92,20 @@ jobs:
|
||||
});
|
||||
});
|
||||
|
||||
it("converts workflow with empty needs", () => {
|
||||
it("converts workflow with empty needs", async () => {
|
||||
const result = parseWorkflow(
|
||||
"wf.yaml",
|
||||
[
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: `on: push
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
needs: # comment to preserve whitespace in test
|
||||
runs-on: ubuntu-latest`
|
||||
}
|
||||
],
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
const template = convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
|
||||
expect(serializeTemplate(template)).toEqual({
|
||||
errors: [
|
||||
@@ -143,13 +134,11 @@ jobs:
|
||||
});
|
||||
});
|
||||
|
||||
it("converts workflow with needs errors", () => {
|
||||
it("converts workflow with needs errors", async () => {
|
||||
const result = parseWorkflow(
|
||||
"wf.yaml",
|
||||
[
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: `on: push
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
job1:
|
||||
needs: [unknown-job, job3]
|
||||
@@ -159,12 +148,11 @@ jobs:
|
||||
job3:
|
||||
needs: job1
|
||||
runs-on: ubuntu-latest`
|
||||
}
|
||||
],
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
const template = convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
|
||||
expect(serializeTemplate(template)).toEqual({
|
||||
errors: [
|
||||
@@ -223,13 +211,11 @@ jobs:
|
||||
});
|
||||
});
|
||||
|
||||
it("converts workflow with invalid on", () => {
|
||||
it("converts workflow with invalid on", async () => {
|
||||
const result = parseWorkflow(
|
||||
"wf.yaml",
|
||||
[
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: `on:
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: `on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test:
|
||||
@@ -239,12 +225,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hello`
|
||||
}
|
||||
],
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
const template = convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
|
||||
expect(template.jobs).not.toBeUndefined();
|
||||
expect(template.jobs).toHaveLength(1);
|
||||
@@ -286,21 +271,18 @@ jobs:
|
||||
});
|
||||
});
|
||||
|
||||
it("converts workflow with invalid jobs", () => {
|
||||
it("converts workflow with invalid jobs", async () => {
|
||||
const result = parseWorkflow(
|
||||
"wf.yaml",
|
||||
[
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: `on: push
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:`
|
||||
}
|
||||
],
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
const template = convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
|
||||
expect(template.jobs).not.toBeUndefined();
|
||||
expect(template.jobs).toHaveLength(0);
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import {TemplateContext} from "../templates/template-context";
|
||||
import {TemplateToken, TemplateTokenError} from "../templates/tokens/template-token";
|
||||
import {FileProvider} from "../workflows/file-provider";
|
||||
import {parseFileReference} from "../workflows/file-reference";
|
||||
import {parseWorkflow} from "../workflows/workflow-parser";
|
||||
import {convertConcurrency} from "./converter/concurrency";
|
||||
import {convertOn} from "./converter/events";
|
||||
import {handleTemplateTokenErrors} from "./converter/handle-errors";
|
||||
import {convertJobs} from "./converter/jobs";
|
||||
import {convertReferencedWorkflow} from "./converter/referencedWorkflow";
|
||||
import {isReusableWorkflowJob} from "./type-guards";
|
||||
import {WorkflowTemplate} from "./workflow-template";
|
||||
|
||||
export enum ErrorPolicy {
|
||||
@@ -11,11 +16,35 @@ export enum ErrorPolicy {
|
||||
TryConversion
|
||||
}
|
||||
|
||||
export function convertWorkflowTemplate(
|
||||
export type WorkflowTemplateConverterOptions = {
|
||||
/**
|
||||
* The maximum depth of reusable workflows allowed in the workflow.
|
||||
* If this depth is exceeded, an error will be reported.
|
||||
* If {@link fetchReusableWorkflowDepth} is less than this value, the maximum depth
|
||||
* won't be enforced.
|
||||
* Default: 4
|
||||
*/
|
||||
maxReusableWorkflowDepth?: number;
|
||||
/**
|
||||
* The depth to fetch reusable workflows, up to {@link maxReusableWorkflowDepth}.
|
||||
* Currently only a fetch depth of 0 or 1 is supported.
|
||||
* Default: 0
|
||||
*/
|
||||
fetchReusableWorkflowDepth?: number;
|
||||
};
|
||||
|
||||
const defaultOptions: Required<WorkflowTemplateConverterOptions> = {
|
||||
maxReusableWorkflowDepth: 4,
|
||||
fetchReusableWorkflowDepth: 0
|
||||
};
|
||||
|
||||
export async function convertWorkflowTemplate(
|
||||
context: TemplateContext,
|
||||
root: TemplateToken,
|
||||
errorPolicy: ErrorPolicy = ErrorPolicy.ReturnErrorsOnly
|
||||
): WorkflowTemplate {
|
||||
errorPolicy: ErrorPolicy = ErrorPolicy.ReturnErrorsOnly,
|
||||
fileProvider?: FileProvider,
|
||||
options: WorkflowTemplateConverterOptions = defaultOptions
|
||||
): Promise<WorkflowTemplate> {
|
||||
const result = {} as WorkflowTemplate;
|
||||
|
||||
if (context.errors.getErrors().length > 0 && errorPolicy === ErrorPolicy.ReturnErrorsOnly) {
|
||||
@@ -25,6 +54,12 @@ export function convertWorkflowTemplate(
|
||||
return result;
|
||||
}
|
||||
|
||||
const opts = getOptionsWithDefaults(options);
|
||||
|
||||
if (fileProvider === undefined && opts.fetchReusableWorkflowDepth > 0) {
|
||||
context.error(root, new Error("A file provider is required to fetch reusable workflows"));
|
||||
}
|
||||
|
||||
try {
|
||||
const rootMapping = root.assertMapping("root");
|
||||
|
||||
@@ -49,6 +84,31 @@ export function convertWorkflowTemplate(
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Load referenced workflows
|
||||
for (const job of result.jobs || []) {
|
||||
if (isReusableWorkflowJob(job)) {
|
||||
if (opts.maxReusableWorkflowDepth === 0) {
|
||||
context.error(job.ref, new Error("Reusable workflows are not allowed"));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (opts.fetchReusableWorkflowDepth === 0 || fileProvider === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const file = await fileProvider.getFileContent(parseFileReference(job.ref.value));
|
||||
const workflow = parseWorkflow(file, context);
|
||||
if (!workflow.value) {
|
||||
continue;
|
||||
}
|
||||
convertReferencedWorkflow(context, workflow.value, job);
|
||||
} catch {
|
||||
context.error(job.ref, new Error("Unable to find reusable workflow"));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof TemplateTokenError) {
|
||||
context.error(err.token, err);
|
||||
@@ -66,3 +126,16 @@ export function convertWorkflowTemplate(
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getOptionsWithDefaults(options: WorkflowTemplateConverterOptions): Required<WorkflowTemplateConverterOptions> {
|
||||
return {
|
||||
maxReusableWorkflowDepth:
|
||||
options.maxReusableWorkflowDepth !== undefined
|
||||
? options.maxReusableWorkflowDepth
|
||||
: defaultOptions.maxReusableWorkflowDepth,
|
||||
fetchReusableWorkflowDepth:
|
||||
options.fetchReusableWorkflowDepth !== undefined
|
||||
? options.fetchReusableWorkflowDepth
|
||||
: defaultOptions.fetchReusableWorkflowDepth
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import {TemplateContext} from "../../templates/template-context";
|
||||
import {StringToken, MappingToken, BasicExpressionToken, TemplateToken, ScalarToken} from "../../templates/tokens";
|
||||
import {isSequence, isString} from "../../templates/tokens/type-guards";
|
||||
import {WorkflowJob, Step} from "../workflow-template";
|
||||
import {convertConcurrency} from "./concurrency";
|
||||
import {convertToJobContainer, convertToJobServices} from "./container";
|
||||
import {handleTemplateTokenErrors} from "./handle-errors";
|
||||
import {IdBuilder} from "./id-builder";
|
||||
import {convertToActionsEnvironmentRef} from "./job/environment";
|
||||
import {convertRunsOn} from "./job/runs-on";
|
||||
import {convertSteps} from "./steps";
|
||||
|
||||
export function convertJob(context: TemplateContext, jobKey: StringToken, token: MappingToken): WorkflowJob {
|
||||
const error = new IdBuilder().tryAddKnownId(jobKey.value);
|
||||
if (error) {
|
||||
context.error(jobKey, error);
|
||||
}
|
||||
|
||||
let concurrency, container, env, environment, name, outputs, runsOn, services, strategy: TemplateToken | undefined;
|
||||
let needs: StringToken[] | undefined = undefined;
|
||||
let steps: Step[] = [];
|
||||
let workflowJobRef: StringToken | undefined;
|
||||
let workflowJobInputs: MappingToken | undefined;
|
||||
|
||||
for (const item of token) {
|
||||
const propertyName = item.key.assertString("job property name");
|
||||
switch (propertyName.value) {
|
||||
case "concurrency":
|
||||
handleTemplateTokenErrors(item.value, context, undefined, () => convertConcurrency(context, item.value));
|
||||
concurrency = item.value;
|
||||
break;
|
||||
|
||||
case "container":
|
||||
convertToJobContainer(context, item.value);
|
||||
container = item.value;
|
||||
break;
|
||||
|
||||
case "env":
|
||||
env = item.value.assertMapping("job env");
|
||||
break;
|
||||
|
||||
case "environment":
|
||||
handleTemplateTokenErrors(item.value, context, undefined, () =>
|
||||
convertToActionsEnvironmentRef(context, item.value)
|
||||
);
|
||||
environment = item.value;
|
||||
break;
|
||||
|
||||
case "name":
|
||||
name = item.value.assertScalar("job name");
|
||||
break;
|
||||
|
||||
case "needs": {
|
||||
needs = [];
|
||||
if (isString(item.value)) {
|
||||
const jobNeeds = item.value.assertString("job needs id");
|
||||
needs.push(jobNeeds);
|
||||
}
|
||||
|
||||
if (isSequence(item.value)) {
|
||||
for (const seqItem of item.value) {
|
||||
const jobNeeds = seqItem.assertString("job needs id");
|
||||
needs.push(jobNeeds);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "outputs":
|
||||
outputs = item.value.assertMapping("job outputs");
|
||||
break;
|
||||
|
||||
case "runs-on":
|
||||
handleTemplateTokenErrors(item.value, context, undefined, () => convertRunsOn(context, item.value));
|
||||
runsOn = item.value;
|
||||
break;
|
||||
|
||||
case "services":
|
||||
convertToJobServices(context, item.value);
|
||||
services = item.value;
|
||||
break;
|
||||
|
||||
case "steps":
|
||||
steps = convertSteps(context, item.value);
|
||||
break;
|
||||
|
||||
case "strategy":
|
||||
strategy = item.value;
|
||||
break;
|
||||
|
||||
case "uses":
|
||||
workflowJobRef = item.value.assertString("job uses value");
|
||||
break;
|
||||
|
||||
case "with":
|
||||
workflowJobInputs = item.value.assertMapping("uses-with value");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (workflowJobRef !== undefined) {
|
||||
return {
|
||||
type: "reusableWorkflowJob",
|
||||
id: jobKey,
|
||||
name: jobName(name, jobKey),
|
||||
needs: needs ?? [],
|
||||
if: new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined),
|
||||
ref: workflowJobRef,
|
||||
"input-definitions": undefined,
|
||||
"input-values": workflowJobInputs,
|
||||
outputs: undefined,
|
||||
concurrency,
|
||||
strategy
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: "job",
|
||||
id: jobKey,
|
||||
name: jobName(name, jobKey),
|
||||
needs,
|
||||
if: new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined),
|
||||
env,
|
||||
concurrency,
|
||||
environment,
|
||||
strategy,
|
||||
"runs-on": runsOn,
|
||||
container,
|
||||
services,
|
||||
outputs,
|
||||
steps
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function jobName(name: ScalarToken | undefined, jobKey: StringToken): ScalarToken {
|
||||
if (name === undefined) {
|
||||
return jobKey;
|
||||
}
|
||||
|
||||
if (isString(name) && name.value === "") {
|
||||
return jobKey;
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import {TemplateContext} from "../../../templates/template-context";
|
||||
import {MappingToken, TemplateToken} from "../../../templates/tokens";
|
||||
import {ReusableWorkflowJob} from "../../workflow-template";
|
||||
|
||||
type TokenMap = Map<string, [key: string, value: TemplateToken]>;
|
||||
|
||||
export function convertWorkflowJobInputs(context: TemplateContext, job: ReusableWorkflowJob) {
|
||||
const inputDefinitions = createTokenMap(
|
||||
job["input-definitions"]?.assertMapping("workflow job input definitions"),
|
||||
"inputs"
|
||||
);
|
||||
|
||||
const inputValues = createTokenMap(job["input-values"]?.assertMapping("workflow job input values"), "with");
|
||||
|
||||
if (inputDefinitions !== undefined) {
|
||||
for (const [_, [name, value]] of inputDefinitions) {
|
||||
const inputSpec = createTokenMap(value.assertMapping(`input ${name}`), `input ${name} key`)!;
|
||||
|
||||
const inputTypeToken = inputSpec.get("type")?.[1];
|
||||
if (!inputTypeToken) {
|
||||
// This should be validated by the template reader per the schema
|
||||
continue;
|
||||
}
|
||||
|
||||
const inputSet = inputValues !== undefined && inputValues.has(name.toLowerCase());
|
||||
const required = inputSpec.get("required")?.[1].assertBoolean(`input ${name} required`).value;
|
||||
if (required && !inputSet) {
|
||||
context.error(job.ref, `Input ${name} is required, but not provided while calling.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inputValues !== undefined) {
|
||||
for (const [_, [name, value]] of inputValues) {
|
||||
if (!inputDefinitions?.has(name.toLowerCase())) {
|
||||
context.error(value, `Invalid input, ${name} is not defined in the referenced workflow.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createTokenMap(mapping: MappingToken | undefined, description: string): TokenMap | undefined {
|
||||
if (!mapping) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result = new Map<string, [key: string, value: TemplateToken]>();
|
||||
for (const item of mapping) {
|
||||
const name = item.key.assertString(`${description} key`);
|
||||
result.set(name.value.toLowerCase(), [name.value, item.value]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import {TemplateContext} from "../../../templates/template-context";
|
||||
import {TemplateToken} from "../../../templates/tokens";
|
||||
import {isMapping, isString, isSequence} from "../../../templates/tokens/type-guards";
|
||||
|
||||
type RunsOn = {
|
||||
labels: Set<string>;
|
||||
group: string;
|
||||
};
|
||||
|
||||
export function convertRunsOn(context: TemplateContext, token: TemplateToken): RunsOn {
|
||||
const labels = convertRunsOnLabels(token);
|
||||
|
||||
if (!isMapping(token)) {
|
||||
return {
|
||||
labels,
|
||||
group: ""
|
||||
};
|
||||
}
|
||||
|
||||
let group = "";
|
||||
|
||||
for (const item of token) {
|
||||
const key = item.key.assertString("job runs-on property name");
|
||||
switch (key.value) {
|
||||
case "group": {
|
||||
if (item.value.isExpression) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const groupName = item.value.assertString("job runs-on group name").value;
|
||||
const names = groupName.split("/");
|
||||
switch (names.length) {
|
||||
case 1: {
|
||||
group = groupName;
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
if (!["org", "organization", "ent", "enterprise"].includes(names[0])) {
|
||||
context.error(
|
||||
item.value,
|
||||
`Invalid runs-on group name '${groupName}. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.'`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!names[1]) {
|
||||
context.error(item.value, `Invalid runs-on group name '${groupName}'.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
group = groupName;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
context.error(
|
||||
item.value,
|
||||
`Invalid runs-on group name '${groupName}. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.'`
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "labels": {
|
||||
const mapLabels = convertRunsOnLabels(item.value);
|
||||
for (const label of mapLabels) {
|
||||
labels.add(label);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
labels,
|
||||
group
|
||||
};
|
||||
}
|
||||
|
||||
function convertRunsOnLabels(token: TemplateToken): Set<string> {
|
||||
const labels = new Set<string>();
|
||||
if (token.isExpression) {
|
||||
return labels;
|
||||
}
|
||||
|
||||
if (isString(token)) {
|
||||
labels.add(token.value);
|
||||
return labels;
|
||||
}
|
||||
|
||||
if (isSequence(token)) {
|
||||
for (const item of token) {
|
||||
if (item.isExpression) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = item.assertString("job runs-on label sequence item");
|
||||
labels.add(label.value);
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
@@ -1,23 +1,19 @@
|
||||
import {TemplateContext} from "../../templates/template-context";
|
||||
import {BasicExpressionToken, MappingToken, StringToken} from "../../templates/tokens";
|
||||
import {StringToken} from "../../templates/tokens";
|
||||
import {TemplateToken} from "../../templates/tokens/template-token";
|
||||
import {isMapping, isSequence, isString} from "../../templates/tokens/type-guards";
|
||||
import {Job} from "../workflow-template";
|
||||
import {convertConcurrency} from "./concurrency";
|
||||
import {convertToJobContainer, convertToJobServices} from "./container";
|
||||
import {isMapping} from "../../templates/tokens/type-guards";
|
||||
import {WorkflowJob} from "../workflow-template";
|
||||
import {handleTemplateTokenErrors} from "./handle-errors";
|
||||
import {IdBuilder} from "./id-builder";
|
||||
import {convertToActionsEnvironmentRef} from "./job/environment";
|
||||
import {convertSteps} from "./steps";
|
||||
import {convertJob} from "./job";
|
||||
|
||||
type nodeInfo = {
|
||||
name: string;
|
||||
needs: StringToken[];
|
||||
};
|
||||
|
||||
export function convertJobs(context: TemplateContext, token: TemplateToken): Job[] {
|
||||
export function convertJobs(context: TemplateContext, token: TemplateToken): WorkflowJob[] {
|
||||
if (isMapping(token)) {
|
||||
const result: Job[] = [];
|
||||
const result: WorkflowJob[] = [];
|
||||
const jobsWithSatisfiedNeeds: nodeInfo[] = [];
|
||||
const alljobsWithUnsatisfiedNeeds: nodeInfo[] = [];
|
||||
|
||||
@@ -53,7 +49,7 @@ export function convertJobs(context: TemplateContext, token: TemplateToken): Job
|
||||
function validateNeeds(
|
||||
token: TemplateToken,
|
||||
context: TemplateContext,
|
||||
result: Job[],
|
||||
result: WorkflowJob[],
|
||||
jobsWithSatisfiedNeeds: nodeInfo[],
|
||||
alljobsWithUnsatisfiedNeeds: nodeInfo[]
|
||||
) {
|
||||
@@ -101,199 +97,3 @@ function validateNeeds(
|
||||
}
|
||||
}
|
||||
|
||||
function convertJob(context: TemplateContext, jobKey: StringToken, token: MappingToken): Job {
|
||||
const error = new IdBuilder().tryAddKnownId(jobKey.value);
|
||||
if (error) {
|
||||
context.error(jobKey, error);
|
||||
}
|
||||
const result: Job = {
|
||||
type: "job",
|
||||
id: jobKey,
|
||||
name: undefined,
|
||||
needs: undefined,
|
||||
if: new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, "success()"),
|
||||
env: undefined,
|
||||
concurrency: undefined,
|
||||
environment: undefined,
|
||||
strategy: undefined,
|
||||
"runs-on": undefined,
|
||||
container: undefined,
|
||||
services: undefined,
|
||||
outputs: undefined,
|
||||
steps: []
|
||||
};
|
||||
|
||||
for (const item of token) {
|
||||
const propertyName = item.key.assertString("job property name");
|
||||
switch (propertyName.value) {
|
||||
case "concurrency":
|
||||
handleTemplateTokenErrors(item.value, context, undefined, () => convertConcurrency(context, item.value));
|
||||
result.concurrency = item.value;
|
||||
break;
|
||||
|
||||
case "container":
|
||||
// Do early validation, but don't convert
|
||||
convertToJobContainer(context, item.value);
|
||||
result.container = item.value;
|
||||
break;
|
||||
|
||||
case "env":
|
||||
result.env = item.value.assertMapping("job env");
|
||||
break;
|
||||
|
||||
case "environment":
|
||||
handleTemplateTokenErrors(item.value, context, undefined, () =>
|
||||
convertToActionsEnvironmentRef(context, item.value)
|
||||
);
|
||||
result.environment = item.value;
|
||||
break;
|
||||
|
||||
case "name":
|
||||
result.name = item.value.assertScalar("job name");
|
||||
break;
|
||||
|
||||
case "needs":
|
||||
result.needs = [];
|
||||
if (isString(item.value)) {
|
||||
const jobNeeds = item.value.assertString("job needs id");
|
||||
result.needs.push(jobNeeds);
|
||||
}
|
||||
|
||||
if (isSequence(item.value)) {
|
||||
for (const seqItem of item.value) {
|
||||
const jobNeeds = seqItem.assertString("job needs id");
|
||||
result.needs.push(jobNeeds);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "outputs":
|
||||
result.outputs = item.value.assertMapping("job outputs");
|
||||
break;
|
||||
|
||||
case "runs-on":
|
||||
handleTemplateTokenErrors(item.value, context, undefined, () => convertRunsOn(context, item.value));
|
||||
result["runs-on"] = item.value;
|
||||
break;
|
||||
|
||||
case "services":
|
||||
// Do early validation, but don't convert
|
||||
convertToJobServices(context, item.value);
|
||||
result.services = item.value;
|
||||
break;
|
||||
|
||||
case "steps":
|
||||
result.steps = convertSteps(context, item.value);
|
||||
break;
|
||||
|
||||
case "strategy":
|
||||
result.strategy = item.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.name) {
|
||||
result.name = result.id;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
type RunsOn = {
|
||||
labels: Set<string>;
|
||||
group: string;
|
||||
};
|
||||
|
||||
function convertRunsOn(context: TemplateContext, token: TemplateToken): RunsOn {
|
||||
const labels = convertRunsOnLabels(token);
|
||||
|
||||
if (!isMapping(token)) {
|
||||
return {
|
||||
labels,
|
||||
group: ""
|
||||
};
|
||||
}
|
||||
|
||||
let group = "";
|
||||
|
||||
for (const item of token) {
|
||||
const key = item.key.assertString("job runs-on property name");
|
||||
switch (key.value) {
|
||||
case "group": {
|
||||
if (item.value.isExpression) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const groupName = item.value.assertString("job runs-on group name").value;
|
||||
const names = groupName.split("/");
|
||||
switch (names.length) {
|
||||
case 1: {
|
||||
group = groupName;
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
if (!["org", "organization", "ent", "enterprise"].includes(names[0])) {
|
||||
context.error(
|
||||
item.value,
|
||||
`Invalid runs-on group name '${groupName}. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.'`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!names[1]) {
|
||||
context.error(item.value, `Invalid runs-on group name '${groupName}'.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
group = groupName;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
context.error(
|
||||
item.value,
|
||||
`Invalid runs-on group name '${groupName}. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.'`
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "labels": {
|
||||
const mapLabels = convertRunsOnLabels(item.value);
|
||||
for (const label of mapLabels) {
|
||||
labels.add(label);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
labels,
|
||||
group
|
||||
};
|
||||
}
|
||||
|
||||
function convertRunsOnLabels(token: TemplateToken): Set<string> {
|
||||
const labels = new Set<string>();
|
||||
if (token.isExpression) {
|
||||
return labels;
|
||||
}
|
||||
|
||||
if (isString(token)) {
|
||||
labels.add(token.value);
|
||||
return labels;
|
||||
}
|
||||
|
||||
if (isSequence(token)) {
|
||||
for (const item of token) {
|
||||
if (item.isExpression) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = item.assertString("job runs-on label sequence item");
|
||||
labels.add(label.value);
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import {TemplateContext} from "../../templates/template-context";
|
||||
import {TemplateToken} from "../../templates/tokens";
|
||||
import {TokenType} from "../../templates/tokens/types";
|
||||
import {ReusableWorkflowJob} from "../workflow-template";
|
||||
import {handleTemplateTokenErrors} from "./handle-errors";
|
||||
import {convertWorkflowJobInputs} from "./job/inputs";
|
||||
import {convertJobs} from "./jobs";
|
||||
|
||||
export function convertReferencedWorkflow(
|
||||
context: TemplateContext,
|
||||
referencedWorkflow: TemplateToken,
|
||||
job: ReusableWorkflowJob
|
||||
) {
|
||||
const mapping = referencedWorkflow.assertMapping("root");
|
||||
|
||||
// The language service doesn't currently handles on other documents,
|
||||
// So use the ref in the original workflow as the error location
|
||||
const tokenForErrors = job.ref;
|
||||
|
||||
for (const pair of mapping) {
|
||||
const key = pair.key.assertString("root key");
|
||||
switch (key.value) {
|
||||
case "on": {
|
||||
handleTemplateTokenErrors(tokenForErrors, context, undefined, () =>
|
||||
convertReferencedWorkflowOn(context, pair.value, job)
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "jobs": {
|
||||
job.jobs = handleTemplateTokenErrors(tokenForErrors, context, [], () => convertJobs(context, pair.value));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function convertReferencedWorkflowOn(context: TemplateContext, on: TemplateToken, job: ReusableWorkflowJob) {
|
||||
const tokenForErrors = job.ref;
|
||||
switch (on.templateTokenType) {
|
||||
case TokenType.String: {
|
||||
const event = on.assertString("Reference workflow on value").value;
|
||||
if (event === "workflow_call") {
|
||||
handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobInputs(context, job));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case TokenType.Sequence: {
|
||||
const events = on.assertSequence("Reference workflow on value");
|
||||
for (const eventToken of events) {
|
||||
const event = eventToken.assertString(`Reference workflow on value ${eventToken}`).value;
|
||||
if (event === "workflow_call") {
|
||||
handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobInputs(context, job));
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case TokenType.Mapping: {
|
||||
const eventMapping = on.assertMapping("Reference workflow on value");
|
||||
|
||||
for (const pair of eventMapping) {
|
||||
const event = pair.key.assertString(`Reference workflow on value ${pair.key}`).value;
|
||||
if (event !== "workflow_call") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pair.value.templateTokenType === TokenType.Null) {
|
||||
handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobInputs(context, job));
|
||||
return;
|
||||
}
|
||||
|
||||
const definitions = pair.value.assertMapping(`Reference workflow on value ${pair.key}`);
|
||||
for (const definition of definitions) {
|
||||
const definitionKey = definition.key.assertString(`on-workflow_call-${definition.key}`).value;
|
||||
switch (definitionKey) {
|
||||
case "inputs":
|
||||
job["input-definitions"] = definition.value.assertMapping(`on-workflow_call-${definition.key}`);
|
||||
break;
|
||||
|
||||
case "outputs":
|
||||
job.outputs = definition.value.assertMapping(`on-workflow_call-${definition.key}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobInputs(context, job));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
context.error(tokenForErrors, "workflow_call key is not defined in the referenced workflow.");
|
||||
}
|
||||
@@ -52,7 +52,7 @@ function convertStep(context: TemplateContext, idBuilder: IdBuilder, step: Templ
|
||||
let uses: StringToken | undefined;
|
||||
let continueOnError: boolean | undefined;
|
||||
let env: MappingToken | undefined;
|
||||
const ifCondition = new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, "success()");
|
||||
const ifCondition = new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined);
|
||||
for (const item of mapping) {
|
||||
const key = item.key.assertString("steps item key");
|
||||
switch (key.value) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ActionStep, RunStep, Step} from "./workflow-template";
|
||||
import {ActionStep, Job, ReusableWorkflowJob, RunStep, Step, WorkflowJob} from "./workflow-template";
|
||||
|
||||
export function isRunStep(step: Step): step is RunStep {
|
||||
return (step as RunStep).run !== undefined;
|
||||
@@ -7,3 +7,11 @@ export function isRunStep(step: Step): step is RunStep {
|
||||
export function isActionStep(step: Step): step is ActionStep {
|
||||
return (step as ActionStep).uses !== undefined;
|
||||
}
|
||||
|
||||
export function isJob(job: WorkflowJob): job is Job {
|
||||
return job.type === "job";
|
||||
}
|
||||
|
||||
export function isReusableWorkflowJob(job: WorkflowJob): job is ReusableWorkflowJob {
|
||||
return job.type === "reusableWorkflowJob";
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
|
||||
export type WorkflowTemplate = {
|
||||
events: EventsConfig;
|
||||
jobs: Job[];
|
||||
jobs: WorkflowJob[];
|
||||
concurrency: TemplateToken;
|
||||
env: TemplateToken;
|
||||
|
||||
@@ -28,23 +28,41 @@ export type ActionsEnvironmentReference = {
|
||||
url?: TemplateToken;
|
||||
};
|
||||
|
||||
export type Job = {
|
||||
type: string;
|
||||
export type WorkflowJob = Job | ReusableWorkflowJob;
|
||||
|
||||
export type JobType = "job" | "reusableWorkflowJob";
|
||||
|
||||
export type BaseJob = {
|
||||
type: JobType;
|
||||
id: StringToken;
|
||||
name?: ScalarToken;
|
||||
needs?: StringToken[];
|
||||
if: BasicExpressionToken;
|
||||
env?: MappingToken;
|
||||
concurrency?: TemplateToken;
|
||||
environment?: TemplateToken;
|
||||
strategy?: TemplateToken;
|
||||
outputs?: MappingToken;
|
||||
};
|
||||
|
||||
// `job-factory` in the schema
|
||||
export type Job = BaseJob & {
|
||||
type: "job";
|
||||
env?: MappingToken;
|
||||
environment?: TemplateToken;
|
||||
"runs-on"?: TemplateToken;
|
||||
container?: TemplateToken;
|
||||
services?: TemplateToken;
|
||||
outputs?: MappingToken;
|
||||
steps: Step[];
|
||||
};
|
||||
|
||||
// `workflow-job` in the schema
|
||||
export type ReusableWorkflowJob = BaseJob & {
|
||||
type: "reusableWorkflowJob";
|
||||
ref: StringToken;
|
||||
"input-definitions"?: MappingToken;
|
||||
"input-values"?: MappingToken;
|
||||
jobs?: WorkflowJob[];
|
||||
};
|
||||
|
||||
export type Container = {
|
||||
image: StringToken;
|
||||
credentials?: Credential;
|
||||
|
||||
@@ -616,10 +616,10 @@ class TemplateReader {
|
||||
tr: TokenRange,
|
||||
rawExpression: string,
|
||||
allowedContext: string[],
|
||||
token: TemplateToken,
|
||||
token: StringToken,
|
||||
definitionInfo: DefinitionInfo | undefined
|
||||
): ExpressionToken | undefined {
|
||||
const parseExpressionResult = this.parseExpression(tr, rawExpression, allowedContext, definitionInfo);
|
||||
const parseExpressionResult = this.parseExpression(tr, token, rawExpression, allowedContext, definitionInfo);
|
||||
|
||||
// Check for error
|
||||
if (parseExpressionResult.error) {
|
||||
@@ -631,7 +631,8 @@ class TemplateReader {
|
||||
}
|
||||
|
||||
private parseExpression(
|
||||
range: TokenRange | undefined,
|
||||
range: TokenRange,
|
||||
token: StringToken,
|
||||
value: string,
|
||||
allowedContext: string[],
|
||||
definitionInfo: DefinitionInfo | undefined
|
||||
@@ -666,9 +667,31 @@ class TemplateReader {
|
||||
};
|
||||
}
|
||||
|
||||
const startTrim = value.length - value.trimStart().length;
|
||||
const endTrim = value.length - value.trimEnd().length;
|
||||
|
||||
const expressionRange: TokenRange = {
|
||||
start: {
|
||||
...range.start,
|
||||
column: range.start.column + OPEN_EXPRESSION.length + startTrim
|
||||
},
|
||||
end: {
|
||||
...range.end,
|
||||
column: range.end.column - CLOSE_EXPRESSION.length - endTrim
|
||||
}
|
||||
};
|
||||
|
||||
// Return the expression
|
||||
return <ParseExpressionResult>{
|
||||
expression: new BasicExpressionToken(this._fileId, range, trimmed, definitionInfo, undefined, value),
|
||||
expression: new BasicExpressionToken(
|
||||
this._fileId,
|
||||
range,
|
||||
trimmed,
|
||||
definitionInfo,
|
||||
undefined,
|
||||
token.source,
|
||||
expressionRange
|
||||
),
|
||||
error: undefined
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,10 +11,20 @@ import {TokenType} from "./types";
|
||||
export class BasicExpressionToken extends ExpressionToken {
|
||||
private readonly expr: string;
|
||||
|
||||
public readonly source: string | undefined;
|
||||
|
||||
public readonly originalExpressions: BasicExpressionToken[] | undefined;
|
||||
|
||||
public readonly source: string;
|
||||
|
||||
/**
|
||||
* The range of the expression within the source string.
|
||||
*
|
||||
* `range` is the range of the entire expression, including the `${{` and `}}`. `expression` is only the expression
|
||||
* without any ${{ }} markers. `expressionRange` is the range of just the expression within the document.
|
||||
*/
|
||||
public readonly expressionRange: TokenRange | undefined;
|
||||
|
||||
/**
|
||||
* @param originalExpressions If the basic expression was transformed from individual expressions, these will be the original ones
|
||||
*/
|
||||
@@ -24,12 +34,14 @@ export class BasicExpressionToken extends ExpressionToken {
|
||||
expression: string,
|
||||
definitionInfo: DefinitionInfo | undefined,
|
||||
originalExpressions: BasicExpressionToken[] | undefined,
|
||||
source: string
|
||||
source: string | undefined,
|
||||
expressionRange?: TokenRange | undefined
|
||||
) {
|
||||
super(TokenType.BasicExpression, file, range, undefined, definitionInfo);
|
||||
this.expr = expression;
|
||||
this.originalExpressions = originalExpressions;
|
||||
this.source = source;
|
||||
this.originalExpressions = originalExpressions;
|
||||
this.expressionRange = expressionRange;
|
||||
}
|
||||
|
||||
public get expression(): string {
|
||||
@@ -44,7 +56,8 @@ export class BasicExpressionToken extends ExpressionToken {
|
||||
this.expr,
|
||||
this.definitionInfo,
|
||||
this.originalExpressions,
|
||||
this.source
|
||||
this.source,
|
||||
this.expressionRange
|
||||
)
|
||||
: new BasicExpressionToken(
|
||||
this.file,
|
||||
@@ -52,7 +65,8 @@ export class BasicExpressionToken extends ExpressionToken {
|
||||
this.expr,
|
||||
this.definitionInfo,
|
||||
this.originalExpressions,
|
||||
this.source
|
||||
this.source,
|
||||
this.expressionRange
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,10 @@ import {TemplateToken} from "./template-token";
|
||||
describe("traverse", () => {
|
||||
it("returns parent token and key", () => {
|
||||
const workflow = parseWorkflow(
|
||||
"wf.yaml",
|
||||
[
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: `on: push`
|
||||
}
|
||||
],
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: `on: push`
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
export type Position = {
|
||||
/** The one-based line value */
|
||||
line: number;
|
||||
|
||||
/** The one-based column value */
|
||||
column: number;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import {File} from "./file";
|
||||
import {FileReference} from "./file-reference";
|
||||
|
||||
export interface FileProvider {
|
||||
getFileContent(ref: FileReference): Promise<File>;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import {parseFileReference} from "./file-reference";
|
||||
|
||||
describe("parseFileReference", () => {
|
||||
it("parses local file reference", () => {
|
||||
const ref = parseFileReference("./workflow/path");
|
||||
expect(ref).toEqual({
|
||||
path: "workflow/path"
|
||||
});
|
||||
});
|
||||
|
||||
it("parses local file references with an empty path", () => {
|
||||
const ref = parseFileReference("./");
|
||||
expect(ref).toEqual({
|
||||
path: ""
|
||||
});
|
||||
});
|
||||
|
||||
it("parses remote file reference", () => {
|
||||
const ref = parseFileReference("owner/repo/path@version");
|
||||
expect(ref).toEqual({
|
||||
owner: "owner",
|
||||
repository: "repo",
|
||||
path: "path",
|
||||
version: "version"
|
||||
});
|
||||
});
|
||||
|
||||
it("parses remote file reference with an empty path", () => {
|
||||
const ref = parseFileReference("owner/repo@version");
|
||||
expect(ref).toEqual({
|
||||
owner: "owner",
|
||||
repository: "repo",
|
||||
path: "",
|
||||
version: "version"
|
||||
});
|
||||
});
|
||||
|
||||
it("parses remote file reference with slashes in the version", () => {
|
||||
const ref = parseFileReference("owner/repo@feature-branch/dev");
|
||||
expect(ref).toEqual({
|
||||
owner: "owner",
|
||||
repository: "repo",
|
||||
path: "",
|
||||
version: "feature-branch/dev"
|
||||
});
|
||||
});
|
||||
|
||||
it("throws for malformed remote file references", () => {
|
||||
expect(() => parseFileReference("owner/repo/path")).toThrowError("Invalid file reference: owner/repo/path");
|
||||
|
||||
expect(() => parseFileReference("owner/repo/path@")).toThrowError("Invalid file reference: owner/repo/path@");
|
||||
|
||||
expect(() => parseFileReference("owner@")).toThrowError("Invalid file reference: owner@");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
export type FileReference = LocalFileReference | RemoteFileReference;
|
||||
|
||||
export type LocalFileReference = {
|
||||
path: string;
|
||||
};
|
||||
|
||||
export type RemoteFileReference = {
|
||||
repository: string;
|
||||
owner: string;
|
||||
path: string;
|
||||
version: string;
|
||||
};
|
||||
|
||||
export function parseFileReference(ref: string): FileReference {
|
||||
if (ref.startsWith("./")) {
|
||||
return {
|
||||
path: ref.substring(2)
|
||||
};
|
||||
}
|
||||
|
||||
const [remotePath, version] = ref.split("@");
|
||||
const [owner, repository, ...pathSegments] = remotePath.split("/").filter(s => s.length > 0);
|
||||
|
||||
if (!owner || !repository || !version) {
|
||||
throw new Error(`Invalid file reference: ${ref}`);
|
||||
}
|
||||
|
||||
return {
|
||||
repository,
|
||||
owner,
|
||||
path: pathSegments.join("/"),
|
||||
version
|
||||
};
|
||||
}
|
||||
|
||||
export function fileIdentifier(ref: FileReference): string {
|
||||
if (!("repository" in ref)) {
|
||||
return "./" + ref.path;
|
||||
}
|
||||
|
||||
return `${ref.owner}/${ref.repository}/${ref.path}@${ref.version}`;
|
||||
}
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
- name: 'Hello \${{ fromJSON('test') == inputs.name }}'
|
||||
run: echo Hello, world!`;
|
||||
|
||||
const result = parseWorkflow("main.yaml", [{name: "main.yaml", content: content}], nullTrace);
|
||||
const result = parseWorkflow({name: "main.yaml", content: content}, nullTrace);
|
||||
|
||||
expect(result.context.errors.count).toBe(1);
|
||||
expect(result.value).toBeUndefined();
|
||||
|
||||
@@ -11,15 +11,16 @@ export interface ParseWorkflowResult {
|
||||
value: TemplateToken | undefined;
|
||||
}
|
||||
|
||||
export function parseWorkflow(entryFileName: string, files: File[], trace: TraceWriter): ParseWorkflowResult {
|
||||
const context = new TemplateContext(new TemplateValidationErrors(), getWorkflowSchema(), trace);
|
||||
export function parseWorkflow(entryFile: File, trace: TraceWriter): ParseWorkflowResult;
|
||||
export function parseWorkflow(entryFile: File, context: TemplateContext): ParseWorkflowResult;
|
||||
export function parseWorkflow(entryFile: File, contextOrTrace: TraceWriter | TemplateContext): ParseWorkflowResult {
|
||||
const context =
|
||||
contextOrTrace instanceof TemplateContext
|
||||
? contextOrTrace
|
||||
: new TemplateContext(new TemplateValidationErrors(), getWorkflowSchema(), contextOrTrace);
|
||||
|
||||
// Add file ids
|
||||
files.forEach(x => context.getFileId(x.name));
|
||||
|
||||
const fileId = context.getFileId(entryFileName);
|
||||
const fileContent = files[fileId - 1].content;
|
||||
const reader = new YamlObjectReader(context, fileId, fileContent);
|
||||
const fileId = context.getFileId(entryFile.name);
|
||||
const reader = new YamlObjectReader(context, fileId, entryFile.content);
|
||||
if (context.errors.count > 0) {
|
||||
// The file is not valid YAML, template errors could be misleading
|
||||
return {
|
||||
|
||||
@@ -81,13 +81,10 @@ it("YAML errors include range information", () => {
|
||||
|
||||
function parseAsWorkflow(content: string): TemplateToken | undefined {
|
||||
const result = parseWorkflow(
|
||||
"test.yaml",
|
||||
[
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: content
|
||||
}
|
||||
],
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: content
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ import * as path from "path";
|
||||
import * as YAML from "yaml";
|
||||
import {convertWorkflowTemplate} from "./model/convert";
|
||||
import {TraceWriter} from "./templates/trace-writer";
|
||||
import {File} from "./workflows/file";
|
||||
import {FileProvider} from "./workflows/file-provider";
|
||||
import {fileIdentifier, FileReference} from "./workflows/file-reference";
|
||||
import {parseWorkflow} from "./workflows/workflow-parser";
|
||||
|
||||
interface TestOptions {
|
||||
@@ -41,31 +44,53 @@ describe("x-lang tests", () => {
|
||||
const testOptions: TestOptions = YAML.parse(testDocs[0]);
|
||||
const unsupportedTest = contains(testOptions.skip, "TypeScript");
|
||||
|
||||
const test = () => {
|
||||
let testFileName = ".github/workflows" + fileName.substring(fileName.lastIndexOf("/"));
|
||||
let testInput = testDocs[1];
|
||||
let expectedTemplate = testDocs[2].trim();
|
||||
// TODO: when reusable workflows are implemented, implement correctly
|
||||
const test = async () => {
|
||||
const testFileName = ".github/workflows" + fileName.substring(fileName.lastIndexOf("/"));
|
||||
const testInput = testDocs[1];
|
||||
const expectedTemplate = testDocs[testDocs.length - 1].trim();
|
||||
|
||||
// For reusable workflow tests, additional workflows are passed in as pairs of
|
||||
// file names and file contents
|
||||
const reusableWorkflows: Record<string, File> = {};
|
||||
if (fileName.indexOf("reusable") !== -1) {
|
||||
testFileName = testDocs[1];
|
||||
testInput = testDocs[2];
|
||||
expectedTemplate = testDocs[3].trim();
|
||||
for (let i = 2; i < testDocs.length - 1; i = i + 2) {
|
||||
reusableWorkflows[testDocs[i]] = {
|
||||
name: testDocs[i],
|
||||
content: testDocs[i + 1]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const parseResult = parseWorkflow(
|
||||
testFileName,
|
||||
[
|
||||
{
|
||||
name: testFileName,
|
||||
content: testInput
|
||||
const testFileProvider: FileProvider = {
|
||||
getFileContent: async (ref: FileReference) => {
|
||||
const file = reusableWorkflows[fileIdentifier(ref)];
|
||||
if (file) {
|
||||
return file;
|
||||
}
|
||||
],
|
||||
|
||||
throw new Error("File not found: " + fileName);
|
||||
}
|
||||
};
|
||||
|
||||
const parseResult = parseWorkflow(
|
||||
{
|
||||
name: testFileName,
|
||||
content: testInput
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
expect(parseResult.value).not.toBeUndefined();
|
||||
|
||||
const workflowTemplate = convertWorkflowTemplate(parseResult.context, parseResult.value!);
|
||||
const workflowTemplate = await convertWorkflowTemplate(
|
||||
parseResult.context,
|
||||
parseResult.value!,
|
||||
undefined,
|
||||
testFileProvider,
|
||||
{
|
||||
fetchReusableWorkflowDepth: 1
|
||||
}
|
||||
);
|
||||
|
||||
// Unless this tests is only used by TypeScript, remove the events for now.
|
||||
// TODO: Remove this once we parse events everywhere
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@ skip:
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
concurrency:
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- run: echo hi
|
||||
continue-on-error: true
|
||||
concurrency:
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
build2:
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
concurrency: staging
|
||||
build4:
|
||||
runs-on: macos-latest
|
||||
concurrency:
|
||||
concurrency:
|
||||
group: ref
|
||||
cancel-in-progress: ${{ github.ref }}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
bad_insert1: "This is a bad ${{ insert }}"
|
||||
bad_insert2: "${{ insert }} are bad at the beginning"
|
||||
${{ insert }}: ${{ github.ref }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
---
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"Message": ".github/workflows/errors-insert.yml (Line: 6, Col: 20): The directive 'insert' is not allowed in this context. Directives are not supported for expressions that are embedded within a string. Directives are only supported when the entire value is an expression."
|
||||
},
|
||||
{
|
||||
"Message": ".github/workflows/errors-insert.yml (Line: 7, Col: 20): The directive 'insert' is not allowed in this context. Directives are not supported for expressions that are embedded within a string. Directives are only supported when the entire value is an expression."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- null : ' '
|
||||
run: echo {{ 😀 }}
|
||||
---
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"Message": ".github/workflows/errors-invalid-mapping-key.yml (Line: 6, Col: 9): Unexpected value ''"
|
||||
}
|
||||
]
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@ jobs:
|
||||
steps:
|
||||
- run: echo hi
|
||||
build3:
|
||||
runs-on:
|
||||
runs-on:
|
||||
group: ent/
|
||||
steps:
|
||||
- run: echo hi
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
bad-strategy-key:
|
||||
strategy:
|
||||
bad-key:
|
||||
os: [10]
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- run: echo hi
|
||||
---
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"Message": ".github/workflows/errors-matrix-bad-key.yml (Line: 5, Col: 7): Unexpected value 'bad-key'"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
empty-vector:
|
||||
strategy:
|
||||
matrix:
|
||||
os: []
|
||||
version: [10,12]
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- run: echo hi
|
||||
---
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"Message": ".github/workflows/errors-matrix-empty-vector.yml (Line: 6, Col: 13): Matrix vector 'os' does not contain any values"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
max-depth: 5
|
||||
skip:
|
||||
- TypeScript
|
||||
max-depth: 5
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
max-file-size: 124
|
||||
skip:
|
||||
- TypeScript
|
||||
max-file-size: 124
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
max-result-size: 768
|
||||
skip:
|
||||
- TypeScript
|
||||
max-result-size: 768
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
|
||||
-1
@@ -1,7 +1,6 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- Go
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
|
||||
-1
@@ -1,7 +1,6 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- Go
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
|
||||
-1
@@ -1,7 +1,6 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- Go
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
max-result-size: 2048
|
||||
skip:
|
||||
- TypeScript
|
||||
- Go
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
steps:
|
||||
- run: echo Hello && World #string token
|
||||
build2:
|
||||
if: false
|
||||
if: false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 1
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Switch to using Python 3.10 by default
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: >-
|
||||
3.10
|
||||
---
|
||||
{
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": "build",
|
||||
"name": "build",
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"runs-on": "ubuntu-latest",
|
||||
"steps": [
|
||||
{
|
||||
"id": "__actions_setup-python",
|
||||
"name": "Switch to using Python 3.10 by default",
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"uses": "actions/setup-python@v4",
|
||||
"with": {
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
"Key": "python-version",
|
||||
"Value": "3.10"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
generate:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix_map: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
run:
|
||||
runs-on: ubuntu-latest
|
||||
needs: generate
|
||||
env:
|
||||
${{ insert }}: ${{ fromJson(needs.generate.outputs.matrix_map) }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
---
|
||||
{
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": "generate",
|
||||
"name": "generate",
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"runs-on": "ubuntu-latest",
|
||||
"outputs": {
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
"Key": "matrix_map",
|
||||
"Value": {
|
||||
"type": 3,
|
||||
"expr": "steps.set-matrix.outputs.matrix"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"id": "__run",
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"run": "echo hi"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "job",
|
||||
"id": "run",
|
||||
"name": "run",
|
||||
"needs": [
|
||||
"generate"
|
||||
],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"env": {
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
"Key": {
|
||||
"type": 4,
|
||||
"directive": "insert"
|
||||
},
|
||||
"Value": {
|
||||
"type": 3,
|
||||
"expr": "fromJson(needs.generate.outputs.matrix_map)"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"runs-on": "ubuntu-latest",
|
||||
"steps": [
|
||||
{
|
||||
"id": "__run",
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"run": "echo hi"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+1
-1
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hi
|
||||
build4:
|
||||
build4:
|
||||
needs:
|
||||
- build
|
||||
- build2
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
matrix-basic:
|
||||
strategy:
|
||||
matrix:
|
||||
version: [10, 12, 14]
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
matrix-nested-sequences:
|
||||
strategy:
|
||||
matrix:
|
||||
version: [[[[10]],2],12,14]
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
matrix-with-infinity:
|
||||
strategy:
|
||||
matrix:
|
||||
version: [1, 999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999]
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
nested-matrix:
|
||||
strategy:
|
||||
matrix: { vector1: [ {foo: {bar: baz} } ] }
|
||||
runs-on: linux
|
||||
steps:
|
||||
- run: echo hi
|
||||
---
|
||||
{
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": "matrix-basic",
|
||||
"name": "matrix-basic",
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"strategy": {
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
"Key": "matrix",
|
||||
"Value": {
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
"Key": "version",
|
||||
"Value": {
|
||||
"type": 1,
|
||||
"seq": [
|
||||
10,
|
||||
12,
|
||||
14
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Key": "os",
|
||||
"Value": {
|
||||
"type": 1,
|
||||
"seq": [
|
||||
"ubuntu-latest",
|
||||
"windows-latest"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"runs-on": {
|
||||
"type": 3,
|
||||
"expr": "matrix.os"
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"id": "__run",
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"run": "echo hi"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "job",
|
||||
"id": "matrix-nested-sequences",
|
||||
"name": "matrix-nested-sequences",
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"strategy": {
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
"Key": "matrix",
|
||||
"Value": {
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
"Key": "version",
|
||||
"Value": {
|
||||
"type": 1,
|
||||
"seq": [
|
||||
{
|
||||
"type": 1,
|
||||
"seq": [
|
||||
{
|
||||
"type": 1,
|
||||
"seq": [
|
||||
{
|
||||
"type": 1,
|
||||
"seq": [
|
||||
10
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
2
|
||||
]
|
||||
},
|
||||
12,
|
||||
14
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Key": "os",
|
||||
"Value": {
|
||||
"type": 1,
|
||||
"seq": [
|
||||
"ubuntu-latest",
|
||||
"windows-latest"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"runs-on": {
|
||||
"type": 3,
|
||||
"expr": "matrix.os"
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"id": "__run",
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"run": "echo hi"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "job",
|
||||
"id": "matrix-with-infinity",
|
||||
"name": "matrix-with-infinity",
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"strategy": {
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
"Key": "matrix",
|
||||
"Value": {
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
"Key": "version",
|
||||
"Value": {
|
||||
"type": 1,
|
||||
"seq": [
|
||||
1,
|
||||
"Infinity"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Key": "os",
|
||||
"Value": {
|
||||
"type": 1,
|
||||
"seq": [
|
||||
"ubuntu-latest",
|
||||
"windows-latest"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"runs-on": {
|
||||
"type": 3,
|
||||
"expr": "matrix.os"
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"id": "__run",
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"run": "echo hi"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "job",
|
||||
"id": "nested-matrix",
|
||||
"name": "nested-matrix",
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"strategy": {
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
"Key": "matrix",
|
||||
"Value": {
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
"Key": "vector1",
|
||||
"Value": {
|
||||
"type": 1,
|
||||
"seq": [
|
||||
{
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
"Key": "foo",
|
||||
"Value": {
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
"Key": "bar",
|
||||
"Value": "baz"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"runs-on": "linux",
|
||||
"steps": [
|
||||
{
|
||||
"id": "__run",
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"run": "echo hi"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+3
-3
@@ -10,7 +10,7 @@ jobs:
|
||||
name: ${{ github.actor }}
|
||||
timeout-minutes: ${{ github.ref }}
|
||||
cancel-timeout-minutes: 300
|
||||
concurrency:
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
env:
|
||||
@@ -28,10 +28,10 @@ jobs:
|
||||
build2:
|
||||
runs-on: [self-hosted, linux]
|
||||
continue-on-error: true
|
||||
name: Jobs Repro Hardcode
|
||||
name: Jobs Repro Hardcode
|
||||
timeout-minutes: 360
|
||||
cancel-timeout-minutes: 300
|
||||
concurrency:
|
||||
concurrency:
|
||||
group: groupA
|
||||
cancel-in-progress: true
|
||||
env:
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- Typescript
|
||||
- TypeScript
|
||||
---
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- Go
|
||||
- Typescript
|
||||
- TypeScript
|
||||
---
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -22,6 +21,10 @@ on:
|
||||
- debug
|
||||
# Defaults to string
|
||||
input4:
|
||||
input5-Env:
|
||||
description: 'Test environment'
|
||||
type: environment
|
||||
required: true
|
||||
jobs:
|
||||
my-job:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -33,7 +36,8 @@ jobs:
|
||||
"input1": "string",
|
||||
"input2": "boolean",
|
||||
"input3": "choice",
|
||||
"input4": "string"
|
||||
"input4": "string",
|
||||
"input5-Env": "environment"
|
||||
},
|
||||
"jobs": [
|
||||
{
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- Typescript
|
||||
- TypeScript
|
||||
---
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- run: echo hi
|
||||
continue-on-error: true
|
||||
deploy:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
include-source: true # Preserve file/line/col in serialized output
|
||||
skip:
|
||||
- Go
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
|
||||
+14
-38
@@ -33,46 +33,34 @@ jobs:
|
||||
{
|
||||
"jobs": [
|
||||
{
|
||||
"Type": "reusableWorkflowJob",
|
||||
"Id": {
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": {
|
||||
"type": 0,
|
||||
"file": 1,
|
||||
"line": 3,
|
||||
"col": 3,
|
||||
"lit": "build"
|
||||
},
|
||||
"Name": {
|
||||
"name": {
|
||||
"type": 0,
|
||||
"file": 1,
|
||||
"line": 3,
|
||||
"col": 3,
|
||||
"lit": "build"
|
||||
},
|
||||
"Needs": [],
|
||||
"If": {
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"Ref": {
|
||||
"ref": {
|
||||
"type": 0,
|
||||
"file": 1,
|
||||
"line": 4,
|
||||
"col": 11,
|
||||
"lit": "some-org-1/some-repo-1/.github/workflows/build.yml@v1"
|
||||
},
|
||||
"Permissions": null,
|
||||
"InputDefinitions": null,
|
||||
"InputValues": null,
|
||||
"SecretDefinitions": null,
|
||||
"SecretValues": null,
|
||||
"InheritSecrets": false,
|
||||
"Outputs": null,
|
||||
"Defaults": null,
|
||||
"Env": null,
|
||||
"Concurrency": null,
|
||||
"EmbeddedConcurrency": null,
|
||||
"Strategy": null,
|
||||
"Jobs": [
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": {
|
||||
@@ -120,46 +108,34 @@ jobs:
|
||||
]
|
||||
},
|
||||
{
|
||||
"Type": "reusableWorkflowJob",
|
||||
"Id": {
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": {
|
||||
"type": 0,
|
||||
"file": 1,
|
||||
"line": 5,
|
||||
"col": 3,
|
||||
"lit": "deploy"
|
||||
},
|
||||
"Name": {
|
||||
"name": {
|
||||
"type": 0,
|
||||
"file": 1,
|
||||
"line": 5,
|
||||
"col": 3,
|
||||
"lit": "deploy"
|
||||
},
|
||||
"Needs": [],
|
||||
"If": {
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"Ref": {
|
||||
"ref": {
|
||||
"type": 0,
|
||||
"file": 1,
|
||||
"line": 6,
|
||||
"col": 11,
|
||||
"lit": "some-org-2/some-repo-2/.github/workflows/deploy.yml@v2"
|
||||
},
|
||||
"Permissions": null,
|
||||
"InputDefinitions": null,
|
||||
"InputValues": null,
|
||||
"SecretDefinitions": null,
|
||||
"SecretValues": null,
|
||||
"InheritSecrets": false,
|
||||
"Outputs": null,
|
||||
"Defaults": null,
|
||||
"Env": null,
|
||||
"Concurrency": null,
|
||||
"EmbeddedConcurrency": null,
|
||||
"Strategy": null,
|
||||
"Jobs": [
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": {
|
||||
|
||||
@@ -3,7 +3,7 @@ skip:
|
||||
- TypeScript
|
||||
---
|
||||
# This is meant to cover all the different types of TemplateToken that are output
|
||||
# with include-source: true. Currently it is missing type 4 (insert expression)
|
||||
# with include-source: true. Currently it is missing type 4 (insert expression)
|
||||
# and type 7 (null), once we have matrix strategy we should be able to get null or combine
|
||||
# this test with preserves-source-info-basic.yml.
|
||||
on: push
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- Go
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
@@ -53,17 +52,16 @@ jobs:
|
||||
{
|
||||
"jobs": [
|
||||
{
|
||||
"Type": "reusableWorkflowJob",
|
||||
"Id": "build",
|
||||
"Name": "build",
|
||||
"Needs": [],
|
||||
"If": {
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "build",
|
||||
"name": "build",
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"Ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"Permissions": null,
|
||||
"InputDefinitions": {
|
||||
"ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"input-definitions": {
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
@@ -200,7 +198,7 @@ jobs:
|
||||
}
|
||||
]
|
||||
},
|
||||
"InputValues": {
|
||||
"input-values": {
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
@@ -229,16 +227,7 @@ jobs:
|
||||
}
|
||||
]
|
||||
},
|
||||
"SecretDefinitions": null,
|
||||
"SecretValues": null,
|
||||
"InheritSecrets": false,
|
||||
"Outputs": null,
|
||||
"Defaults": null,
|
||||
"Env": null,
|
||||
"Concurrency": null,
|
||||
"EmbeddedConcurrency": null,
|
||||
"Strategy": null,
|
||||
"Jobs": [
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": "deploy",
|
||||
|
||||
@@ -66,19 +66,18 @@ jobs:
|
||||
]
|
||||
},
|
||||
{
|
||||
"Type": "reusableWorkflowJob",
|
||||
"Id": "deploy-1",
|
||||
"Name": "deploy-1",
|
||||
"Needs": [
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "deploy-1",
|
||||
"name": "deploy-1",
|
||||
"needs": [
|
||||
"build"
|
||||
],
|
||||
"If": {
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"Ref": "contoso/templates/.github/workflows/deploy.yml@v1",
|
||||
"Permissions": null,
|
||||
"InputDefinitions": {
|
||||
"ref": "contoso/templates/.github/workflows/deploy.yml@v1",
|
||||
"input-definitions": {
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
@@ -99,7 +98,7 @@ jobs:
|
||||
}
|
||||
]
|
||||
},
|
||||
"InputValues": {
|
||||
"input-values": {
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
@@ -108,7 +107,7 @@ jobs:
|
||||
}
|
||||
]
|
||||
},
|
||||
"SecretDefinitions": {
|
||||
"secret-definitions": {
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
@@ -129,7 +128,7 @@ jobs:
|
||||
}
|
||||
]
|
||||
},
|
||||
"SecretValues": {
|
||||
"secret-values": {
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
@@ -141,14 +140,7 @@ jobs:
|
||||
}
|
||||
]
|
||||
},
|
||||
"InheritSecrets": false,
|
||||
"Outputs": null,
|
||||
"Defaults": null,
|
||||
"Env": null,
|
||||
"Concurrency": null,
|
||||
"EmbeddedConcurrency": null,
|
||||
"Strategy": null,
|
||||
"Jobs": [
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": "job1",
|
||||
@@ -184,19 +176,18 @@ jobs:
|
||||
]
|
||||
},
|
||||
{
|
||||
"Type": "reusableWorkflowJob",
|
||||
"Id": "deploy-2",
|
||||
"Name": "deploy-2",
|
||||
"Needs": [
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "deploy-2",
|
||||
"name": "deploy-2",
|
||||
"needs": [
|
||||
"build"
|
||||
],
|
||||
"If": {
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"Ref": "contoso/templates/.github/workflows/deploy.yml@v1",
|
||||
"Permissions": null,
|
||||
"InputDefinitions": {
|
||||
"ref": "contoso/templates/.github/workflows/deploy.yml@v1",
|
||||
"input-definitions": {
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
@@ -217,7 +208,7 @@ jobs:
|
||||
}
|
||||
]
|
||||
},
|
||||
"InputValues": {
|
||||
"input-values": {
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
@@ -226,7 +217,7 @@ jobs:
|
||||
}
|
||||
]
|
||||
},
|
||||
"SecretDefinitions": {
|
||||
"secret-definitions": {
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
@@ -247,15 +238,8 @@ jobs:
|
||||
}
|
||||
]
|
||||
},
|
||||
"SecretValues": null,
|
||||
"InheritSecrets": true,
|
||||
"Outputs": null,
|
||||
"Defaults": null,
|
||||
"Env": null,
|
||||
"Concurrency": null,
|
||||
"EmbeddedConcurrency": null,
|
||||
"Strategy": null,
|
||||
"Jobs": [
|
||||
"inherit-secrets": true,
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": "job1",
|
||||
|
||||
+9
-20
@@ -1,7 +1,6 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- Go
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
@@ -35,17 +34,16 @@ jobs:
|
||||
{
|
||||
"jobs": [
|
||||
{
|
||||
"Type": "reusableWorkflowJob",
|
||||
"Id": "deploy",
|
||||
"Name": "deploy",
|
||||
"Needs": [],
|
||||
"If": {
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "deploy",
|
||||
"name": "deploy",
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"Ref": "contoso/templates/.github/workflows/deploy.yml@v1",
|
||||
"Permissions": null,
|
||||
"InputDefinitions": {
|
||||
"ref": "contoso/templates/.github/workflows/deploy.yml@v1",
|
||||
"input-definitions": {
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
@@ -98,7 +96,7 @@ jobs:
|
||||
}
|
||||
]
|
||||
},
|
||||
"InputValues": {
|
||||
"input-values": {
|
||||
"type": 2,
|
||||
"map": [
|
||||
{
|
||||
@@ -119,16 +117,7 @@ jobs:
|
||||
}
|
||||
]
|
||||
},
|
||||
"SecretDefinitions": null,
|
||||
"SecretValues": null,
|
||||
"InheritSecrets": false,
|
||||
"Outputs": null,
|
||||
"Defaults": null,
|
||||
"Env": null,
|
||||
"Concurrency": null,
|
||||
"EmbeddedConcurrency": null,
|
||||
"Strategy": null,
|
||||
"Jobs": [
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": "job1",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- Go
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
@@ -26,28 +25,16 @@ jobs:
|
||||
{
|
||||
"jobs": [
|
||||
{
|
||||
"Type": "reusableWorkflowJob",
|
||||
"Id": "build",
|
||||
"Name": "build",
|
||||
"Needs": [],
|
||||
"If": {
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "build",
|
||||
"name": "build",
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"Ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"Permissions": null,
|
||||
"InputDefinitions": null,
|
||||
"InputValues": null,
|
||||
"SecretDefinitions": null,
|
||||
"SecretValues": null,
|
||||
"InheritSecrets": false,
|
||||
"Outputs": null,
|
||||
"Defaults": null,
|
||||
"Env": null,
|
||||
"Concurrency": null,
|
||||
"EmbeddedConcurrency": null,
|
||||
"Strategy": null,
|
||||
"Jobs": [
|
||||
"ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": "build-it",
|
||||
@@ -71,28 +58,16 @@ jobs:
|
||||
]
|
||||
},
|
||||
{
|
||||
"Type": "reusableWorkflowJob",
|
||||
"Id": "build1",
|
||||
"Name": "custom name",
|
||||
"Needs": [],
|
||||
"If": {
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "build1",
|
||||
"name": "custom name",
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"Ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"Permissions": null,
|
||||
"InputDefinitions": null,
|
||||
"InputValues": null,
|
||||
"SecretDefinitions": null,
|
||||
"SecretValues": null,
|
||||
"InheritSecrets": false,
|
||||
"Outputs": null,
|
||||
"Defaults": null,
|
||||
"Env": null,
|
||||
"Concurrency": null,
|
||||
"EmbeddedConcurrency": null,
|
||||
"Strategy": null,
|
||||
"Jobs": [
|
||||
"ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": "build-it",
|
||||
@@ -116,31 +91,19 @@ jobs:
|
||||
]
|
||||
},
|
||||
{
|
||||
"Type": "reusableWorkflowJob",
|
||||
"Id": "build2",
|
||||
"Name": {
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "build2",
|
||||
"name": {
|
||||
"type": 3,
|
||||
"expr": "github.ref"
|
||||
},
|
||||
"Needs": [],
|
||||
"If": {
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"Ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"Permissions": null,
|
||||
"InputDefinitions": null,
|
||||
"InputValues": null,
|
||||
"SecretDefinitions": null,
|
||||
"SecretValues": null,
|
||||
"InheritSecrets": false,
|
||||
"Outputs": null,
|
||||
"Defaults": null,
|
||||
"Env": null,
|
||||
"Concurrency": null,
|
||||
"EmbeddedConcurrency": null,
|
||||
"Strategy": null,
|
||||
"Jobs": [
|
||||
"ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": "build-it",
|
||||
|
||||
+14
-38
@@ -28,51 +28,27 @@ jobs:
|
||||
{
|
||||
"jobs": [
|
||||
{
|
||||
"Type": "reusableWorkflowJob",
|
||||
"Id": "deploy-level-0",
|
||||
"Name": "deploy-level-0",
|
||||
"Needs": [],
|
||||
"If": {
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "deploy-level-0",
|
||||
"name": "deploy-level-0",
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"Ref": "contoso/templates/.github/workflows/deploy-level-1.yml@v1",
|
||||
"Permissions": null,
|
||||
"InputDefinitions": null,
|
||||
"InputValues": null,
|
||||
"SecretDefinitions": null,
|
||||
"SecretValues": null,
|
||||
"InheritSecrets": false,
|
||||
"Outputs": null,
|
||||
"Defaults": null,
|
||||
"Env": null,
|
||||
"Concurrency": null,
|
||||
"EmbeddedConcurrency": null,
|
||||
"Strategy": null,
|
||||
"Jobs": [
|
||||
"ref": "contoso/templates/.github/workflows/deploy-level-1.yml@v1",
|
||||
"jobs": [
|
||||
{
|
||||
"Type": "reusableWorkflowJob",
|
||||
"Id": "deploy-level-1",
|
||||
"Name": "deploy-level-1",
|
||||
"Needs": [],
|
||||
"If": {
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "deploy-level-1",
|
||||
"name": "deploy-level-1",
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"Ref": "contoso/templates/.github/workflows/deploy-level-2.yml@v1",
|
||||
"Permissions": null,
|
||||
"InputDefinitions": null,
|
||||
"InputValues": null,
|
||||
"SecretDefinitions": null,
|
||||
"SecretValues": null,
|
||||
"InheritSecrets": false,
|
||||
"Outputs": null,
|
||||
"Defaults": null,
|
||||
"Env": null,
|
||||
"Concurrency": null,
|
||||
"EmbeddedConcurrency": null,
|
||||
"Strategy": null,
|
||||
"Jobs": [
|
||||
"ref": "contoso/templates/.github/workflows/deploy-level-2.yml@v1",
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": "deploy-level-1",
|
||||
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- Go
|
||||
- TypeScript
|
||||
max-nested-reusable-workflows-depth: 2
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
a:
|
||||
uses: contoso/templates/.github/workflows/deploy-level-1a.yml@v1
|
||||
b:
|
||||
uses: contoso/templates/.github/workflows/deploy-level-1b.yml@v1
|
||||
c:
|
||||
uses: contoso/templates/.github/workflows/deploy-level-1c.yml@v1
|
||||
---
|
||||
contoso/templates/.github/workflows/deploy-level-1a.yml@v1
|
||||
---
|
||||
on: workflow_call
|
||||
jobs:
|
||||
deploy-level-1a:
|
||||
uses: contoso/templates/.github/workflows/deploy-level-2a.yml@v1
|
||||
---
|
||||
contoso/templates/.github/workflows/deploy-level-2a.yml@v1
|
||||
---
|
||||
on: workflow_call
|
||||
jobs:
|
||||
deploy-level-2a:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hi
|
||||
---
|
||||
contoso/templates/.github/workflows/deploy-level-1b.yml@v1
|
||||
---
|
||||
on: workflow_call
|
||||
jobs:
|
||||
deploy-level-1b:
|
||||
uses: contoso/templates/.github/workflows/deploy-level-2b.yml@v1
|
||||
---
|
||||
contoso/templates/.github/workflows/deploy-level-2b.yml@v1
|
||||
---
|
||||
on: workflow_call
|
||||
jobs:
|
||||
deploy-level-2b:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hi
|
||||
---
|
||||
contoso/templates/.github/workflows/deploy-level-1c.yml@v1
|
||||
---
|
||||
on: workflow_call
|
||||
jobs:
|
||||
deploy-level-1c:
|
||||
uses: contoso/templates/.github/workflows/deploy-level-2c.yml@v1
|
||||
---
|
||||
contoso/templates/.github/workflows/deploy-level-2c.yml@v1
|
||||
---
|
||||
on: workflow_call
|
||||
jobs:
|
||||
deploy-level-2c:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hi
|
||||
---
|
||||
{
|
||||
"jobs": [
|
||||
{
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "a",
|
||||
"name": "a",
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"ref": "contoso/templates/.github/workflows/deploy-level-1a.yml@v1",
|
||||
"jobs": [
|
||||
{
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "deploy-level-1a",
|
||||
"name": "deploy-level-1a",
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"ref": "contoso/templates/.github/workflows/deploy-level-2a.yml@v1",
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": "deploy-level-2a",
|
||||
"name": "deploy-level-2a",
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"runs-on": "ubuntu-latest",
|
||||
"steps": [
|
||||
{
|
||||
"id": "__run",
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"run": "echo hi"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "b",
|
||||
"name": "b",
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"ref": "contoso/templates/.github/workflows/deploy-level-1b.yml@v1",
|
||||
"jobs": [
|
||||
{
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "deploy-level-1b",
|
||||
"name": "deploy-level-1b",
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"ref": "contoso/templates/.github/workflows/deploy-level-2b.yml@v1",
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": "deploy-level-2b",
|
||||
"name": "deploy-level-2b",
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"runs-on": "ubuntu-latest",
|
||||
"steps": [
|
||||
{
|
||||
"id": "__run",
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"run": "echo hi"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "c",
|
||||
"name": "c",
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"ref": "contoso/templates/.github/workflows/deploy-level-1c.yml@v1",
|
||||
"jobs": [
|
||||
{
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "deploy-level-1c",
|
||||
"name": "deploy-level-1c",
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"ref": "contoso/templates/.github/workflows/deploy-level-2c.yml@v1",
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": "deploy-level-2c",
|
||||
"name": "deploy-level-2c",
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"runs-on": "ubuntu-latest",
|
||||
"steps": [
|
||||
{
|
||||
"id": "__run",
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"run": "echo hi"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+24
-57
@@ -42,80 +42,47 @@ jobs:
|
||||
},
|
||||
"jobs": [
|
||||
{
|
||||
"Type": "reusableWorkflowJob",
|
||||
"Id": "deploy-level-0",
|
||||
"Name": "deploy-level-0",
|
||||
"Needs": [],
|
||||
"If": {
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "deploy-level-0",
|
||||
"name": "deploy-level-0",
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"Ref": "contoso/templates/.github/workflows/deploy-level-1.yml@v1",
|
||||
"Permissions": {
|
||||
"ref": "contoso/templates/.github/workflows/deploy-level-1.yml@v1",
|
||||
"permissions": {
|
||||
"actions": "write"
|
||||
},
|
||||
"InputDefinitions": null,
|
||||
"InputValues": null,
|
||||
"SecretDefinitions": null,
|
||||
"SecretValues": null,
|
||||
"InheritSecrets": false,
|
||||
"Outputs": null,
|
||||
"Defaults": null,
|
||||
"Env": null,
|
||||
"Concurrency": null,
|
||||
"EmbeddedConcurrency": null,
|
||||
"Strategy": null,
|
||||
"Jobs": [
|
||||
"jobs": [
|
||||
{
|
||||
"Type": "reusableWorkflowJob",
|
||||
"Id": "deploy-level-1",
|
||||
"Name": "deploy-level-1",
|
||||
"Needs": [],
|
||||
"If": {
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "deploy-level-1",
|
||||
"name": "deploy-level-1",
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"Ref": "contoso/templates/.github/workflows/deploy-level-2.yml@v1",
|
||||
"Permissions": {
|
||||
"ref": "contoso/templates/.github/workflows/deploy-level-2.yml@v1",
|
||||
"permissions": {
|
||||
"actions": "read"
|
||||
},
|
||||
"InputDefinitions": null,
|
||||
"InputValues": null,
|
||||
"SecretDefinitions": null,
|
||||
"SecretValues": null,
|
||||
"InheritSecrets": false,
|
||||
"Outputs": null,
|
||||
"Defaults": null,
|
||||
"Env": null,
|
||||
"Concurrency": null,
|
||||
"EmbeddedConcurrency": null,
|
||||
"Strategy": null,
|
||||
"Jobs": [
|
||||
"jobs": [
|
||||
{
|
||||
"Type": "reusableWorkflowJob",
|
||||
"Id": "deploy-level-3",
|
||||
"Name": "deploy-level-3",
|
||||
"Needs": [],
|
||||
"If": {
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "deploy-level-3",
|
||||
"name": "deploy-level-3",
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"Ref": "contoso/templates/.github/workflows/deploy-level-3.yml@v1",
|
||||
"Permissions": {
|
||||
"ref": "contoso/templates/.github/workflows/deploy-level-3.yml@v1",
|
||||
"permissions": {
|
||||
"actions": "read"
|
||||
},
|
||||
"InputDefinitions": null,
|
||||
"InputValues": null,
|
||||
"SecretDefinitions": null,
|
||||
"SecretValues": null,
|
||||
"InheritSecrets": false,
|
||||
"Outputs": null,
|
||||
"Defaults": null,
|
||||
"Env": null,
|
||||
"Concurrency": null,
|
||||
"EmbeddedConcurrency": null,
|
||||
"Strategy": null,
|
||||
"Jobs": [
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": "deploy-level-3",
|
||||
|
||||
+8
-19
@@ -47,30 +47,19 @@ permissions:
|
||||
},
|
||||
"jobs": [
|
||||
{
|
||||
"Type": "reusableWorkflowJob",
|
||||
"Id": "build",
|
||||
"Name": "build",
|
||||
"Needs": [],
|
||||
"If": {
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "build",
|
||||
"name": "build",
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"Ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"Permissions": {
|
||||
"ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"permissions": {
|
||||
"actions": "write"
|
||||
},
|
||||
"InputDefinitions": null,
|
||||
"InputValues": null,
|
||||
"SecretDefinitions": null,
|
||||
"SecretValues": null,
|
||||
"InheritSecrets": false,
|
||||
"Outputs": null,
|
||||
"Defaults": null,
|
||||
"Env": null,
|
||||
"Concurrency": null,
|
||||
"EmbeddedConcurrency": null,
|
||||
"Strategy": null,
|
||||
"Jobs": [
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": "deploy",
|
||||
|
||||
Vendored
+8
-19
@@ -47,30 +47,19 @@ jobs:
|
||||
},
|
||||
"jobs": [
|
||||
{
|
||||
"Type": "reusableWorkflowJob",
|
||||
"Id": "build",
|
||||
"Name": "build",
|
||||
"Needs": [],
|
||||
"If": {
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "build",
|
||||
"name": "build",
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"Ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"Permissions": {
|
||||
"ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"permissions": {
|
||||
"actions": "read"
|
||||
},
|
||||
"InputDefinitions": null,
|
||||
"InputValues": null,
|
||||
"SecretDefinitions": null,
|
||||
"SecretValues": null,
|
||||
"InheritSecrets": false,
|
||||
"Outputs": null,
|
||||
"Defaults": null,
|
||||
"Env": null,
|
||||
"Concurrency": null,
|
||||
"EmbeddedConcurrency": null,
|
||||
"Strategy": null,
|
||||
"Jobs": [
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": "deploy",
|
||||
|
||||
+15
-38
@@ -27,28 +27,16 @@ jobs:
|
||||
{
|
||||
"jobs": [
|
||||
{
|
||||
"Type": "reusableWorkflowJob",
|
||||
"Id": "build",
|
||||
"Name": "build",
|
||||
"Needs": [],
|
||||
"If": {
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "build",
|
||||
"name": "build",
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"Ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"Permissions": null,
|
||||
"InputDefinitions": null,
|
||||
"InputValues": null,
|
||||
"SecretDefinitions": null,
|
||||
"SecretValues": null,
|
||||
"InheritSecrets": false,
|
||||
"Outputs": null,
|
||||
"Defaults": null,
|
||||
"Env": null,
|
||||
"Concurrency": null,
|
||||
"EmbeddedConcurrency": null,
|
||||
"Strategy": null,
|
||||
"Jobs": [
|
||||
"ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": "deploy",
|
||||
@@ -72,30 +60,19 @@ jobs:
|
||||
]
|
||||
},
|
||||
{
|
||||
"Type": "reusableWorkflowJob",
|
||||
"Id": "build2",
|
||||
"Name": "build2",
|
||||
"Needs": [],
|
||||
"If": {
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "build2",
|
||||
"name": "build2",
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"Ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"Permissions": {
|
||||
"ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"permissions": {
|
||||
"actions": "write"
|
||||
},
|
||||
"InputDefinitions": null,
|
||||
"InputValues": null,
|
||||
"SecretDefinitions": null,
|
||||
"SecretValues": null,
|
||||
"InheritSecrets": false,
|
||||
"Outputs": null,
|
||||
"Defaults": null,
|
||||
"Env": null,
|
||||
"Concurrency": null,
|
||||
"EmbeddedConcurrency": null,
|
||||
"Strategy": null,
|
||||
"Jobs": [
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": "deploy",
|
||||
|
||||
+15
-38
@@ -27,28 +27,16 @@ jobs:
|
||||
{
|
||||
"jobs": [
|
||||
{
|
||||
"Type": "reusableWorkflowJob",
|
||||
"Id": "build",
|
||||
"Name": "build",
|
||||
"Needs": [],
|
||||
"If": {
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "build",
|
||||
"name": "build",
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"Ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"Permissions": null,
|
||||
"InputDefinitions": null,
|
||||
"InputValues": null,
|
||||
"SecretDefinitions": null,
|
||||
"SecretValues": null,
|
||||
"InheritSecrets": false,
|
||||
"Outputs": null,
|
||||
"Defaults": null,
|
||||
"Env": null,
|
||||
"Concurrency": null,
|
||||
"EmbeddedConcurrency": null,
|
||||
"Strategy": null,
|
||||
"Jobs": [
|
||||
"ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": "deploy",
|
||||
@@ -72,30 +60,19 @@ jobs:
|
||||
]
|
||||
},
|
||||
{
|
||||
"Type": "reusableWorkflowJob",
|
||||
"Id": "build2",
|
||||
"Name": "build2",
|
||||
"Needs": [],
|
||||
"If": {
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "build2",
|
||||
"name": "build2",
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"Ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"Permissions": {
|
||||
"ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"permissions": {
|
||||
"actions": "write"
|
||||
},
|
||||
"InputDefinitions": null,
|
||||
"InputValues": null,
|
||||
"SecretDefinitions": null,
|
||||
"SecretValues": null,
|
||||
"InheritSecrets": false,
|
||||
"Outputs": null,
|
||||
"Defaults": null,
|
||||
"Env": null,
|
||||
"Concurrency": null,
|
||||
"EmbeddedConcurrency": null,
|
||||
"Strategy": null,
|
||||
"Jobs": [
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": "deploy",
|
||||
|
||||
+16
-38
@@ -32,30 +32,19 @@ jobs:
|
||||
},
|
||||
"jobs": [
|
||||
{
|
||||
"Type": "reusableWorkflowJob",
|
||||
"Id": "build",
|
||||
"Name": "build",
|
||||
"Needs": [],
|
||||
"If": {
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "build",
|
||||
"name": "build",
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"Ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"Permissions": {
|
||||
"ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"permissions": {
|
||||
"actions": "read"
|
||||
},
|
||||
"InputDefinitions": null,
|
||||
"InputValues": null,
|
||||
"SecretDefinitions": null,
|
||||
"SecretValues": null,
|
||||
"InheritSecrets": false,
|
||||
"Outputs": null,
|
||||
"Defaults": null,
|
||||
"Env": null,
|
||||
"Concurrency": null,
|
||||
"EmbeddedConcurrency": null,
|
||||
"Strategy": null,
|
||||
"Jobs": [
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": "deploy",
|
||||
@@ -82,30 +71,19 @@ jobs:
|
||||
]
|
||||
},
|
||||
{
|
||||
"Type": "reusableWorkflowJob",
|
||||
"Id": "build2",
|
||||
"Name": "build2",
|
||||
"Needs": [],
|
||||
"If": {
|
||||
"type": "reusableWorkflowJob",
|
||||
"id": "build2",
|
||||
"name": "build2",
|
||||
"needs": [],
|
||||
"if": {
|
||||
"type": 3,
|
||||
"expr": "success()"
|
||||
},
|
||||
"Ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"Permissions": {
|
||||
"ref": "contoso/templates/.github/workflows/build.yml@v1",
|
||||
"permissions": {
|
||||
"actions": "write"
|
||||
},
|
||||
"InputDefinitions": null,
|
||||
"InputValues": null,
|
||||
"SecretDefinitions": null,
|
||||
"SecretValues": null,
|
||||
"InheritSecrets": false,
|
||||
"Outputs": null,
|
||||
"Defaults": null,
|
||||
"Env": null,
|
||||
"Concurrency": null,
|
||||
"EmbeddedConcurrency": null,
|
||||
"Strategy": null,
|
||||
"Jobs": [
|
||||
"jobs": [
|
||||
{
|
||||
"type": "job",
|
||||
"id": "deploy",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user