feat: improve --list-csv and --list output (#1810)
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

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Benoît COUETIL
2026-04-16 09:51:07 +02:00
committed by GitHub
parent ac02922b9e
commit 09fc774f35
5 changed files with 103 additions and 33 deletions
+2
View File
@@ -9,4 +9,6 @@
/.gitlab-ci-local/
.DS_Store
.vscode
.history
/tmp/
/.gitlab-ci.yml
+32 -20
View File
@@ -180,46 +180,58 @@ The command `gitlab-ci-local --list` will return pretty output and will also fil
to `when: never`.
```text
name description stage when allow_failure needs
test-job Run Tests test on_success false
build-job build on_success true [test-job]
name description stage when allow_failure needs
test-job Run Tests test on_success false
build-job build on_success true [test-job]
exit-codes-job build on_success [42,137] []
deploy-job deploy on_success [1]
```
- **description**: always shown, empty when not set
- **allow_failure**: `true`, `false`, or `[exit_code1,exit_code2]` when specific exit codes are allowed to fail
- **needs**: omitted when not specified (job follows stage ordering), `[]` when explicitly set to no dependencies (job starts immediately)
#### --list-all
Same as `--list` but will also print out jobs which are set to `when: never` (directly and implicit e.g. via rules).
```text
name description stage when allow_failure needs
test-job Run Tests test on_success false
build-job build on_success true [test-job]
deploy-job deploy never false [build-job]
name description stage when allow_failure needs
test-job Run Tests test on_success false
build-job build on_success true [test-job]
exit-codes-job build on_success [42,137] []
deploy-job deploy on_success [1]
never-job test never false
```
#### --list-csv
The command `gitlab-ci-local --list-csv` will output the pipeline jobs as csv formatted list and will also filter all
jobs which are set
to `when: never`.
The description will always be wrapped in quotes (even if there is none) to prevent semicolons in the description
disturb the csv structure.
The command `gitlab-ci-local --list-csv` will output the pipeline jobs as a CSV-formatted list and will also filter all
jobs which are set to `when: never`.
```text
name;description;stage;when;allow_failure;needs
test-job;"Run Tests";test;on_success;false;[]
build-job;"";build;on_success;true;[test-job]
name;stage;when;allowFailure;needs
test-job;test;on_success;false;
build-job;build;on_success;true;[test-job]
exit-codes-job;build;on_success;[42,137];[]
deploy-job;deploy;on_success;[1];
```
- **allowFailure**: `true`, `false`, or `[exit_code1,exit_code2]` when specific exit codes are allowed to fail
- **needs**: empty when not specified (job follows stage ordering), `[]` when explicitly set to no dependencies (job starts immediately)
#### --list-csv-all
Same as `--list-csv-all` but will also print out jobs which are set to `when: never` (directly and implicit e.g. via
Same as `--list-csv` but will also print out jobs which are set to `when: never` (directly and implicit e.g. via
rules).
```text
name;description;stage;when;allow_failure;needs
test-job;"Run Tests";test;on_success;false;[]
build-job;"";build;on_success;true;[test-job]
deploy-job;"";deploy;never;false;[build-job]
name;stage;when;allowFailure;needs
test-job;test;on_success;false;
build-job;build;on_success;true;[test-job]
exit-codes-job;build;on_success;[42,137];[]
deploy-job;deploy;on_success;[1];
never-job;test;never;false;
```
## Quirks
+25 -6
View File
@@ -224,11 +224,11 @@ export class Commander {
writeStreams.stdout(chalk`{grey allow_failure needs}\n`);
const renderLine = (job: Job) => {
const needs = job.needs?.filter(n => !n.project && !n.pipeline).map(n => n.job);
const allowFailure = job.allowFailure ? "true " : "false ";
const needs = Commander.formatNeeds(job);
const allowFailure = Commander.formatAllowFailure(job.allowFailure);
let jobLine = chalk`{blueBright ${job.name.padEnd(jobNamePad)}} ${job.description.padEnd(descriptionPadEnd)} `;
jobLine += chalk`{yellow ${job.stage.padEnd(stagePadEnd)}} ${job.when.padEnd(whenPadEnd)} ${allowFailure.padEnd(11)}`;
if (needs) {
if (needs !== null) {
jobLine += chalk` [{blueBright ${needs}}]`;
}
writeStreams.stdout(`${jobLine}\n`);
@@ -267,10 +267,14 @@ export class Commander {
jobs = jobs.filter(j => j.when !== "never");
}
writeStreams.stdout("name;description;stage;when;allowFailure;needs\n");
writeStreams.stdout("name;stage;when;allowFailure;needs\n");
jobs.forEach((job) => {
const needs = job.needs?.filter(n => !n.project && !n.pipeline).map(n => n.job).join(",") ?? [];
writeStreams.stdout(`${job.name};"${job.description}";${job.stage};${job.when};${job.allowFailure ? "true" : "false"};[${needs}]\n`);
const needs = Commander.formatNeeds(job);
const needsStr = needs === null ? "" : `[${needs.join(",")}]`;
const allowFailure = Commander.formatAllowFailure(job.allowFailure);
const row = [job.name, job.stage, job.when, allowFailure, needsStr].join(";");
writeStreams.stdout(`${row}\n`);
});
}
@@ -294,4 +298,19 @@ export class Commander {
}
}
}
/** Returns `[code1,code2]` for exit_codes, `true`/`false` otherwise. */
private static formatAllowFailure (allowFailure: Job["allowFailure"]): string {
if (typeof allowFailure === "object") {
const codes = Array.isArray(allowFailure.exit_codes) ? allowFailure.exit_codes : [allowFailure.exit_codes];
return `[${codes.join(",")}]`;
}
return allowFailure ? "true" : "false";
}
/** Returns `[job1, job2]` when needs lists jobs, `[]` when explicitly set to no dependencies, or nothing when unset (job follows stage ordering). */
private static formatNeeds (job: Job): string[] | null {
if (job.needs === null) return null;
return job.needs.filter(n => !n.project && !n.pipeline).map(n => n.job);
}
}
+16 -1
View File
@@ -1,5 +1,5 @@
---
stages: [test, build]
stages: [test, build, deploy]
# @Description Run Tests
test-job:
@@ -14,3 +14,18 @@ build-job:
allow_failure: true
script:
- 'echo "Build something"'
exit-codes-job:
stage: build
needs: []
allow_failure:
exit_codes: [42, 137]
script:
- 'echo "Exit codes"'
deploy-job:
stage: deploy
allow_failure:
exit_codes: 1
script:
- 'echo "Deploy"'
@@ -1,5 +1,6 @@
import {WriteStreamsMock} from "../../../src/write-streams.js";
import {handler} from "../../../src/handler.js";
import chalk from "chalk-template";
import {initSpawnSpy} from "../../mocks/utils.mock.js";
import {WhenStatics} from "../../mocks/when-statics.js";
@@ -16,9 +17,11 @@ test.concurrent("list-csv-case --list-csv", async () => {
}, writeStreams);
const expected = [
"name;description;stage;when;allowFailure;needs",
"test-job;\"Run Tests\";test;on_success;false;[]",
"build-job;\"\";build;on_success;true;[test-job]",
"name;stage;when;allowFailure;needs",
"test-job;test;on_success;false;",
"build-job;build;on_success;true;[test-job]",
"exit-codes-job;build;on_success;[42,137];[]",
"deploy-job;deploy;on_success;[1];",
];
expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected));
});
@@ -34,10 +37,29 @@ test.concurrent("list-csv-case --list-csv colon should add process descriptors w
}, writeStreams);
const expected = [
"name;description;stage;when;allowFailure;needs",
"test-job;\"Run;Tests\";test;on_success;false;[]",
"build-job;\"\";build;on_success;true;[test-job]",
"name;stage;when;allowFailure;needs",
"test-job;test;on_success;false;",
"build-job;build;on_success;true;[test-job]",
];
expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected));
});
test.concurrent("list-csv-case --list", async () => {
const writeStreams = new WriteStreamsMock();
await handler({
cwd: "tests/test-cases/list-csv-case/",
list: true,
stateDir: ".gitlab-ci-local-list-csv-case-list",
}, writeStreams);
// jobNamePad=14 (exit-codes-job), descriptionPadEnd=11, stagePadEnd=6 (deploy), whenPadEnd=10
const expected = [
chalk`{grey ${"name".padEnd(14)} ${"description".padEnd(11)}} {grey ${"stage".padEnd(6)} ${"when".padEnd(10)}} {grey allow_failure needs}`,
chalk`{blueBright ${"test-job".padEnd(14)}} ${"Run Tests".padEnd(11)} {yellow ${"test".padEnd(6)}} ${"on_success".padEnd(10)} ${"false".padEnd(11)}`,
chalk`{blueBright ${"build-job".padEnd(14)}} ${"".padEnd(11)} {yellow ${"build".padEnd(6)}} ${"on_success".padEnd(10)} ${"true".padEnd(11)} [{blueBright test-job}]`,
chalk`{blueBright ${"exit-codes-job".padEnd(14)}} ${"".padEnd(11)} {yellow ${"build".padEnd(6)}} ${"on_success".padEnd(10)} ${"[42,137]".padEnd(11)} [{blueBright }]`,
chalk`{blueBright ${"deploy-job".padEnd(14)}} ${"".padEnd(11)} {yellow ${"deploy".padEnd(6)}} ${"on_success".padEnd(10)} ${"[1]".padEnd(11)}`,
];
expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected));
});