-
Notifications
You must be signed in to change notification settings - Fork 210
Expand file tree
/
Copy pathvalidator.ts
More file actions
166 lines (147 loc) · 7.29 KB
/
Copy pathvalidator.ts
File metadata and controls
166 lines (147 loc) · 7.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
import Ajv from "ajv";
import {Job} from "./job.js";
import assert from "assert";
import chalk from "chalk-template";
import schema from "./schema.js";
import {betterAjvErrors} from "./schema-error.js";
import terminalLink from "terminal-link";
import {Argv} from "./argv.js";
const MAX_ERRORS = 5;
export class Validator {
static jsonSchemaValidation ({pathToExpandedGitLabCi, gitLabCiConfig, argv}: {
pathToExpandedGitLabCi: string;
gitLabCiConfig: object;
argv: Argv;
}) {
const ajv = new Ajv({
verbose: true,
allErrors: true,
allowUnionTypes: true,
validateFormats: false,
strictTypes: false, // to suppress the missing types defined in the gitlab-ci json schema
keywords: ["markdownDescription"],
});
const validate = ajv.compile(schema);
const valid = validate(gitLabCiConfig);
if (valid) return;
const betterErrors = betterAjvErrors({
data: gitLabCiConfig,
errors: validate.errors,
}).filter(betterError => !argv.ignoreSchemaPaths.includes(betterError.schemaPath));
let e: string = "";
for (let i = 0, len = betterErrors.length; i < len; i++) {
if (i + 1 > MAX_ERRORS) {
e += `\t... and ${len - MAX_ERRORS} more`;
break;
}
e += chalk`\t• {redBright ${betterErrors[i].message}} at {blueBright ${betterErrors[i].path}} {grey [${betterErrors[i].schemaPath}]}\n`;
}
assert(valid || betterErrors.length == 0, chalk`
{reset Invalid .gitlab-ci.yml configuration!
${e.trimEnd()}
For further troubleshooting, consider either of the following:
\t• Copy the content of {blueBright ${terminalLink(".gitlab-ci-local/expanded-gitlab-ci.yml", pathToExpandedGitLabCi)}} to the ${terminalLink("pipeline editor", "https://docs.gitlab.com/ee/ci/pipeline_editor/")} to debug it
\t• Use --ignore-schema-paths "#/definitions/tags/minItems" --ignore-schema-paths "#/additionalProperties" to partially disable certain validation rule
\t• Use --json-schema-validation=false to disable schema validation (not recommended)}
`);
}
private static needs (jobs: ReadonlyArray<Job>, stages: readonly string[]): string[] {
const warnings: string[] = [];
for (const job of jobs) {
if (job.needs === null || job.needs.length === 0) continue;
for (const [i, need] of job.needs.entries()) {
if (need.pipeline) {
warnings.push(`${job.name}.needs[${i}].job:${need.job} ignored, pipeline key not supported`);
continue;
}
if (need.project) {
warnings.push(`${job.name}.needs[${i}] ignored, project key not supported`);
continue;
}
const needJob = jobs.find(j => j.baseName === need.job);
if (need.optional && !needJob) continue;
assert(needJob != null, chalk`needs: [{blueBright ${need.job}}] for {blueBright ${job.baseName}} could not be found`);
const needJobStageIndex = stages.indexOf(needJob.stage);
const jobStageIndex = stages.indexOf(job.stage);
assert(needJobStageIndex <= jobStageIndex, chalk`needs: [{blueBright ${needJob.name}}] for {blueBright ${job.name}} is in a future stage`);
}
}
return warnings;
}
private static dependencies (jobs: ReadonlyArray<Job>, stages: readonly string[]) {
for (const job of jobs) {
if (job.dependencies === null || job.dependencies.length === 0) continue;
const undefDeps = job.dependencies.filter((j) => !jobs.some(n => n.baseName === j));
assert(undefDeps.length !== job.dependencies.length, chalk`dependencies: [{blueBright ${undefDeps.join(",")}}] for {blueBright ${job.name}} cannot be found`);
for (const dep of job.dependencies) {
const depJob = jobs.find(j => j.baseName === dep);
assert(depJob != null, chalk`dependencies: [{blueBright ${dep}}] for {blueBright ${job.baseName}} could not be found`);
const depJobStageIndex = stages.indexOf(depJob.stage);
const jobStageIndex = stages.indexOf(job.stage);
assert(depJobStageIndex <= jobStageIndex, chalk`dependencies: [{blueBright ${depJob.name}}] for {blueBright ${job.name}} is in a future stage`);
}
}
}
private static dependenciesContainment (jobs: ReadonlyArray<Job>) {
for (const job of jobs) {
const needs = job.needs;
const dependencies = job.dependencies;
if (needs && needs.length === 0) continue;
if (!dependencies || !needs) continue;
const everyIncluded = dependencies.every((dep: string) => {
return needs.some(n => n.job === dep);
});
const assertMsg = `${job.formattedJobName} needs: '${needs.map(n => n.job).join(",")}' doesn't fully contain dependencies: '${dependencies.join(",")}'`;
assert(everyIncluded, assertMsg);
}
}
/**
* These jobs named are reserved keywords in GitLab CI but does not prevent the pipeline from running
* https://github.com/firecow/gitlab-ci-local/issues/1263
* @param jobsNames
* @private
*/
private static potentialIllegalJobName (jobsNames: string[]) {
const warnings = [];
for (const jobName of jobsNames) {
if (new Set(["types", "true", "false", "nil"]).has(jobName)) {
warnings.push(`Job name "${jobName}" is a reserved keyword. (https://docs.gitlab.com/ee/ci/jobs/#job-name-limitations)`);
}
}
return warnings;
}
private static scriptBlank (jobs: ReadonlyArray<Job>) {
for (const job of jobs) {
if (job.trigger) continue; // Jobs with trigger are allowed to have empty script
assert(job.scripts.length > 0, chalk`{blue ${job.name}} has empty script`);
}
}
private static arrayOfStrings (jobs: ReadonlyArray<Job>) {
for (const job of jobs) {
if (job.trigger) continue;
job.beforeScripts.forEach((s: any) => assert(typeof s === "string", chalk`{blue ${job.name}} before_script contains non string value`));
job.afterScripts.forEach((s: any) => assert(typeof s === "string", chalk`{blue ${job.name}} after_script contains non string value`));
job.scripts.forEach((s: any) => assert(typeof s === "string", chalk`{blue ${job.name}} script contains non string value`));
}
}
static async run (jobs: ReadonlyArray<Job>, stages: readonly string[]) {
this.scriptBlank(jobs);
this.arrayOfStrings(jobs);
this.dependencies(jobs, stages);
this.dependenciesContainment(jobs);
return [
...this.needs(jobs, stages),
...this.potentialIllegalJobName(jobs.map(j => j.baseName)),
...this.artifacts(jobs),
];
}
private static artifacts (jobs: ReadonlyArray<Job>) {
const warnings: string[] = [];
for (const job of jobs) {
if (job.artifacts === null) {
warnings.push(`${job.name}.artifacts is null, ignoring.`);
}
}
return warnings;
}
}