Lint the workflow parser package
This commit is contained in:
@@ -4,4 +4,8 @@ module.exports = {
|
|||||||
project: './tsconfig.json',
|
project: './tsconfig.json',
|
||||||
tsconfigRootDir: __dirname,
|
tsconfigRootDir: __dirname,
|
||||||
},
|
},
|
||||||
|
rules: {
|
||||||
|
// TypeScript doesn't correctly detect toString() implementations on the token classes
|
||||||
|
"@typescript-eslint/restrict-template-expressions": "off",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import {StringToken} from "./templates/tokens";
|
import {StringToken} from "./templates/tokens";
|
||||||
import {isBasicExpression, isString} from "./templates/tokens/type-guards";
|
import {isBasicExpression, isString} from "./templates/tokens/type-guards";
|
||||||
import {nullTrace} from "./test-utils/null-trace";
|
import {nullTrace} from "./test-utils/null-trace";
|
||||||
@@ -25,7 +26,7 @@ jobs:
|
|||||||
const runNameMapping = run.get(1)!;
|
const runNameMapping = run.get(1)!;
|
||||||
expect(runNameMapping?.key?.assertString("run-name key").value).toBe("run-name");
|
expect(runNameMapping?.key?.assertString("run-name key").value).toBe("run-name");
|
||||||
|
|
||||||
const v = runNameMapping.value!;
|
const v = runNameMapping.value;
|
||||||
expect(v).not.toBeUndefined();
|
expect(v).not.toBeUndefined();
|
||||||
|
|
||||||
if (!isBasicExpression(v)) {
|
if (!isBasicExpression(v)) {
|
||||||
@@ -61,14 +62,14 @@ jobs:
|
|||||||
const jobsMapping = run.get(1)!;
|
const jobsMapping = run.get(1)!;
|
||||||
expect(jobsMapping?.key?.assertString("jobs").value).toBe("jobs");
|
expect(jobsMapping?.key?.assertString("jobs").value).toBe("jobs");
|
||||||
|
|
||||||
const job = jobsMapping.value!.assertMapping("jobs")!.get(0)!.value!.assertMapping("job");
|
const job = jobsMapping.value.assertMapping("jobs").get(0).value.assertMapping("job");
|
||||||
const stepRun = job
|
const stepRun = job
|
||||||
.get(1)
|
.get(1)
|
||||||
.value!.assertSequence("steps")
|
.value.assertSequence("steps")
|
||||||
.get(0)
|
.get(0)
|
||||||
.assertMapping("step")
|
.assertMapping("step")
|
||||||
.get(0)
|
.get(0)
|
||||||
.value!.assertScalar("step-run");
|
.value.assertScalar("step-run");
|
||||||
|
|
||||||
if (!isBasicExpression(stepRun)) {
|
if (!isBasicExpression(stepRun)) {
|
||||||
throw new Error("expected run-name to be a basic expression");
|
throw new Error("expected run-name to be a basic expression");
|
||||||
@@ -123,14 +124,14 @@ jobs:
|
|||||||
const jobsMapping = run.get(1)!;
|
const jobsMapping = run.get(1)!;
|
||||||
expect(jobsMapping?.key?.assertString("jobs").value).toBe("jobs");
|
expect(jobsMapping?.key?.assertString("jobs").value).toBe("jobs");
|
||||||
|
|
||||||
const job = jobsMapping.value!.assertMapping("jobs")!.get(0)!.value!.assertMapping("job");
|
const job = jobsMapping.value.assertMapping("jobs").get(0).value.assertMapping("job");
|
||||||
const stepRun = job
|
const stepRun = job
|
||||||
.get(1)
|
.get(1)
|
||||||
.value!.assertSequence("steps")
|
.value.assertSequence("steps")
|
||||||
.get(0)
|
.get(0)
|
||||||
.assertMapping("step")
|
.assertMapping("step")
|
||||||
.get(0)
|
.get(0)
|
||||||
.value!.assertScalar("step-run");
|
.value.assertScalar("step-run");
|
||||||
|
|
||||||
expect(isString(stepRun)).toBe(true);
|
expect(isString(stepRun)).toBe(true);
|
||||||
expect((stepRun as StringToken).value).toContain("${{");
|
expect((stepRun as StringToken).value).toContain("${{");
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ jobs:
|
|||||||
|
|
||||||
expect(result.context.errors.getErrors()).toHaveLength(0);
|
expect(result.context.errors.getErrors()).toHaveLength(0);
|
||||||
expect(result.value).not.toBeUndefined();
|
expect(result.value).not.toBeUndefined();
|
||||||
const root = result.value!.assertMapping("root");
|
const root = result.value!.assertMapping("root"); // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
||||||
expect(root.description).toBe("Workflow file with strict validation");
|
expect(root.description).toBe("Workflow file with strict validation");
|
||||||
for (const pair of root) {
|
for (const pair of root) {
|
||||||
const key = pair.key.assertString("key").value;
|
const key = pair.key.assertString("key").value;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function convertToJobContainer(context: TemplateContext, container: Templ
|
|||||||
|
|
||||||
// Skip validation for expressions for now to match
|
// Skip validation for expressions for now to match
|
||||||
// behavior of the other parsers
|
// behavior of the other parsers
|
||||||
for (const [_, token, __] of TemplateToken.traverse(container)) {
|
for (const [, token] of TemplateToken.traverse(container)) {
|
||||||
if (token.isExpression) {
|
if (token.isExpression) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export class IdBuilder {
|
|||||||
if (attempt === 1) {
|
if (attempt === 1) {
|
||||||
suffix = "";
|
suffix = "";
|
||||||
} else {
|
} else {
|
||||||
suffix = "_" + attempt;
|
suffix = `_${attempt}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const candidate = original.substring(0, Math.min(original.length, MAX_LENGTH - suffix.length)) + suffix;
|
const candidate = original.substring(0, Math.min(original.length, MAX_LENGTH - suffix.length)) + suffix;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {TemplateContext} from "../../templates/template-context";
|
import {TemplateContext} from "../../templates/template-context";
|
||||||
import {BasicExpressionToken, MappingToken, ScalarToken, StringToken, TemplateToken} from "../../templates/tokens";
|
import {BasicExpressionToken, MappingToken, ScalarToken, StringToken, TemplateToken} from "../../templates/tokens";
|
||||||
import {isMapping, isSequence, isString} from "../../templates/tokens/type-guards";
|
import {isSequence, isString} from "../../templates/tokens/type-guards";
|
||||||
import {Step, WorkflowJob} from "../workflow-template";
|
import {Step, WorkflowJob} from "../workflow-template";
|
||||||
import {convertConcurrency} from "./concurrency";
|
import {convertConcurrency} from "./concurrency";
|
||||||
import {convertToJobContainer, convertToJobServices} from "./container";
|
import {convertToJobContainer, convertToJobServices} from "./container";
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ export function convertWorkflowJobInputs(context: TemplateContext, job: Reusable
|
|||||||
const inputValues = createTokenMap(job["input-values"]?.assertMapping("workflow job input values"), "with");
|
const inputValues = createTokenMap(job["input-values"]?.assertMapping("workflow job input values"), "with");
|
||||||
|
|
||||||
if (inputDefinitions !== undefined) {
|
if (inputDefinitions !== undefined) {
|
||||||
for (const [_, [name, value]] of inputDefinitions) {
|
for (const [, [name, value]] of inputDefinitions) {
|
||||||
const inputSpec = createTokenMap(value.assertMapping(`input ${name}`), `input ${name} key`)!;
|
const inputSpec = createTokenMap(value.assertMapping(`input ${name}`), `input ${name} key`);
|
||||||
|
|
||||||
const inputTypeToken = inputSpec.get("type")?.[1];
|
const inputTypeToken = inputSpec?.get("type")?.[1];
|
||||||
if (!inputTypeToken) {
|
if (!inputTypeToken) {
|
||||||
// This should be validated by the template reader per the schema
|
// This should be validated by the template reader per the schema
|
||||||
continue;
|
continue;
|
||||||
@@ -31,7 +31,7 @@ export function convertWorkflowJobInputs(context: TemplateContext, job: Reusable
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (inputValues !== undefined) {
|
if (inputValues !== undefined) {
|
||||||
for (const [_, [name, value]] of inputValues) {
|
for (const [, [name, value]] of inputValues) {
|
||||||
if (!inputDefinitions?.has(name.toLowerCase())) {
|
if (!inputDefinitions?.has(name.toLowerCase())) {
|
||||||
context.error(value, `Invalid input, ${name} is not defined in the referenced workflow.`);
|
context.error(value, `Invalid input, ${name} is not defined in the referenced workflow.`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {createTokenMap} from "./inputs";
|
|||||||
|
|
||||||
export function convertWorkflowJobSecrets(context: TemplateContext, job: ReusableWorkflowJob) {
|
export function convertWorkflowJobSecrets(context: TemplateContext, job: ReusableWorkflowJob) {
|
||||||
// No validation if job passes all secrets
|
// No validation if job passes all secrets
|
||||||
if (!!job["inherit-secrets"]) {
|
if (job["inherit-secrets"]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,14 +17,14 @@ export function convertWorkflowJobSecrets(context: TemplateContext, job: Reusabl
|
|||||||
const secretValues = createTokenMap(job["secret-values"]?.assertMapping("workflow job secret values"), "secrets");
|
const secretValues = createTokenMap(job["secret-values"]?.assertMapping("workflow job secret values"), "secrets");
|
||||||
|
|
||||||
if (secretDefinitions !== undefined) {
|
if (secretDefinitions !== undefined) {
|
||||||
for (const [_, [name, value]] of secretDefinitions) {
|
for (const [, [name, value]] of secretDefinitions) {
|
||||||
if (value instanceof NullToken) {
|
if (value instanceof NullToken) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const secretSpec = createTokenMap(value.assertMapping(`secret ${name}`), `secret ${name} key`)!;
|
const secretSpec = createTokenMap(value.assertMapping(`secret ${name}`), `secret ${name} key`);
|
||||||
|
|
||||||
const required = secretSpec.get("required")?.[1].assertBoolean(`secret ${name} required`).value;
|
const required = secretSpec?.get("required")?.[1].assertBoolean(`secret ${name} required`).value;
|
||||||
if (required) {
|
if (required) {
|
||||||
if (secretValues == undefined || !secretValues.has(name.toLowerCase())) {
|
if (secretValues == undefined || !secretValues.has(name.toLowerCase())) {
|
||||||
context.error(job.ref, `Secret ${name} is required, but not provided while calling.`);
|
context.error(job.ref, `Secret ${name} is required, but not provided while calling.`);
|
||||||
@@ -34,7 +34,7 @@ export function convertWorkflowJobSecrets(context: TemplateContext, job: Reusabl
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (secretValues !== undefined) {
|
if (secretValues !== undefined) {
|
||||||
for (const [_, [name, value]] of secretValues) {
|
for (const [, [name, value]] of secretValues) {
|
||||||
if (!secretDefinitions?.has(name.toLowerCase())) {
|
if (!secretDefinitions?.has(name.toLowerCase())) {
|
||||||
context.error(value, `Invalid secret, ${name} is not defined in the referenced workflow.`);
|
context.error(value, `Invalid secret, ${name} is not defined in the referenced workflow.`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import {TemplateSchema} from ".";
|
|
||||||
import {DEFINITION, BOOLEAN} from "../template-constants";
|
import {DEFINITION, BOOLEAN} from "../template-constants";
|
||||||
import {MappingToken, LiteralToken} from "../tokens";
|
import {MappingToken, LiteralToken} from "../tokens";
|
||||||
import {TokenType} from "../tokens/types";
|
import {TokenType} from "../tokens/types";
|
||||||
@@ -40,5 +39,7 @@ export class BooleanDefinition extends ScalarDefinition {
|
|||||||
return literal.templateTokenType === TokenType.Boolean;
|
return literal.templateTokenType === TokenType.Boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override validate(schema: TemplateSchema, name: string): void {}
|
public override validate(): void {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import {TemplateSchema} from "./template-schema";
|
|
||||||
import {DEFINITION, NULL} from "../template-constants";
|
import {DEFINITION, NULL} from "../template-constants";
|
||||||
import {MappingToken, LiteralToken} from "../tokens";
|
import {MappingToken, LiteralToken} from "../tokens";
|
||||||
import {DefinitionType} from "./definition-type";
|
import {DefinitionType} from "./definition-type";
|
||||||
@@ -40,5 +39,7 @@ export class NullDefinition extends ScalarDefinition {
|
|||||||
return literal.templateTokenType === TokenType.Null;
|
return literal.templateTokenType === TokenType.Null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override validate(schema: TemplateSchema, name: string): void {}
|
public override validate(): void {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import {TemplateSchema} from "./template-schema";
|
|
||||||
import {DEFINITION, NUMBER} from "../template-constants";
|
import {DEFINITION, NUMBER} from "../template-constants";
|
||||||
import {MappingToken, LiteralToken} from "../tokens";
|
import {MappingToken, LiteralToken} from "../tokens";
|
||||||
import {DefinitionType} from "./definition-type";
|
import {DefinitionType} from "./definition-type";
|
||||||
@@ -40,5 +39,7 @@ export class NumberDefinition extends ScalarDefinition {
|
|||||||
return literal.templateTokenType === TokenType.Number;
|
return literal.templateTokenType === TokenType.Number;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override validate(schema: TemplateSchema, name: string): void {}
|
public override validate(): void {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {LiteralToken, MappingToken, StringToken} from "../tokens";
|
|||||||
import {TokenType} from "../tokens/types";
|
import {TokenType} from "../tokens/types";
|
||||||
import {DefinitionType} from "./definition-type";
|
import {DefinitionType} from "./definition-type";
|
||||||
import {ScalarDefinition} from "./scalar-definition";
|
import {ScalarDefinition} from "./scalar-definition";
|
||||||
import {TemplateSchema} from "./template-schema";
|
|
||||||
|
|
||||||
export class StringDefinition extends ScalarDefinition {
|
export class StringDefinition extends ScalarDefinition {
|
||||||
public constant = "";
|
public constant = "";
|
||||||
@@ -80,7 +79,7 @@ export class StringDefinition extends ScalarDefinition {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override validate(schema: TemplateSchema, name: string): void {
|
public override validate(): void {
|
||||||
if (this.constant && this.requireNonEmpty) {
|
if (this.constant && this.requireNonEmpty) {
|
||||||
throw new Error(`Properties '${CONSTANT}' and '${REQUIRE_NON_EMPTY}' cannot both be set`);
|
throw new Error(`Properties '${CONSTANT}' and '${REQUIRE_NON_EMPTY}' cannot both be set`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -324,6 +324,7 @@ export class TemplateSchema {
|
|||||||
const template = readTemplate(context, TEMPLATE_SCHEMA, objectReader, undefined);
|
const template = readTemplate(context, TEMPLATE_SCHEMA, objectReader, undefined);
|
||||||
context.errors.check();
|
context.errors.check();
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const mapping = template!.assertMapping(TEMPLATE_SCHEMA);
|
const mapping = template!.assertMapping(TEMPLATE_SCHEMA);
|
||||||
const schema = new TemplateSchema(mapping);
|
const schema = new TemplateSchema(mapping);
|
||||||
schema.validate();
|
schema.validate();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-type-assertion */
|
||||||
import {nullTrace} from "../../test-utils/null-trace";
|
import {nullTrace} from "../../test-utils/null-trace";
|
||||||
import {parseWorkflow} from "../../workflows/workflow-parser";
|
import {parseWorkflow} from "../../workflows/workflow-parser";
|
||||||
import {StringToken} from "./string-token";
|
import {StringToken} from "./string-token";
|
||||||
@@ -20,13 +21,13 @@ describe("traverse", () => {
|
|||||||
expect(traverser.next()!.value).toEqual([undefined, root, undefined]);
|
expect(traverser.next()!.value).toEqual([undefined, root, undefined]);
|
||||||
|
|
||||||
// On
|
// On
|
||||||
const onResult = traverser.next()!.value!;
|
const onResult = traverser.next().value!;
|
||||||
expect(onResult[0]).toBe(root);
|
expect(onResult[0]).toBe(root);
|
||||||
expect(getValue(onResult[1])).toEqual("on");
|
expect(getValue(onResult[1])).toEqual("on");
|
||||||
expect(onResult[2]).toBeUndefined();
|
expect(onResult[2]).toBeUndefined();
|
||||||
|
|
||||||
// Push
|
// Push
|
||||||
const pushResult = traverser.next()!.value!;
|
const pushResult = traverser.next().value!;
|
||||||
expect(pushResult[0]).toBe(root);
|
expect(pushResult[0]).toBe(root);
|
||||||
expect(getValue(pushResult[1])).toEqual("push");
|
expect(getValue(pushResult[1])).toEqual("push");
|
||||||
expect(getValue(pushResult[2])).toEqual("on");
|
expect(getValue(pushResult[2])).toEqual("on");
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as fs from "fs";
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import * as YAML from "yaml";
|
import * as YAML from "yaml";
|
||||||
import {convertWorkflowTemplate} from "./model/convert";
|
import {convertWorkflowTemplate} from "./model/convert";
|
||||||
import {TraceWriter} from "./templates/trace-writer";
|
import {NoOperationTraceWriter} from "./templates/trace-writer";
|
||||||
import {File} from "./workflows/file";
|
import {File} from "./workflows/file";
|
||||||
import {FileProvider} from "./workflows/file-provider";
|
import {FileProvider} from "./workflows/file-provider";
|
||||||
import {fileIdentifier, FileReference} from "./workflows/file-reference";
|
import {fileIdentifier, FileReference} from "./workflows/file-reference";
|
||||||
@@ -13,12 +13,7 @@ interface TestOptions {
|
|||||||
skip?: string[];
|
skip?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const nullTrace: TraceWriter = {
|
const nullTrace = new NoOperationTraceWriter();
|
||||||
info: x => {},
|
|
||||||
verbose: x => {},
|
|
||||||
error: x => {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testFiles = "./testdata/reader";
|
const testFiles = "./testdata/reader";
|
||||||
const skippedTestsFile = "./testdata/skipped-tests.txt";
|
const skippedTestsFile = "./testdata/skipped-tests.txt";
|
||||||
|
|
||||||
@@ -44,7 +39,7 @@ describe("x-lang tests", () => {
|
|||||||
const testDocs: string[] = inputFile.split(/\r?\n---\r?\n/);
|
const testDocs: string[] = inputFile.split(/\r?\n---\r?\n/);
|
||||||
expect(testDocs.length).toBeGreaterThanOrEqual(3);
|
expect(testDocs.length).toBeGreaterThanOrEqual(3);
|
||||||
|
|
||||||
const testOptions: TestOptions = YAML.parse(testDocs[0]);
|
const testOptions = YAML.parse(testDocs[0]) as TestOptions;
|
||||||
const unsupportedTest = skippedTests.has(file);
|
const unsupportedTest = skippedTests.has(file);
|
||||||
|
|
||||||
const test = async () => {
|
const test = async () => {
|
||||||
@@ -65,10 +60,10 @@ describe("x-lang tests", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const testFileProvider: FileProvider = {
|
const testFileProvider: FileProvider = {
|
||||||
getFileContent: async (ref: FileReference) => {
|
getFileContent: (ref: FileReference) => {
|
||||||
const file = reusableWorkflows[fileIdentifier(ref)];
|
const file = reusableWorkflows[fileIdentifier(ref)];
|
||||||
if (file) {
|
if (file) {
|
||||||
return file;
|
return Promise.resolve(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error("File not found: " + fileName);
|
throw new Error("File not found: " + fileName);
|
||||||
@@ -87,7 +82,7 @@ describe("x-lang tests", () => {
|
|||||||
|
|
||||||
const workflowTemplate = await convertWorkflowTemplate(
|
const workflowTemplate = await convertWorkflowTemplate(
|
||||||
parseResult.context,
|
parseResult.context,
|
||||||
parseResult.value!,
|
parseResult.value!, // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
||||||
testFileProvider,
|
testFileProvider,
|
||||||
{
|
{
|
||||||
fetchReusableWorkflowDepth: 1
|
fetchReusableWorkflowDepth: 1
|
||||||
@@ -99,11 +94,13 @@ describe("x-lang tests", () => {
|
|||||||
const includeEvents =
|
const includeEvents =
|
||||||
testOptions.skip !== undefined && contains(testOptions.skip, "Go") && contains(testOptions.skip, "C#");
|
testOptions.skip !== undefined && contains(testOptions.skip, "Go") && contains(testOptions.skip, "C#");
|
||||||
if (!includeEvents) {
|
if (!includeEvents) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
||||||
delete (workflowTemplate as any).events;
|
delete (workflowTemplate as any).events;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Other parsers don't have a partial template when there are errors
|
// Other parsers don't have a partial template when there are errors
|
||||||
if (workflowTemplate.errors) {
|
if (workflowTemplate.errors) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
||||||
delete (workflowTemplate as any).jobs;
|
delete (workflowTemplate as any).jobs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user