From bb649a7995c2a8e4c5c741f4edc5dfe7109601b8 Mon Sep 17 00:00:00 2001 From: "tushar.gupta" Date: Tue, 7 Oct 2025 18:41:00 +0530 Subject: [PATCH 01/12] feat: add --validate-dependency-chain flag to verify all local job needs exist --- src/argv.ts | 4 ++++ src/commander.ts | 33 +++++++++++++++++++++++++++++++++ src/handler.ts | 10 ++++++++++ src/index.ts | 5 +++++ 4 files changed, 52 insertions(+) diff --git a/src/argv.ts b/src/argv.ts index e697dac21..4b182c939 100644 --- a/src/argv.ts +++ b/src/argv.ts @@ -266,6 +266,10 @@ export class Argv { return this.map.get("preview") ?? false; } + get validateDependencyChain (): boolean { + return this.map.get("validateDependencyChain") ?? false; + } + get shellIsolation (): boolean { // TODO: default to true in 5.x.x return this.map.get("shellIsolation") ?? false; diff --git a/src/commander.ts b/src/commander.ts index 006417aea..6dec9ead5 100644 --- a/src/commander.ts +++ b/src/commander.ts @@ -272,4 +272,37 @@ export class Commander { }); } + static validateDependencyChain (parser: Parser, writeStreams: WriteStreams, argv: any): boolean { + const allJobs = [...parser.jobs.values()]; + const activeJobs = allJobs.filter(j => j.when !== "never"); + const activeJobNames = new Set(activeJobs.map(job => job.name)); + const missingDependencies: {job: string; missing: string}[] = []; + + for (const job of activeJobs) { + if (job.needs) { + const localNeeds = job.needs.filter(n => !n.project && !n.pipeline); + + for (const need of localNeeds) { + if (!activeJobNames.has(need.job)) { + missingDependencies.push({ + job: job.name, + missing: need.job, + }); + } + } + } + } + + if (missingDependencies.length > 0) { + + + for (const dep of missingDependencies) { + writeStreams.stderr(chalk` {yellow ${dep.job}} needs {red ${dep.missing}} which does not exist\n`); + } + return false; + } + + writeStreams.stdout(chalk`{green ✓ All job dependencies are valid}\n`); + return true; + } } diff --git a/src/handler.ts b/src/handler.ts index bdbbe530a..513374bde 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -54,6 +54,16 @@ export async function handler (args: any, writeStreams: WriteStreams, jobs: Job[ const pipelineIid = await state.getPipelineIid(cwd, stateDir); parser = await Parser.create(argv, writeStreams, pipelineIid, jobs); Commander.runList(parser, writeStreams, argv.listAll); + + if (argv.validateDependencyChain) { + const isValid = Commander.validateDependencyChain(parser, writeStreams, argv) + if (!isValid) { + const variableStrings = Object.entries(argv.variable) + .map(([key, value]) => `${key}=${value}`) + .join(" "); + throw new assert.AssertionError({ message: `Dependency chain validation will fail with event: ${variableStrings}` }); + } + } } else if (argv.listJson) { const pipelineIid = await state.getPipelineIid(cwd, stateDir); parser = await Parser.create(argv, writeStreams, pipelineIid, jobs); diff --git a/src/index.ts b/src/index.ts index cb8ef1af5..4ca98478d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -95,6 +95,11 @@ process.on("SIGUSR2", async () => await cleanupJobResources(jobs)); description: "List job information in csv format, when:never included", requiresArg: false, }) + .option("validate-dependency-chain", { + type: "boolean", + description: "Validate that all local jobs referenced in 'needs' are defined in the current pipeline (excludes external project/pipeline dependencies)", + requiresArg: false, + }) .option("preview", { type: "boolean", description: "Print YML with defaults, includes, extends and reference's expanded", From 39a43395d8d5b12f9359c2f57a8d61853d96314f Mon Sep 17 00:00:00 2001 From: "tushar.gupta" Date: Tue, 7 Oct 2025 21:21:32 +0530 Subject: [PATCH 02/12] feat: add --validate-dependency-chain flag to verify all local job needs exist --- src/commander.ts | 15 +++--- src/handler.ts | 6 +-- .../validate-dependency-chain/.gitlab-ci.yml | 33 ++++++++++++ .../integration.test.ts | 52 +++++++++++++++++++ 4 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 tests/test-cases/validate-dependency-chain/.gitlab-ci.yml create mode 100644 tests/test-cases/validate-dependency-chain/integration.test.ts diff --git a/src/commander.ts b/src/commander.ts index 6dec9ead5..ec1aa4114 100644 --- a/src/commander.ts +++ b/src/commander.ts @@ -272,16 +272,16 @@ export class Commander { }); } - static validateDependencyChain (parser: Parser, writeStreams: WriteStreams, argv: any): boolean { - const allJobs = [...parser.jobs.values()]; + static validateDependencyChain (parser: Parser, writeStreams: WriteStreams): boolean { + const allJobs = parser.jobs; const activeJobs = allJobs.filter(j => j.when !== "never"); const activeJobNames = new Set(activeJobs.map(job => job.name)); const missingDependencies: {job: string; missing: string}[] = []; - + for (const job of activeJobs) { if (job.needs) { const localNeeds = job.needs.filter(n => !n.project && !n.pipeline); - + for (const need of localNeeds) { if (!activeJobNames.has(need.job)) { missingDependencies.push({ @@ -292,16 +292,15 @@ export class Commander { } } } - + if (missingDependencies.length > 0) { - for (const dep of missingDependencies) { writeStreams.stderr(chalk` {yellow ${dep.job}} needs {red ${dep.missing}} which does not exist\n`); - } + } return false; } - + writeStreams.stdout(chalk`{green ✓ All job dependencies are valid}\n`); return true; } diff --git a/src/handler.ts b/src/handler.ts index 513374bde..d6b844223 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -56,12 +56,12 @@ export async function handler (args: any, writeStreams: WriteStreams, jobs: Job[ Commander.runList(parser, writeStreams, argv.listAll); if (argv.validateDependencyChain) { - const isValid = Commander.validateDependencyChain(parser, writeStreams, argv) + const isValid = Commander.validateDependencyChain(parser, writeStreams); if (!isValid) { const variableStrings = Object.entries(argv.variable) .map(([key, value]) => `${key}=${value}`) - .join(" "); - throw new assert.AssertionError({ message: `Dependency chain validation will fail with event: ${variableStrings}` }); + .join(" "); + throw new assert.AssertionError({message: `Dependency chain validation will fail with event: ${variableStrings}`}); } } } else if (argv.listJson) { diff --git a/tests/test-cases/validate-dependency-chain/.gitlab-ci.yml b/tests/test-cases/validate-dependency-chain/.gitlab-ci.yml new file mode 100644 index 000000000..5c9b8e3d0 --- /dev/null +++ b/tests/test-cases/validate-dependency-chain/.gitlab-ci.yml @@ -0,0 +1,33 @@ +--- +alpine-guest: + image: nginxinc/nginx-unprivileged:alpine3.18 + needs: ["alpine-root"] + script: + - stat -c "%a %n %u %g" one.txt + - stat -c "%a %n %u %g" script.sh + rules: + - if: '$RUN_ALL == "true"' + - if: '$RUN_SINGLE == "alpine-root"' + - if: '$RUN_SINGLE == "alpine-guest"' + +alpine-root: + needs: ["kaniko-root"] + image: nginx:alpine3.18 + rules: + - if: '$RUN_ALL == "true"' + - if: '$RUN_SINGLE == "alpine-root"' + - if: '$RUN_SINGLE == "alpine-guest"' + when: never + script: + - stat -c "%a %n %u %g" one.txt + - stat -c "%a %n %u %g" script.sh + +kaniko-root: + image: + name: gcr.io/kaniko-project/executor:v1.23.0-debug + entrypoint: [""] + rules: + - if: '$RUN_ALL == "true"' + script: + - stat -c "%a %n %u %g" one.txt + - stat -c "%a %n %u %g" script.sh diff --git a/tests/test-cases/validate-dependency-chain/integration.test.ts b/tests/test-cases/validate-dependency-chain/integration.test.ts new file mode 100644 index 000000000..ec946ad48 --- /dev/null +++ b/tests/test-cases/validate-dependency-chain/integration.test.ts @@ -0,0 +1,52 @@ +import {WriteStreamsMock} from "../../../src/write-streams.js"; +import {handler} from "../../../src/handler.js"; +import chalk from "chalk"; +import {initSpawnSpy} from "../../mocks/utils.mock.js"; +import {WhenStatics} from "../../mocks/when-statics.js"; + +beforeAll(() => { + initSpawnSpy(WhenStatics.all); +}); + +describe("validate-dependency-chain", () => { + test("should pass when all dependencies are valid", async () => { + const writeStreams = new WriteStreamsMock(); + await handler({ + cwd: "tests/test-cases/validate-dependency-chain", + list: true, + validateDependencyChain: true, + variable: ["RUN_ALL=true"], + }, writeStreams); + + const output = writeStreams.stdoutLines.join("\n"); + expect(output).toContain(chalk`{green ✓ All job dependencies are valid}`); + + // Check that there are no validation errors in stderr (only info messages) + const validationErrors = writeStreams.stderrLines.filter(line => + line.includes("Dependency chain validation will fail with event"), + ); + expect(validationErrors.length).toBe(0); + }); + + test("should fail when dependency chain is broken due to a non-existent job", async () => { + const writeStreams = new WriteStreamsMock(); + + await expect(handler({ + cwd: "tests/test-cases/validate-dependency-chain", + list: true, + validateDependencyChain: true, + variable: ["RUN_SINGLE=alpine-root"], + }, writeStreams)).rejects.toThrow("Dependency chain validation will fail with event: RUN_SINGLE=alpine-root"); + }); + + test("should fail when dependency chain is broken due to a job that never runs", async () => { + const writeStreams = new WriteStreamsMock(); + + await expect(handler({ + cwd: "tests/test-cases/validate-dependency-chain", + list: true, + validateDependencyChain: true, + variable: ["RUN_SINGLE=alpine-guest"], + }, writeStreams)).rejects.toThrow("Dependency chain validation will fail with event: RUN_SINGLE=alpine-guest"); + }); +}); \ No newline at end of file From b5bf591548251b3761c86a22be82aaf45283e4f9 Mon Sep 17 00:00:00 2001 From: "tushar.gupta" Date: Tue, 7 Oct 2025 23:02:56 +0530 Subject: [PATCH 03/12] feat: add --validate-dependency-chain flag to verify all local job needs exist --- src/commander.ts | 11 +++++ .../validate-dependency-chain/.gitlab-ci.yml | 48 +++++++++++++++++++ .../integration.test.ts | 11 +++++ 3 files changed, 70 insertions(+) diff --git a/src/commander.ts b/src/commander.ts index ec1aa4114..776cac372 100644 --- a/src/commander.ts +++ b/src/commander.ts @@ -291,6 +291,17 @@ export class Commander { } } } + + if (job.dependencies) { + for (const dependency of job.dependencies) { + if (!activeJobNames.has(dependency)) { + missingDependencies.push({ + job: job.name, + missing: dependency, + }); + } + } + } } if (missingDependencies.length > 0) { diff --git a/tests/test-cases/validate-dependency-chain/.gitlab-ci.yml b/tests/test-cases/validate-dependency-chain/.gitlab-ci.yml index 5c9b8e3d0..4b141a5ad 100644 --- a/tests/test-cases/validate-dependency-chain/.gitlab-ci.yml +++ b/tests/test-cases/validate-dependency-chain/.gitlab-ci.yml @@ -1,4 +1,8 @@ --- +stages: + - build + - test + alpine-guest: image: nginxinc/nginx-unprivileged:alpine3.18 needs: ["alpine-root"] @@ -31,3 +35,47 @@ kaniko-root: script: - stat -c "%a %n %u %g" one.txt - stat -c "%a %n %u %g" script.sh + + +kaniko-guest: + image: + name: gcr.io/kaniko-project/executor:v1.23.0-debug + entrypoint: [""] + rules: + - if: '$RUN_ALL == "true"' + script: + - stat -c "%a %n %u %g" one.txt + - stat -c "%a %n %u %g" script.sh + + +build-job-1: + stage: build + script: echo "build 1" + artifacts: + paths: [build1.txt] + rules: + - if: '$RUN_ALL == "true"' + - if: '$TEST_DEPENDENCIES == "true"' + + +test-job: + stage: test + dependencies: [build-job-1] + rules: + - if: '$RUN_ALL == "true"' + script: echo "testing" + +build-job-2: + stage: build + script: echo "build 2" + artifacts: + paths: [build2.txt] + rules: + - if: '$RUN_ALL == "true"' + +broken-dependencies-job: + stage: test + dependencies: [build-job-1, build-job-2] + rules: + - if: '$TEST_DEPENDENCIES == "true"' + script: echo "This has broken dependencies" \ No newline at end of file diff --git a/tests/test-cases/validate-dependency-chain/integration.test.ts b/tests/test-cases/validate-dependency-chain/integration.test.ts index ec946ad48..fcf4fa580 100644 --- a/tests/test-cases/validate-dependency-chain/integration.test.ts +++ b/tests/test-cases/validate-dependency-chain/integration.test.ts @@ -49,4 +49,15 @@ describe("validate-dependency-chain", () => { variable: ["RUN_SINGLE=alpine-guest"], }, writeStreams)).rejects.toThrow("Dependency chain validation will fail with event: RUN_SINGLE=alpine-guest"); }); + + test("should fail when dependencies keyword references missing artifact jobs", async () => { + const writeStreams = new WriteStreamsMock(); + + await expect(handler({ + cwd: "tests/test-cases/validate-dependency-chain", + list: true, + validateDependencyChain: true, + variable: ["TEST_DEPENDENCIES=true"], + }, writeStreams)).rejects.toThrow("Dependency chain validation will fail with event: TEST_DEPENDENCIES=true"); + }); }); \ No newline at end of file From d82b6fb33e7ea9bb2a11f80f4b176ed8600b43dd Mon Sep 17 00:00:00 2001 From: "tushar.gupta" Date: Wed, 8 Oct 2025 18:25:24 +0530 Subject: [PATCH 04/12] feat: add --validate-dependency-chain flag to verify all local job needs exist --- src/commander.ts | 45 +++++---------------------------------------- src/executor.ts | 22 +++++++++++++++++++--- src/handler.ts | 8 +------- src/index.ts | 2 +- 4 files changed, 26 insertions(+), 51 deletions(-) diff --git a/src/commander.ts b/src/commander.ts index 776cac372..7bdbe357b 100644 --- a/src/commander.ts +++ b/src/commander.ts @@ -272,47 +272,12 @@ export class Commander { }); } - static validateDependencyChain (parser: Parser, writeStreams: WriteStreams): boolean { + static validateDependencyChain (parser: Parser) { const allJobs = parser.jobs; + // This is only the jobs that will actually run const activeJobs = allJobs.filter(j => j.when !== "never"); - const activeJobNames = new Set(activeJobs.map(job => job.name)); - const missingDependencies: {job: string; missing: string}[] = []; - - for (const job of activeJobs) { - if (job.needs) { - const localNeeds = job.needs.filter(n => !n.project && !n.pipeline); - - for (const need of localNeeds) { - if (!activeJobNames.has(need.job)) { - missingDependencies.push({ - job: job.name, - missing: need.job, - }); - } - } - } - - if (job.dependencies) { - for (const dependency of job.dependencies) { - if (!activeJobNames.has(dependency)) { - missingDependencies.push({ - job: job.name, - missing: dependency, - }); - } - } - } - } - - if (missingDependencies.length > 0) { - - for (const dep of missingDependencies) { - writeStreams.stderr(chalk` {yellow ${dep.job}} needs {red ${dep.missing}} which does not exist\n`); - } - return false; - } - - writeStreams.stdout(chalk`{green ✓ All job dependencies are valid}\n`); - return true; + const stages = parser.stages; + // This will throw an assertion errror if the dependency chain is broken on specific events without having to run the full pipeline + Executor.getStartCandidates(allJobs, stages, activeJobs, []); } } diff --git a/src/executor.ts b/src/executor.ts index 2131428ae..24ed70c07 100644 --- a/src/executor.ts +++ b/src/executor.ts @@ -4,6 +4,7 @@ import assert, {AssertionError} from "assert"; import {Argv} from "./argv.js"; import pMap from "p-map"; + export class Executor { static async runLoop (argv: Argv, jobs: ReadonlyArray, stages: readonly string[], potentialStarters: Job[]) { @@ -69,7 +70,9 @@ export class Executor { while (waitForLoopArray.length > 0) { const loopJob = waitForLoopArray.pop(); assert(loopJob != null, "Job not found in getPastToWaitFor, should be impossible!"); - if (loopJob.needs) { + console.log(`Checking dependencies for job: ${loopJob.name}`); + if (loopJob.needs || loopJob.dependencies) { + console.log(`Checking dependencies for job: ${loopJob.name}`); const neededToWaitFor = this.getNeededToWaitFor(jobs, manuals, loopJob); waitForLoopArray.push(...neededToWaitFor); } else { @@ -85,8 +88,21 @@ export class Executor { static getNeededToWaitFor (jobs: ReadonlyArray, manuals: string[], job: Job) { const toWaitFor = []; - assert(job.needs != null, chalk`${job.name}.needs cannot be null in getNeededToWaitFor`); - for (const need of job.needs) { + const allDependentJobs: Array<{job: string; optional?: boolean}> = []; + + if (job.needs) { + allDependentJobs.push(...job.needs); + } + + if (job.dependencies) { + for (const dependency of job.dependencies) { + // optional dependencies are not a thing in GitLab CI, so we mark them all as non-optional + allDependentJobs.push({job: dependency, optional: false}); + } + } + + assert(allDependentJobs.length > 0, chalk`${job.name} has no needs or dependencies in getNeededToWaitFor`); + for (const need of allDependentJobs) { const baseJobs = jobs.filter(j => j.baseName === need.job); for (const j of baseJobs) { if (j.when === "never" && !need.optional) { diff --git a/src/handler.ts b/src/handler.ts index d6b844223..f4aabf2ec 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -56,13 +56,7 @@ export async function handler (args: any, writeStreams: WriteStreams, jobs: Job[ Commander.runList(parser, writeStreams, argv.listAll); if (argv.validateDependencyChain) { - const isValid = Commander.validateDependencyChain(parser, writeStreams); - if (!isValid) { - const variableStrings = Object.entries(argv.variable) - .map(([key, value]) => `${key}=${value}`) - .join(" "); - throw new assert.AssertionError({message: `Dependency chain validation will fail with event: ${variableStrings}`}); - } + Commander.validateDependencyChain(parser, writeStreams); } } else if (argv.listJson) { const pipelineIid = await state.getPipelineIid(cwd, stateDir); diff --git a/src/index.ts b/src/index.ts index 4ca98478d..a29eda086 100644 --- a/src/index.ts +++ b/src/index.ts @@ -97,7 +97,7 @@ process.on("SIGUSR2", async () => await cleanupJobResources(jobs)); }) .option("validate-dependency-chain", { type: "boolean", - description: "Validate that all local jobs referenced in 'needs' are defined in the current pipeline (excludes external project/pipeline dependencies)", + description: "Validate that jobs needed or dependent by active jobs are also active without actually running the jobs", requiresArg: false, }) .option("preview", { From 13467a2f8df679397809357f6ea1364163a54852 Mon Sep 17 00:00:00 2001 From: "tushar.gupta" Date: Wed, 8 Oct 2025 18:38:26 +0530 Subject: [PATCH 05/12] feat: add --validate-dependency-chain flag to verify all local job needs exist --- src/executor.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/executor.ts b/src/executor.ts index 24ed70c07..4455f2b6b 100644 --- a/src/executor.ts +++ b/src/executor.ts @@ -72,7 +72,6 @@ export class Executor { assert(loopJob != null, "Job not found in getPastToWaitFor, should be impossible!"); console.log(`Checking dependencies for job: ${loopJob.name}`); if (loopJob.needs || loopJob.dependencies) { - console.log(`Checking dependencies for job: ${loopJob.name}`); const neededToWaitFor = this.getNeededToWaitFor(jobs, manuals, loopJob); waitForLoopArray.push(...neededToWaitFor); } else { From b276bf97e2ce47e6223f68b5062764d0c6588444 Mon Sep 17 00:00:00 2001 From: "tushar.gupta" Date: Wed, 8 Oct 2025 20:57:39 +0530 Subject: [PATCH 06/12] feat: add --validate-dependency-chain flag to verify all local job needs exist --- src/commander.ts | 14 +++++++++++++- src/executor.ts | 22 ++++------------------ src/handler.ts | 2 +- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/commander.ts b/src/commander.ts index 7bdbe357b..691a630bb 100644 --- a/src/commander.ts +++ b/src/commander.ts @@ -277,7 +277,19 @@ export class Commander { // This is only the jobs that will actually run const activeJobs = allJobs.filter(j => j.when !== "never"); const stages = parser.stages; - // This will throw an assertion errror if the dependency chain is broken on specific events without having to run the full pipeline + // This will throw an assertion errror if the dependency chain is broke due to needs keyword on specific events without having to run the full pipeline Executor.getStartCandidates(allJobs, stages, activeJobs, []); + + const activeJobNames = new Set(activeJobs.map(job => job.name)); + // this willl throw an assertion error if the dependency chain is broken due to dependencies keyword (a job depending on artifacts from a job that will never run) + for (const job of activeJobs) { + if (job.dependencies) { + for (const dependency of job.dependencies) { + if (!activeJobNames.has(dependency)) { + throw new AssertionError({message: chalk`{blueBright ${dependency}} is when:never, but its depended on by {blueBright ${job.name}}`}); + } + } + } + } } } diff --git a/src/executor.ts b/src/executor.ts index 4455f2b6b..db24258e8 100644 --- a/src/executor.ts +++ b/src/executor.ts @@ -4,7 +4,6 @@ import assert, {AssertionError} from "assert"; import {Argv} from "./argv.js"; import pMap from "p-map"; - export class Executor { static async runLoop (argv: Argv, jobs: ReadonlyArray, stages: readonly string[], potentialStarters: Job[]) { @@ -70,8 +69,8 @@ export class Executor { while (waitForLoopArray.length > 0) { const loopJob = waitForLoopArray.pop(); assert(loopJob != null, "Job not found in getPastToWaitFor, should be impossible!"); - console.log(`Checking dependencies for job: ${loopJob.name}`); - if (loopJob.needs || loopJob.dependencies) { + console.log(chalk`Getting jobs to wait for for {blueBright ${loopJob.name}}`); + if (loopJob.needs) { const neededToWaitFor = this.getNeededToWaitFor(jobs, manuals, loopJob); waitForLoopArray.push(...neededToWaitFor); } else { @@ -87,21 +86,8 @@ export class Executor { static getNeededToWaitFor (jobs: ReadonlyArray, manuals: string[], job: Job) { const toWaitFor = []; - const allDependentJobs: Array<{job: string; optional?: boolean}> = []; - - if (job.needs) { - allDependentJobs.push(...job.needs); - } - - if (job.dependencies) { - for (const dependency of job.dependencies) { - // optional dependencies are not a thing in GitLab CI, so we mark them all as non-optional - allDependentJobs.push({job: dependency, optional: false}); - } - } - - assert(allDependentJobs.length > 0, chalk`${job.name} has no needs or dependencies in getNeededToWaitFor`); - for (const need of allDependentJobs) { + assert(job.needs != null, chalk`${job.name}.needs cannot be null in getNeededToWaitFor`); + for (const need of job.needs) { const baseJobs = jobs.filter(j => j.baseName === need.job); for (const j of baseJobs) { if (j.when === "never" && !need.optional) { diff --git a/src/handler.ts b/src/handler.ts index f4aabf2ec..f14ed6602 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -56,7 +56,7 @@ export async function handler (args: any, writeStreams: WriteStreams, jobs: Job[ Commander.runList(parser, writeStreams, argv.listAll); if (argv.validateDependencyChain) { - Commander.validateDependencyChain(parser, writeStreams); + Commander.validateDependencyChain(parser); } } else if (argv.listJson) { const pipelineIid = await state.getPipelineIid(cwd, stateDir); From 3e0a7e10e4f04419bc6f9aad6a1f211ad06d4eb7 Mon Sep 17 00:00:00 2001 From: "tushar.gupta" Date: Wed, 8 Oct 2025 20:58:27 +0530 Subject: [PATCH 07/12] feat: add --validate-dependency-chain flag to verify all local job needs exist --- src/commander.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commander.ts b/src/commander.ts index 691a630bb..d0c347204 100644 --- a/src/commander.ts +++ b/src/commander.ts @@ -281,7 +281,7 @@ export class Commander { Executor.getStartCandidates(allJobs, stages, activeJobs, []); const activeJobNames = new Set(activeJobs.map(job => job.name)); - // this willl throw an assertion error if the dependency chain is broken due to dependencies keyword (a job depending on artifacts from a job that will never run) + // This willl throw an assertion error if the dependency chain is broken due to dependencies keyword (a job depending on artifacts from a job that will never run) for (const job of activeJobs) { if (job.dependencies) { for (const dependency of job.dependencies) { From ee5c5c06e8711698c94f63a05b407a6cc935819c Mon Sep 17 00:00:00 2001 From: "tushar.gupta" Date: Wed, 8 Oct 2025 21:00:29 +0530 Subject: [PATCH 08/12] feat: add --validate-dependency-chain flag to verify all local job needs exist --- src/commander.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commander.ts b/src/commander.ts index d0c347204..ac49c54bf 100644 --- a/src/commander.ts +++ b/src/commander.ts @@ -277,11 +277,11 @@ export class Commander { // This is only the jobs that will actually run const activeJobs = allJobs.filter(j => j.when !== "never"); const stages = parser.stages; - // This will throw an assertion errror if the dependency chain is broke due to needs keyword on specific events without having to run the full pipeline + // This will throw an assertion errror if the dependency chain is broken due to needs keyword on specific events without having to run the full pipeline Executor.getStartCandidates(allJobs, stages, activeJobs, []); const activeJobNames = new Set(activeJobs.map(job => job.name)); - // This willl throw an assertion error if the dependency chain is broken due to dependencies keyword (a job depending on artifacts from a job that will never run) + // This willl throw an assertion error if the dependency chain is broken due to dependencies keyword (a job depending on artifacts from a job that will never run) without having to run the full pipeline for (const job of activeJobs) { if (job.dependencies) { for (const dependency of job.dependencies) { From 28a04ef10b46dfde62993d3ddb005f3536727988 Mon Sep 17 00:00:00 2001 From: "tushar.gupta" Date: Wed, 8 Oct 2025 21:02:19 +0530 Subject: [PATCH 09/12] feat: add --validate-dependency-chain flag to verify all local job needs exist --- src/executor.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/executor.ts b/src/executor.ts index db24258e8..2131428ae 100644 --- a/src/executor.ts +++ b/src/executor.ts @@ -69,7 +69,6 @@ export class Executor { while (waitForLoopArray.length > 0) { const loopJob = waitForLoopArray.pop(); assert(loopJob != null, "Job not found in getPastToWaitFor, should be impossible!"); - console.log(chalk`Getting jobs to wait for for {blueBright ${loopJob.name}}`); if (loopJob.needs) { const neededToWaitFor = this.getNeededToWaitFor(jobs, manuals, loopJob); waitForLoopArray.push(...neededToWaitFor); From cd60d429144202b1521b793b76e43ea22f5d7ef6 Mon Sep 17 00:00:00 2001 From: "tushar.gupta" Date: Wed, 8 Oct 2025 23:24:34 +0530 Subject: [PATCH 10/12] feat: add --validate-dependency-chain flag to verify all local job needs exist --- src/handler.ts | 1 + .../validate-dependency-chain/.gitlab-ci.yml | 116 +++++++++--------- .../integration.test.ts | 6 +- 3 files changed, 62 insertions(+), 61 deletions(-) diff --git a/src/handler.ts b/src/handler.ts index f14ed6602..d57b048c9 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -57,6 +57,7 @@ export async function handler (args: any, writeStreams: WriteStreams, jobs: Job[ if (argv.validateDependencyChain) { Commander.validateDependencyChain(parser); + writeStreams.stdout(chalk`{green ✓ All job dependencies are valid}\n`); } } else if (argv.listJson) { const pipelineIid = await state.getPipelineIid(cwd, stateDir); diff --git a/tests/test-cases/validate-dependency-chain/.gitlab-ci.yml b/tests/test-cases/validate-dependency-chain/.gitlab-ci.yml index 4b141a5ad..22e9857c2 100644 --- a/tests/test-cases/validate-dependency-chain/.gitlab-ci.yml +++ b/tests/test-cases/validate-dependency-chain/.gitlab-ci.yml @@ -4,78 +4,78 @@ stages: - test alpine-guest: - image: nginxinc/nginx-unprivileged:alpine3.18 - needs: ["alpine-root"] - script: - - stat -c "%a %n %u %g" one.txt - - stat -c "%a %n %u %g" script.sh - rules: - - if: '$RUN_ALL == "true"' - - if: '$RUN_SINGLE == "alpine-root"' - - if: '$RUN_SINGLE == "alpine-guest"' + image: nginxinc/nginx-unprivileged:alpine3.18 + needs: ["alpine-root"] + script: + - stat -c "%a %n %u %g" one.txt + - stat -c "%a %n %u %g" script.sh + rules: + - if: '$RUN_ALL == "true"' + - if: '$RUN_SINGLE == "alpine-root"' + - if: '$RUN_SINGLE == "alpine-guest"' alpine-root: - needs: ["kaniko-root"] - image: nginx:alpine3.18 - rules: - - if: '$RUN_ALL == "true"' - - if: '$RUN_SINGLE == "alpine-root"' - - if: '$RUN_SINGLE == "alpine-guest"' - when: never - script: - - stat -c "%a %n %u %g" one.txt - - stat -c "%a %n %u %g" script.sh + needs: ["kaniko-root"] + image: nginx:alpine3.18 + rules: + - if: '$RUN_ALL == "true"' + - if: '$RUN_SINGLE == "alpine-root"' + - if: '$RUN_SINGLE == "alpine-guest"' + when: never + script: + - stat -c "%a %n %u %g" one.txt + - stat -c "%a %n %u %g" script.sh kaniko-root: - image: - name: gcr.io/kaniko-project/executor:v1.23.0-debug - entrypoint: [""] - rules: - - if: '$RUN_ALL == "true"' - script: - - stat -c "%a %n %u %g" one.txt - - stat -c "%a %n %u %g" script.sh + image: + name: gcr.io/kaniko-project/executor:v1.23.0-debug + entrypoint: [""] + rules: + - if: '$RUN_ALL == "true"' + script: + - stat -c "%a %n %u %g" one.txt + - stat -c "%a %n %u %g" script.sh kaniko-guest: - image: - name: gcr.io/kaniko-project/executor:v1.23.0-debug - entrypoint: [""] - rules: - - if: '$RUN_ALL == "true"' - script: - - stat -c "%a %n %u %g" one.txt - - stat -c "%a %n %u %g" script.sh + image: + name: gcr.io/kaniko-project/executor:v1.23.0-debug + entrypoint: [""] + rules: + - if: '$RUN_ALL == "true"' + script: + - stat -c "%a %n %u %g" one.txt + - stat -c "%a %n %u %g" script.sh build-job-1: - stage: build - script: echo "build 1" - artifacts: - paths: [build1.txt] - rules: - - if: '$RUN_ALL == "true"' - - if: '$TEST_DEPENDENCIES == "true"' + stage: build + script: echo "build 1" + artifacts: + paths: [build1.txt] + rules: + - if: '$RUN_ALL == "true"' + - if: '$TEST_DEPENDENCIES == "true"' test-job: - stage: test - dependencies: [build-job-1] - rules: - - if: '$RUN_ALL == "true"' - script: echo "testing" + stage: test + dependencies: [build-job-1] + rules: + - if: '$RUN_ALL == "true"' + script: echo "testing" build-job-2: - stage: build - script: echo "build 2" - artifacts: - paths: [build2.txt] - rules: - - if: '$RUN_ALL == "true"' + stage: build + script: echo "build 2" + artifacts: + paths: [build2.txt] + rules: + - if: '$RUN_ALL == "true"' broken-dependencies-job: - stage: test - dependencies: [build-job-1, build-job-2] - rules: - - if: '$TEST_DEPENDENCIES == "true"' - script: echo "This has broken dependencies" \ No newline at end of file + stage: test + dependencies: [build-job-1, build-job-2] + rules: + - if: '$TEST_DEPENDENCIES == "true"' + script: echo "This has broken dependencies" diff --git a/tests/test-cases/validate-dependency-chain/integration.test.ts b/tests/test-cases/validate-dependency-chain/integration.test.ts index fcf4fa580..e3dda8554 100644 --- a/tests/test-cases/validate-dependency-chain/integration.test.ts +++ b/tests/test-cases/validate-dependency-chain/integration.test.ts @@ -36,7 +36,7 @@ describe("validate-dependency-chain", () => { list: true, validateDependencyChain: true, variable: ["RUN_SINGLE=alpine-root"], - }, writeStreams)).rejects.toThrow("Dependency chain validation will fail with event: RUN_SINGLE=alpine-root"); + }, writeStreams)).rejects.toThrow(chalk`{blueBright kaniko-root} is when:never, but its needed by {blueBright alpine-root}`); }); test("should fail when dependency chain is broken due to a job that never runs", async () => { @@ -47,7 +47,7 @@ describe("validate-dependency-chain", () => { list: true, validateDependencyChain: true, variable: ["RUN_SINGLE=alpine-guest"], - }, writeStreams)).rejects.toThrow("Dependency chain validation will fail with event: RUN_SINGLE=alpine-guest"); + }, writeStreams)).rejects.toThrow(chalk`{blueBright alpine-root} is when:never, but its needed by {blueBright alpine-guest}`); }); test("should fail when dependencies keyword references missing artifact jobs", async () => { @@ -58,6 +58,6 @@ describe("validate-dependency-chain", () => { list: true, validateDependencyChain: true, variable: ["TEST_DEPENDENCIES=true"], - }, writeStreams)).rejects.toThrow("Dependency chain validation will fail with event: TEST_DEPENDENCIES=true"); + }, writeStreams)).rejects.toThrow(chalk`{blueBright build-job-2} is when:never, but its depended on by {blueBright broken-dependencies-job}`); }); }); \ No newline at end of file From f9c74f4d9d6fba7dfe3c9d564a506d4ba52cdc92 Mon Sep 17 00:00:00 2001 From: "tushar.gupta" Date: Fri, 10 Oct 2025 19:49:53 +0530 Subject: [PATCH 11/12] feat: add --validate-dependency-chain flag to verify all local job needs exist --- src/handler.ts | 10 +++++----- src/index.ts | 2 +- .../validate-dependency-chain/integration.test.ts | 4 ---- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/handler.ts b/src/handler.ts index d57b048c9..0b9621a29 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -54,11 +54,11 @@ export async function handler (args: any, writeStreams: WriteStreams, jobs: Job[ const pipelineIid = await state.getPipelineIid(cwd, stateDir); parser = await Parser.create(argv, writeStreams, pipelineIid, jobs); Commander.runList(parser, writeStreams, argv.listAll); - - if (argv.validateDependencyChain) { - Commander.validateDependencyChain(parser); - writeStreams.stdout(chalk`{green ✓ All job dependencies are valid}\n`); - } + } else if (argv.validateDependencyChain) { + const pipelineIid = await state.getPipelineIid(cwd, stateDir); + parser = await Parser.create(argv, writeStreams, pipelineIid, jobs); + Commander.validateDependencyChain(parser); + writeStreams.stdout(chalk`{green ✓ All job dependencies are valid}\n`); } else if (argv.listJson) { const pipelineIid = await state.getPipelineIid(cwd, stateDir); parser = await Parser.create(argv, writeStreams, pipelineIid, jobs); diff --git a/src/index.ts b/src/index.ts index a29eda086..65615ebe3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -97,7 +97,7 @@ process.on("SIGUSR2", async () => await cleanupJobResources(jobs)); }) .option("validate-dependency-chain", { type: "boolean", - description: "Validate that jobs needed or dependent by active jobs are also active without actually running the jobs", + description: "Validate that jobs needed or dependent by active jobs under specified conditions are also active without actually running the jobs. If validation fails, use --list flag to see which jobs will run under specified conditions", requiresArg: false, }) .option("preview", { diff --git a/tests/test-cases/validate-dependency-chain/integration.test.ts b/tests/test-cases/validate-dependency-chain/integration.test.ts index e3dda8554..f25a1e47b 100644 --- a/tests/test-cases/validate-dependency-chain/integration.test.ts +++ b/tests/test-cases/validate-dependency-chain/integration.test.ts @@ -13,7 +13,6 @@ describe("validate-dependency-chain", () => { const writeStreams = new WriteStreamsMock(); await handler({ cwd: "tests/test-cases/validate-dependency-chain", - list: true, validateDependencyChain: true, variable: ["RUN_ALL=true"], }, writeStreams); @@ -33,7 +32,6 @@ describe("validate-dependency-chain", () => { await expect(handler({ cwd: "tests/test-cases/validate-dependency-chain", - list: true, validateDependencyChain: true, variable: ["RUN_SINGLE=alpine-root"], }, writeStreams)).rejects.toThrow(chalk`{blueBright kaniko-root} is when:never, but its needed by {blueBright alpine-root}`); @@ -44,7 +42,6 @@ describe("validate-dependency-chain", () => { await expect(handler({ cwd: "tests/test-cases/validate-dependency-chain", - list: true, validateDependencyChain: true, variable: ["RUN_SINGLE=alpine-guest"], }, writeStreams)).rejects.toThrow(chalk`{blueBright alpine-root} is when:never, but its needed by {blueBright alpine-guest}`); @@ -55,7 +52,6 @@ describe("validate-dependency-chain", () => { await expect(handler({ cwd: "tests/test-cases/validate-dependency-chain", - list: true, validateDependencyChain: true, variable: ["TEST_DEPENDENCIES=true"], }, writeStreams)).rejects.toThrow(chalk`{blueBright build-job-2} is when:never, but its depended on by {blueBright broken-dependencies-job}`); From a7607bd6b53df3cf8a6ef92c872246d6205aae3a Mon Sep 17 00:00:00 2001 From: "tushar.gupta" Date: Fri, 10 Oct 2025 20:52:30 +0530 Subject: [PATCH 12/12] feat: add --validate-dependency-chain flag to verify all local job needs exist --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 65615ebe3..88d99f635 100644 --- a/src/index.ts +++ b/src/index.ts @@ -97,7 +97,7 @@ process.on("SIGUSR2", async () => await cleanupJobResources(jobs)); }) .option("validate-dependency-chain", { type: "boolean", - description: "Validate that jobs needed or dependent by active jobs under specified conditions are also active without actually running the jobs. If validation fails, use --list flag to see which jobs will run under specified conditions", + description: "Validate that jobs needed or dependent by active jobs under specified conditions are also active without actually running the jobs. Uses fail-fast approach - stops at first validation error for both 'needs' and 'dependencies' keywords. If validation fails, use --list flag to see which jobs will run under specified conditions", requiresArg: false, }) .option("preview", {