mirror of
https://github.com/firecow/gitlab-ci-local.git
synced 2026-10-16 04:16:55 +02:00
feat: add option to skip CI/CD input validation (#1933)
build / yamllint (push) Has been cancelled
build / test (push) Has been cancelled
build / CodeQL (push) Has been cancelled
build / smoke-test (1.2.23, 24) (push) Has been cancelled
build / smoke-test (1.2.23, 25) (push) Has been cancelled
build / smoke-test (1.3.10, 24) (push) Has been cancelled
build / smoke-test (1.3.10, 25) (push) Has been cancelled
build / typecheck (push) Has been cancelled
build / eslint (push) Has been cancelled
build / unused-deps (push) Has been cancelled
build / yamllint (push) Has been cancelled
build / test (push) Has been cancelled
build / CodeQL (push) Has been cancelled
build / smoke-test (1.2.23, 24) (push) Has been cancelled
build / smoke-test (1.2.23, 25) (push) Has been cancelled
build / smoke-test (1.3.10, 24) (push) Has been cancelled
build / smoke-test (1.3.10, 25) (push) Has been cancelled
build / typecheck (push) Has been cancelled
build / eslint (push) Has been cancelled
build / unused-deps (push) Has been cancelled
This commit is contained in:
1 parent
f3c3248969
commit
110c1d85e0
11 files changed
+184
-13
No files matched your search
@@ -42,6 +42,7 @@ Get rid of all those dev specific shell scripts and make files.
|
||||
* [Remote File Variables](#remote-file-variables)
|
||||
* [Project File Variables](#project-file-variables)
|
||||
* [Decorators](#decorators)
|
||||
* [CI/CD Inputs](#cicd-inputs)
|
||||
* [Includes](#includes)
|
||||
* [Artifacts](#artifacts)
|
||||
* [Self Hosted Custom Ports](#self-hosted-custom-ports)
|
||||
@@ -391,6 +392,16 @@ A global configuration is possible when setting the following flag
|
||||
gitlab-ci-local --no-artifacts-to-source
|
||||
```
|
||||
|
||||
### CI/CD Inputs
|
||||
|
||||
Input types, options, and regular expressions are validated by default. If an outdated component declares an input type that does not match its default or provided value, use `--skip-input-validation` to render the pipeline without these checks.
|
||||
|
||||
```shell
|
||||
gitlab-ci-local --skip-input-validation --list
|
||||
```
|
||||
|
||||
Required inputs and interpolation keys are still validated.
|
||||
|
||||
### Includes
|
||||
|
||||
Includes from external sources are only fetched once and cached. Use `--fetch-includes` to ensure that the latest external sources are always fetched.
|
||||
|
||||
@@ -171,6 +171,10 @@ export class Argv {
|
||||
return this.map.get("inputsFile") ?? Argv.default.inputsFile;
|
||||
}
|
||||
|
||||
get skipInputValidation (): boolean {
|
||||
return this.map.get("skipInputValidation") ?? false;
|
||||
}
|
||||
|
||||
get ignoresFile (): string {
|
||||
return this.map.get("ignoresFile") ?? Argv.default.ignoresFile;
|
||||
}
|
||||
|
||||
@@ -138,6 +138,11 @@ process.on("SIGUSR2", async () => {
|
||||
requiresArg: true,
|
||||
default: Argv.default.inputsFile,
|
||||
})
|
||||
.option("skip-input-validation", {
|
||||
type: "boolean",
|
||||
description: "Skip CI/CD input type, options, and regex validation",
|
||||
requiresArg: false,
|
||||
})
|
||||
.option("ignores-file", {
|
||||
type: "string",
|
||||
description: "Path to an ignores file",
|
||||
|
||||
+2
-2
@@ -1786,14 +1786,14 @@ If you know what you're doing and would like to suppress this warning, use one o
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const content = await Parser.loadYaml(file, {}, true, this.writeStreams);
|
||||
const content = await Parser.loadYaml(file, {skipInputValidation: this.argv.skipInputValidation}, true, this.writeStreams);
|
||||
contents = {
|
||||
...contents,
|
||||
...content,
|
||||
};
|
||||
}
|
||||
} else if (include["artifact"]) {
|
||||
const content = await Parser.loadYaml(`${cwd}/${stateDir}/artifacts/${include["job"]}/${include["artifact"]}`, {}, true, this.writeStreams);
|
||||
const content = await Parser.loadYaml(`${cwd}/${stateDir}/artifacts/${include["job"]}/${include["artifact"]}`, {skipInputValidation: this.argv.skipInputValidation}, true, this.writeStreams);
|
||||
contents = {
|
||||
...contents,
|
||||
...content,
|
||||
|
||||
@@ -144,7 +144,7 @@ export class ParserIncludes {
|
||||
}
|
||||
for (const localFile of files) {
|
||||
const mergedInputs = {...(value.inputs ?? {}), ...globalInputs};
|
||||
const content = await Parser.loadYaml(localFile, {inputs: mergedInputs}, expandVariables, writeStreams);
|
||||
const content = await Parser.loadYaml(localFile, {inputs: mergedInputs, skipInputValidation: argv.skipInputValidation}, expandVariables, writeStreams);
|
||||
includeDatas = includeDatas.concat(await this.init(content, opts));
|
||||
}
|
||||
} else if (value["project"]) {
|
||||
@@ -155,7 +155,7 @@ export class ParserIncludes {
|
||||
const matches = globbySync(normalizedFile, {cwd: includeDir, absolute: true}).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
||||
const filePaths = matches.length > 0 ? matches : [`${includeDir}/${normalizedFile}`];
|
||||
for (const filePath of filePaths) {
|
||||
const fileDoc = await Parser.loadYaml(filePath, {inputs: mergedInputs}, expandVariables, writeStreams);
|
||||
const fileDoc = await Parser.loadYaml(filePath, {inputs: mergedInputs, skipInputValidation: argv.skipInputValidation}, expandVariables, writeStreams);
|
||||
// Expand local includes inside a "project"-like include
|
||||
fileDoc["include"] = this.expandInnerLocalIncludes(fileDoc["include"], value["project"], value["ref"], opts);
|
||||
includeDatas = includeDatas.concat(await this.init(fileDoc, opts));
|
||||
@@ -185,7 +185,7 @@ export class ParserIncludes {
|
||||
const fileComponentInputs = isStructured ? (fileInputs[componentName] ?? {}) : {};
|
||||
const cliComponentSpecificInputs = cliComponentInputs[componentName] ?? {};
|
||||
const mergedInputs = {...(value.inputs ?? {}), ...globalInputs, ...fileComponentInputs, ...cliComponentSpecificInputs};
|
||||
const fileDoc = await Parser.loadYaml(file, {inputs: mergedInputs, component}, expandVariables, writeStreams);
|
||||
const fileDoc = await Parser.loadYaml(file, {inputs: mergedInputs, component, skipInputValidation: argv.skipInputValidation}, expandVariables, writeStreams);
|
||||
if (!component.isLocal) {
|
||||
// Expand local includes inside to a "project"-like include
|
||||
fileDoc["include"] = this.expandInnerLocalIncludes(fileDoc["include"], component.projectPath, component.effectiveRef, opts);
|
||||
@@ -196,14 +196,14 @@ export class ParserIncludes {
|
||||
const fsUrl = Utils.fsUrl(`https://${domain}/${project}/-/raw/${ref}/${file}`);
|
||||
const mergedInputs = {...(value.inputs ?? {}), ...globalInputs};
|
||||
const fileDoc = await Parser.loadYaml(
|
||||
`${cwd}/${stateDir}/includes/${fsUrl}`, {inputs: mergedInputs}, expandVariables, writeStreams,
|
||||
`${cwd}/${stateDir}/includes/${fsUrl}`, {inputs: mergedInputs, skipInputValidation: argv.skipInputValidation}, expandVariables, writeStreams,
|
||||
);
|
||||
includeDatas = includeDatas.concat(await this.init(fileDoc, opts));
|
||||
} else if (value["remote"]) {
|
||||
const fsUrl = Utils.fsUrl(value["remote"]);
|
||||
const mergedInputs = {...(value.inputs ?? {}), ...globalInputs};
|
||||
const fileDoc = await Parser.loadYaml(
|
||||
`${cwd}/${stateDir}/includes/${fsUrl}`, {inputs: mergedInputs}, expandVariables, writeStreams,
|
||||
`${cwd}/${stateDir}/includes/${fsUrl}`, {inputs: mergedInputs, skipInputValidation: argv.skipInputValidation}, expandVariables, writeStreams,
|
||||
);
|
||||
includeDatas = includeDatas.concat(await this.init(fileDoc, opts));
|
||||
} else {
|
||||
|
||||
+15
-4
@@ -112,12 +112,12 @@ export class Parser {
|
||||
const rootInputs = {...Utils.getGlobalFileInputs(fileInputs), ...inputs._cliGlobal};
|
||||
|
||||
let yamlDataList: any[] = [{stages: [".pre", "build", "test", "deploy", ".post"]}];
|
||||
const gitlabCiData = await Parser.loadYaml(`${cwd}/${file}`, {inputs: rootInputs}, this.expandVariables, writeStreams);
|
||||
const gitlabCiData = await Parser.loadYaml(`${cwd}/${file}`, {inputs: rootInputs, skipInputValidation: argv.skipInputValidation}, this.expandVariables, writeStreams);
|
||||
|
||||
yamlDataList = yamlDataList.concat(await ParserIncludes.init(gitlabCiData, {argv, cwd, stateDir, writeStreams, gitData, fetchIncludes, variables: expanded, expandVariables: this.expandVariables, maximumIncludes: argv.maximumIncludes, inputs}));
|
||||
ParserIncludes.resetCount();
|
||||
|
||||
const gitlabCiLocalData = await Parser.loadYaml(`${cwd}/.gitlab-ci-local.yml`, {}, this.expandVariables, writeStreams);
|
||||
const gitlabCiLocalData = await Parser.loadYaml(`${cwd}/.gitlab-ci-local.yml`, {skipInputValidation: argv.skipInputValidation}, this.expandVariables, writeStreams);
|
||||
yamlDataList = yamlDataList.concat(await ParserIncludes.init(gitlabCiLocalData, {argv, cwd, stateDir, writeStreams, gitData, fetchIncludes, variables: expanded, expandVariables: this.expandVariables, maximumIncludes: argv.maximumIncludes, inputs}));
|
||||
ParserIncludes.resetCount();
|
||||
|
||||
@@ -480,8 +480,19 @@ function validateInput (ctx: any) {
|
||||
function parseIncludeInputs (ctx: any): {inputValue: any; inputType: InputType} {
|
||||
validateInterpolationKey(ctx);
|
||||
validateInterpolationFunctions(ctx);
|
||||
validateInput(ctx);
|
||||
return {inputValue: getInputValue(ctx), inputType: getExpectedInputType(ctx)};
|
||||
if (!ctx.skipInputValidation) validateInput(ctx);
|
||||
|
||||
const inputValue = getInputValue(ctx);
|
||||
const inputType = ctx.skipInputValidation ? getActualInputType(inputValue, ctx) : getExpectedInputType(ctx);
|
||||
return {inputValue, inputType};
|
||||
}
|
||||
|
||||
function getActualInputType (inputValue: any, ctx: any): InputType {
|
||||
const {configFilePath, interpolationKey} = ctx;
|
||||
const inputType = inputValue === null ? "null" : Array.isArray(inputValue) ? "array" : typeof inputValue;
|
||||
assert(INCLUDE_INPUTS_SUPPORTED_TYPES.includes(inputType as InputType),
|
||||
chalk`This GitLab CI configuration is invalid: \`{blueBright ${configFilePath}}\`: \`{blueBright ${interpolationKey}}\` input: provided value has unsupported type {blueBright ${inputType}}.`);
|
||||
return inputType as InputType;
|
||||
}
|
||||
|
||||
function getInputValue (ctx: any) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
spec:
|
||||
inputs:
|
||||
null_input:
|
||||
default: null
|
||||
---
|
||||
scan-website:
|
||||
script:
|
||||
- echo $[[ inputs.null_input ]]
|
||||
+3
@@ -3,7 +3,10 @@ spec:
|
||||
inputs:
|
||||
number_input:
|
||||
type: number
|
||||
default: "1"
|
||||
---
|
||||
scan-website:
|
||||
variables:
|
||||
NUMBER_INPUT: $[[ inputs.number_input ]]
|
||||
script:
|
||||
- echo $[[ inputs.number_input]]
|
||||
@@ -1,7 +1,5 @@
|
||||
---
|
||||
include:
|
||||
- local: '/.gitlab-ci-input-template.yml'
|
||||
inputs:
|
||||
number_input: "1"
|
||||
stages:
|
||||
- test
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
spec:
|
||||
inputs:
|
||||
object_input:
|
||||
default:
|
||||
key: value
|
||||
---
|
||||
scan-website:
|
||||
script:
|
||||
- echo $[[ inputs.object_input ]]
|
||||
@@ -1,5 +1,6 @@
|
||||
import {WriteStreamsMock} from "../../../src/write-streams.js";
|
||||
import {handler} from "../../../src/handler.js";
|
||||
import {Parser} from "../../../src/parser.js";
|
||||
import assert, {AssertionError} from "assert";
|
||||
import {initSpawnSpy} from "../../mocks/utils.mock.js";
|
||||
import {WhenStatics} from "../../mocks/when-statics.js";
|
||||
@@ -201,6 +202,85 @@ test.concurrent("include-inputs inputs validation for number", async () => {
|
||||
throw new Error("Error is expected but not thrown/caught");
|
||||
});
|
||||
|
||||
test.concurrent("include-inputs can skip type validation for a mismatched default type", async () => {
|
||||
const writeStreams = new WriteStreamsMock();
|
||||
await handler({
|
||||
cwd: "tests/test-cases/include-inputs/input-templates/type-validation/number",
|
||||
preview: true,
|
||||
skipInputValidation: true,
|
||||
}, writeStreams);
|
||||
|
||||
const expected = `---
|
||||
stages:
|
||||
- .pre
|
||||
- test
|
||||
- .post
|
||||
scan-website:
|
||||
variables:
|
||||
NUMBER_INPUT: '1'
|
||||
script:
|
||||
- echo 1`;
|
||||
|
||||
expect(writeStreams.stdoutLines[0]).toEqual(expected);
|
||||
});
|
||||
|
||||
test.concurrent("include-inputs can skip validation and preserve a provided value's actual type", async () => {
|
||||
const writeStreams = new WriteStreamsMock();
|
||||
const templatePath =
|
||||
"tests/test-cases/include-inputs/input-templates/type-validation/number/.gitlab-ci-input-template.yml";
|
||||
|
||||
const parsed = await Parser.loadYaml(
|
||||
templatePath,
|
||||
{
|
||||
inputs: {
|
||||
number_input: "1",
|
||||
},
|
||||
skipInputValidation: true,
|
||||
},
|
||||
true,
|
||||
writeStreams,
|
||||
);
|
||||
|
||||
expect(parsed["scan-website"].variables.NUMBER_INPUT).toBe("1");
|
||||
expect(typeof parsed["scan-website"].variables.NUMBER_INPUT).toBe("string");
|
||||
});
|
||||
|
||||
test.concurrent("include-inputs reports unsupported value types when validation is skipped", async () => {
|
||||
try {
|
||||
const writeStreams = new WriteStreamsMock();
|
||||
await handler({
|
||||
cwd: "tests/test-cases/include-inputs/input-templates/type-validation/object",
|
||||
preview: true,
|
||||
skipInputValidation: true,
|
||||
}, writeStreams);
|
||||
} catch (e: any) {
|
||||
assert(e instanceof AssertionError, "e is not instanceof AssertionError");
|
||||
expect(e.message).toContain("This GitLab CI configuration is invalid:");
|
||||
expect(e.message).toContain(chalk`\`{blueBright object_input}\` input: provided value has unsupported type {blueBright object}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error("Error is expected but not thrown/caught");
|
||||
});
|
||||
|
||||
test.concurrent("include-inputs reports null values when validation is skipped", async () => {
|
||||
try {
|
||||
const writeStreams = new WriteStreamsMock();
|
||||
await handler({
|
||||
cwd: "tests/test-cases/include-inputs/input-templates/type-validation/null",
|
||||
preview: true,
|
||||
skipInputValidation: true,
|
||||
}, writeStreams);
|
||||
} catch (e: any) {
|
||||
assert(e instanceof AssertionError, "e is not instanceof AssertionError");
|
||||
expect(e.message).toContain("This GitLab CI configuration is invalid:");
|
||||
expect(e.message).toContain(chalk`\`{blueBright null_input}\` input: provided value has unsupported type {blueBright null}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error("Error is expected but not thrown/caught");
|
||||
});
|
||||
|
||||
test.concurrent("include-inputs for type array", async () => {
|
||||
const writeStreams = new WriteStreamsMock();
|
||||
await handler({
|
||||
@@ -321,6 +401,26 @@ test.concurrent("include-inputs options validation", async () => {
|
||||
throw new Error("Error is expected but not thrown/caught");
|
||||
});
|
||||
|
||||
test.concurrent("include-inputs can skip options validation", async () => {
|
||||
const writeStreams = new WriteStreamsMock();
|
||||
await handler({
|
||||
cwd: "tests/test-cases/include-inputs/input-templates/options-validation",
|
||||
preview: true,
|
||||
skipInputValidation: true,
|
||||
}, writeStreams);
|
||||
|
||||
const expected = `---
|
||||
stages:
|
||||
- .pre
|
||||
- test
|
||||
- .post
|
||||
scan-website:
|
||||
script:
|
||||
- echo fizz`;
|
||||
|
||||
expect(writeStreams.stdoutLines[0]).toEqual(expected);
|
||||
});
|
||||
|
||||
test.concurrent("include-inputs options array1 validation", async () => {
|
||||
const writeStreams = new WriteStreamsMock();
|
||||
await handler({
|
||||
@@ -416,6 +516,26 @@ test.concurrent("include-inputs regex validation (invalid)", async () => {
|
||||
throw new Error("Error is expected but not thrown/caught");
|
||||
});
|
||||
|
||||
test.concurrent("include-inputs can skip regex validation", async () => {
|
||||
const writeStreams = new WriteStreamsMock();
|
||||
await handler({
|
||||
cwd: "tests/test-cases/include-inputs/input-templates/regex-validation",
|
||||
preview: true,
|
||||
skipInputValidation: true,
|
||||
}, writeStreams);
|
||||
|
||||
const expected = `---
|
||||
stages:
|
||||
- .pre
|
||||
- test
|
||||
- .post
|
||||
deploy:
|
||||
script:
|
||||
- echo invalid-version`;
|
||||
|
||||
expect(writeStreams.stdoutLines[0]).toEqual(expected);
|
||||
});
|
||||
|
||||
test.concurrent("include-inputs regex validation (valid)", async () => {
|
||||
const writeStreams = new WriteStreamsMock();
|
||||
await handler({
|
||||
|
||||
Reference in new issue
Block a user