Lint the workflow parser package
This commit is contained in:
@@ -4,4 +4,8 @@ module.exports = {
|
||||
project: './tsconfig.json',
|
||||
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 {isBasicExpression, isString} from "./templates/tokens/type-guards";
|
||||
import {nullTrace} from "./test-utils/null-trace";
|
||||
@@ -25,7 +26,7 @@ jobs:
|
||||
const runNameMapping = run.get(1)!;
|
||||
expect(runNameMapping?.key?.assertString("run-name key").value).toBe("run-name");
|
||||
|
||||
const v = runNameMapping.value!;
|
||||
const v = runNameMapping.value;
|
||||
expect(v).not.toBeUndefined();
|
||||
|
||||
if (!isBasicExpression(v)) {
|
||||
@@ -61,14 +62,14 @@ jobs:
|
||||
const jobsMapping = run.get(1)!;
|
||||
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
|
||||
.get(1)
|
||||
.value!.assertSequence("steps")
|
||||
.value.assertSequence("steps")
|
||||
.get(0)
|
||||
.assertMapping("step")
|
||||
.get(0)
|
||||
.value!.assertScalar("step-run");
|
||||
.value.assertScalar("step-run");
|
||||
|
||||
if (!isBasicExpression(stepRun)) {
|
||||
throw new Error("expected run-name to be a basic expression");
|
||||
@@ -123,14 +124,14 @@ jobs:
|
||||
const jobsMapping = run.get(1)!;
|
||||
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
|
||||
.get(1)
|
||||
.value!.assertSequence("steps")
|
||||
.value.assertSequence("steps")
|
||||
.get(0)
|
||||
.assertMapping("step")
|
||||
.get(0)
|
||||
.value!.assertScalar("step-run");
|
||||
.value.assertScalar("step-run");
|
||||
|
||||
expect(isString(stepRun)).toBe(true);
|
||||
expect((stepRun as StringToken).value).toContain("${{");
|
||||
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
|
||||
expect(result.context.errors.getErrors()).toHaveLength(0);
|
||||
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");
|
||||
for (const pair of root) {
|
||||
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
|
||||
// behavior of the other parsers
|
||||
for (const [_, token, __] of TemplateToken.traverse(container)) {
|
||||
for (const [, token] of TemplateToken.traverse(container)) {
|
||||
if (token.isExpression) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export class IdBuilder {
|
||||
if (attempt === 1) {
|
||||
suffix = "";
|
||||
} else {
|
||||
suffix = "_" + attempt;
|
||||
suffix = `_${attempt}`;
|
||||
}
|
||||
|
||||
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 {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 {convertConcurrency} from "./concurrency";
|
||||
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");
|
||||
|
||||
if (inputDefinitions !== undefined) {
|
||||
for (const [_, [name, value]] of inputDefinitions) {
|
||||
const inputSpec = createTokenMap(value.assertMapping(`input ${name}`), `input ${name} key`)!;
|
||||
for (const [, [name, value]] of inputDefinitions) {
|
||||
const inputSpec = createTokenMap(value.assertMapping(`input ${name}`), `input ${name} key`);
|
||||
|
||||
const inputTypeToken = inputSpec.get("type")?.[1];
|
||||
const inputTypeToken = inputSpec?.get("type")?.[1];
|
||||
if (!inputTypeToken) {
|
||||
// This should be validated by the template reader per the schema
|
||||
continue;
|
||||
@@ -31,7 +31,7 @@ export function convertWorkflowJobInputs(context: TemplateContext, job: Reusable
|
||||
}
|
||||
|
||||
if (inputValues !== undefined) {
|
||||
for (const [_, [name, value]] of inputValues) {
|
||||
for (const [, [name, value]] of inputValues) {
|
||||
if (!inputDefinitions?.has(name.toLowerCase())) {
|
||||
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) {
|
||||
// No validation if job passes all secrets
|
||||
if (!!job["inherit-secrets"]) {
|
||||
if (job["inherit-secrets"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -17,14 +17,14 @@ export function convertWorkflowJobSecrets(context: TemplateContext, job: Reusabl
|
||||
const secretValues = createTokenMap(job["secret-values"]?.assertMapping("workflow job secret values"), "secrets");
|
||||
|
||||
if (secretDefinitions !== undefined) {
|
||||
for (const [_, [name, value]] of secretDefinitions) {
|
||||
for (const [, [name, value]] of secretDefinitions) {
|
||||
if (value instanceof NullToken) {
|
||||
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 (secretValues == undefined || !secretValues.has(name.toLowerCase())) {
|
||||
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) {
|
||||
for (const [_, [name, value]] of secretValues) {
|
||||
for (const [, [name, value]] of secretValues) {
|
||||
if (!secretDefinitions?.has(name.toLowerCase())) {
|
||||
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 {MappingToken, LiteralToken} from "../tokens";
|
||||
import {TokenType} from "../tokens/types";
|
||||
@@ -40,5 +39,7 @@ export class BooleanDefinition extends ScalarDefinition {
|
||||
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 {MappingToken, LiteralToken} from "../tokens";
|
||||
import {DefinitionType} from "./definition-type";
|
||||
@@ -40,5 +39,7 @@ export class NullDefinition extends ScalarDefinition {
|
||||
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 {MappingToken, LiteralToken} from "../tokens";
|
||||
import {DefinitionType} from "./definition-type";
|
||||
@@ -40,5 +39,7 @@ export class NumberDefinition extends ScalarDefinition {
|
||||
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 {DefinitionType} from "./definition-type";
|
||||
import {ScalarDefinition} from "./scalar-definition";
|
||||
import {TemplateSchema} from "./template-schema";
|
||||
|
||||
export class StringDefinition extends ScalarDefinition {
|
||||
public constant = "";
|
||||
@@ -80,7 +79,7 @@ export class StringDefinition extends ScalarDefinition {
|
||||
return false;
|
||||
}
|
||||
|
||||
public override validate(schema: TemplateSchema, name: string): void {
|
||||
public override validate(): void {
|
||||
if (this.constant && this.requireNonEmpty) {
|
||||
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);
|
||||
context.errors.check();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const mapping = template!.assertMapping(TEMPLATE_SCHEMA);
|
||||
const schema = new TemplateSchema(mapping);
|
||||
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 {parseWorkflow} from "../../workflows/workflow-parser";
|
||||
import {StringToken} from "./string-token";
|
||||
@@ -20,13 +21,13 @@ describe("traverse", () => {
|
||||
expect(traverser.next()!.value).toEqual([undefined, root, undefined]);
|
||||
|
||||
// On
|
||||
const onResult = traverser.next()!.value!;
|
||||
const onResult = traverser.next().value!;
|
||||
expect(onResult[0]).toBe(root);
|
||||
expect(getValue(onResult[1])).toEqual("on");
|
||||
expect(onResult[2]).toBeUndefined();
|
||||
|
||||
// Push
|
||||
const pushResult = traverser.next()!.value!;
|
||||
const pushResult = traverser.next().value!;
|
||||
expect(pushResult[0]).toBe(root);
|
||||
expect(getValue(pushResult[1])).toEqual("push");
|
||||
expect(getValue(pushResult[2])).toEqual("on");
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as YAML from "yaml";
|
||||
import {convertWorkflowTemplate} from "./model/convert";
|
||||
import {TraceWriter} from "./templates/trace-writer";
|
||||
import {NoOperationTraceWriter} from "./templates/trace-writer";
|
||||
import {File} from "./workflows/file";
|
||||
import {FileProvider} from "./workflows/file-provider";
|
||||
import {fileIdentifier, FileReference} from "./workflows/file-reference";
|
||||
@@ -13,12 +13,7 @@ interface TestOptions {
|
||||
skip?: string[];
|
||||
}
|
||||
|
||||
const nullTrace: TraceWriter = {
|
||||
info: x => {},
|
||||
verbose: x => {},
|
||||
error: x => {}
|
||||
};
|
||||
|
||||
const nullTrace = new NoOperationTraceWriter();
|
||||
const testFiles = "./testdata/reader";
|
||||
const skippedTestsFile = "./testdata/skipped-tests.txt";
|
||||
|
||||
@@ -44,7 +39,7 @@ describe("x-lang tests", () => {
|
||||
const testDocs: string[] = inputFile.split(/\r?\n---\r?\n/);
|
||||
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 test = async () => {
|
||||
@@ -65,10 +60,10 @@ describe("x-lang tests", () => {
|
||||
}
|
||||
|
||||
const testFileProvider: FileProvider = {
|
||||
getFileContent: async (ref: FileReference) => {
|
||||
getFileContent: (ref: FileReference) => {
|
||||
const file = reusableWorkflows[fileIdentifier(ref)];
|
||||
if (file) {
|
||||
return file;
|
||||
return Promise.resolve(file);
|
||||
}
|
||||
|
||||
throw new Error("File not found: " + fileName);
|
||||
@@ -87,7 +82,7 @@ describe("x-lang tests", () => {
|
||||
|
||||
const workflowTemplate = await convertWorkflowTemplate(
|
||||
parseResult.context,
|
||||
parseResult.value!,
|
||||
parseResult.value!, // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
||||
testFileProvider,
|
||||
{
|
||||
fetchReusableWorkflowDepth: 1
|
||||
@@ -99,11 +94,13 @@ describe("x-lang tests", () => {
|
||||
const includeEvents =
|
||||
testOptions.skip !== undefined && contains(testOptions.skip, "Go") && contains(testOptions.skip, "C#");
|
||||
if (!includeEvents) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
||||
delete (workflowTemplate as any).events;
|
||||
}
|
||||
|
||||
// Other parsers don't have a partial template when there are errors
|
||||
if (workflowTemplate.errors) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
||||
delete (workflowTemplate as any).jobs;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user