Skip to content

Commit 77187d8

Browse files
author
Benoit COUETIL
committed
feat: improve --list-csv and --list output
- allowFailure: show exit codes [42,137] instead of just true/false - needs: empty when not specified, [] when explicitly empty - description column: only shown when at least one job uses it - Skip 'id -u' on Windows to avoid error message Closes #1809
1 parent 17ffa0b commit 77187d8

8 files changed

Lines changed: 133 additions & 29 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@
99
/.gitlab-ci-local/
1010
.DS_Store
1111
.vscode
12+
.history
13+
/tmp/
1214
/.gitlab-ci.yml

README.md

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -201,25 +201,28 @@ deploy-job deploy never false [build-job]
201201
The command `gitlab-ci-local --list-csv` will output the pipeline jobs as csv formatted list and will also filter all
202202
jobs which are set
203203
to `when: never`.
204-
The description will always be wrapped in quotes (even if there is none) to prevent semicolons in the description
205-
disturb the csv structure.
206204

207205
```text
208-
name;description;stage;when;allow_failure;needs
209-
test-job;"Run Tests";test;on_success;false;[]
210-
build-job;"";build;on_success;true;[test-job]
206+
name;stage;when;allowFailure;needs
207+
test-job;test;on_success;false;
208+
build-job;build;on_success;true;[test-job]
209+
deploy-job;deploy;on_success;[99];
211210
```
212211

212+
- **description**: only included when at least one job uses it, always wrapped in quotes to prevent semicolons from disturbing the csv structure
213+
- **allowFailure**: `true`, `false`, or `[exit_code1,exit_code2]` when specific exit codes are allowed to fail
214+
- **needs**: empty when not specified (job follows stage ordering), `[]` when explicitly set to no dependencies (job starts immediately)
215+
213216
#### --list-csv-all
214217

215-
Same as `--list-csv-all` but will also print out jobs which are set to `when: never` (directly and implicit e.g. via
218+
Same as `--list-csv` but will also print out jobs which are set to `when: never` (directly and implicit e.g. via
216219
rules).
217220

218221
```text
219-
name;description;stage;when;allow_failure;needs
220-
test-job;"Run Tests";test;on_success;false;[]
221-
build-job;"";build;on_success;true;[test-job]
222-
deploy-job;"";deploy;never;false;[build-job]
222+
name;stage;when;allowFailure;needs
223+
test-job;test;on_success;false;
224+
build-job;build;on_success;true;[test-job]
225+
deploy-job;deploy;never;false;
223226
```
224227

225228
## Quirks

src/commander.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,23 +210,34 @@ export class Commander {
210210
let stagePadEnd = 5;
211211
stages.forEach(s => stagePadEnd = Math.max(s.length, stagePadEnd));
212212

213+
const hasDescriptions = jobs.some(j => j.description !== "");
214+
213215
let descriptionPadEnd = 11;
214-
jobs.forEach(j => descriptionPadEnd = Math.max(j.description.length, descriptionPadEnd));
216+
if (hasDescriptions) {
217+
jobs.forEach(j => descriptionPadEnd = Math.max(j.description.length, descriptionPadEnd));
218+
}
215219

216220
const jobNamePad = parser.jobNamePad;
217221

218222
if (!listAll) {
219223
jobs = jobs.filter(j => j.when !== "never");
220224
}
221225

222-
writeStreams.stdout(chalk`{grey ${"name".padEnd(jobNamePad)} ${"description".padEnd(descriptionPadEnd)}} `);
226+
if (hasDescriptions) {
227+
writeStreams.stdout(chalk`{grey ${"name".padEnd(jobNamePad)} ${"description".padEnd(descriptionPadEnd)}} `);
228+
} else {
229+
writeStreams.stdout(chalk`{grey ${"name".padEnd(jobNamePad)}} `);
230+
}
223231
writeStreams.stdout(chalk`{grey ${"stage".padEnd(stagePadEnd)} ${"when".padEnd(whenPadEnd)}} `);
224232
writeStreams.stdout(chalk`{grey allow_failure needs}\n`);
225233

226234
const renderLine = (job: Job) => {
227235
const needs = job.needs?.filter(n => !n.project && !n.pipeline).map(n => n.job);
228236
const allowFailure = job.allowFailure ? "true " : "false ";
229-
let jobLine = chalk`{blueBright ${job.name.padEnd(jobNamePad)}} ${job.description.padEnd(descriptionPadEnd)} `;
237+
let jobLine = chalk`{blueBright ${job.name.padEnd(jobNamePad)}} `;
238+
if (hasDescriptions) {
239+
jobLine += `${job.description.padEnd(descriptionPadEnd)} `;
240+
}
230241
jobLine += chalk`{yellow ${job.stage.padEnd(stagePadEnd)}} ${job.when.padEnd(whenPadEnd)} ${allowFailure.padEnd(11)}`;
231242
if (needs) {
232243
jobLine += chalk` [{blueBright ${needs}}]`;
@@ -267,10 +278,23 @@ export class Commander {
267278
jobs = jobs.filter(j => j.when !== "never");
268279
}
269280

270-
writeStreams.stdout("name;description;stage;when;allowFailure;needs\n");
281+
const hasDescriptions = jobs.some(j => j.description !== "");
282+
283+
const header = ["name", hasDescriptions ? "description" : null, "stage", "when", "allowFailure", "needs"].filter(Boolean).join(";");
284+
writeStreams.stdout(`${header}\n`);
285+
271286
jobs.forEach((job) => {
272-
const needs = job.needs?.filter(n => !n.project && !n.pipeline).map(n => n.job).join(",") ?? [];
273-
writeStreams.stdout(`${job.name};"${job.description}";${job.stage};${job.when};${job.allowFailure ? "true" : "false"};[${needs}]\n`);
287+
const needs = job.needs === null ? "" : `[${job.needs.filter(n => !n.project && !n.pipeline).map(n => n.job).join(",")}]`;
288+
let allowFailure: string;
289+
if (typeof job.allowFailure === "object") {
290+
const codes = Array.isArray(job.allowFailure.exit_codes) ? job.allowFailure.exit_codes : [job.allowFailure.exit_codes];
291+
allowFailure = `[${codes.join(",")}]`;
292+
} else {
293+
allowFailure = job.allowFailure ? "true" : "false";
294+
}
295+
296+
const row = [job.name, hasDescriptions ? `"${job.description}"` : null, job.stage, job.when, allowFailure, needs].filter(v => v !== null).join(";");
297+
writeStreams.stdout(`${row}\n`);
274298
});
275299
}
276300

src/git-data.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,19 @@ export class GitData {
172172
});
173173
promises.push(gitEmailPromise);
174174

175-
const osUidPromise = Utils.spawn(["id", "-u"], cwd).then(({stdout}) => {
176-
this.user.GITLAB_USER_ID = stdout.trimEnd();
177-
}).catch((e) => {
178-
writeStreams.stderr(chalk`{yellow Using fallback linux user id}\n`);
179-
writeStreams.stderr(chalk`{yellow ${e.message}\n}`);
180-
});
175+
const osUidPromise = (async () => {
176+
if (process.platform === "win32") {
177+
// GITLAB_USER_ID is a Unix UID concept; keep default on Windows
178+
return;
179+
}
180+
try {
181+
const {stdout} = await Utils.spawn(["id", "-u"], cwd);
182+
this.user.GITLAB_USER_ID = stdout.trimEnd();
183+
} catch (e: any) {
184+
writeStreams.stderr(chalk`{yellow Using fallback linux user id}\n`);
185+
writeStreams.stderr(chalk`{yellow ${e.message}\n}`);
186+
}
187+
})();
181188
promises.push(osUidPromise);
182189

183190
await Promise.all(promises);

tests/test-cases/list-case/integration.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,21 @@ test.concurrent("list-case --list", async () => {
2323
];
2424
expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected));
2525
});
26+
27+
test.concurrent("list-case --list no description column when unused", async () => {
28+
const writeStreams = new WriteStreamsMock();
29+
await handler({
30+
cwd: "tests/test-cases/list-csv-case/",
31+
file: ".gitlab-ci-no-description.yml",
32+
list: true,
33+
stateDir: ".gitlab-ci-local-list-case-no-description",
34+
}, writeStreams);
35+
36+
const expected = [
37+
chalk`{grey ${"name".padEnd(9)}} {grey stage when } {grey allow_failure needs}`,
38+
chalk`{blueBright ${"test-job".padEnd(9)}} {yellow test } on_success false `,
39+
chalk`{blueBright build-job} {yellow build} on_success true [{blueBright test-job}]`,
40+
];
41+
expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected));
42+
expect(writeStreams.stdoutLines.join("\n")).not.toContain("description");
43+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
stages: [test, build]
3+
4+
test-job:
5+
stage: test
6+
script:
7+
- echo test
8+
9+
build-job:
10+
stage: build
11+
needs: [test-job]
12+
allow_failure: true
13+
script:
14+
- echo build

tests/test-cases/list-csv-case/.gitlab-ci.yml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
stages: [test, build]
2+
stages: [test, build, deploy]
33

44
# @Description Run Tests
55
test-job:
@@ -14,3 +14,18 @@ build-job:
1414
allow_failure: true
1515
script:
1616
- 'echo "Build something"'
17+
18+
exit-codes-job:
19+
stage: build
20+
needs: []
21+
allow_failure:
22+
exit_codes: [42, 137]
23+
script:
24+
- 'echo "Exit codes"'
25+
26+
deploy-job:
27+
stage: deploy
28+
allow_failure:
29+
exit_codes: 1
30+
script:
31+
- 'echo "Deploy"'

tests/test-cases/list-csv-case/integration.test.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import {WriteStreamsMock} from "../../../src/write-streams.js";
2-
import {handler} from "../../../src/handler.js";
3-
import {initSpawnSpy} from "../../mocks/utils.mock.js";
4-
import {WhenStatics} from "../../mocks/when-statics.js";
1+
import { WriteStreamsMock } from "../../../src/write-streams.js";
2+
import { handler } from "../../../src/handler.js";
3+
import { initSpawnSpy } from "../../mocks/utils.mock.js";
4+
import { WhenStatics } from "../../mocks/when-statics.js";
55

66
beforeAll(() => {
77
initSpawnSpy(WhenStatics.all);
@@ -17,8 +17,10 @@ test.concurrent("list-csv-case --list-csv", async () => {
1717

1818
const expected = [
1919
"name;description;stage;when;allowFailure;needs",
20-
"test-job;\"Run Tests\";test;on_success;false;[]",
20+
"test-job;\"Run Tests\";test;on_success;false;",
2121
"build-job;\"\";build;on_success;true;[test-job]",
22+
"exit-codes-job;\"\";build;on_success;[42,137];[]",
23+
"deploy-job;\"\";deploy;on_success;[1];",
2224
];
2325
expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected));
2426
});
@@ -35,9 +37,28 @@ test.concurrent("list-csv-case --list-csv colon should add process descriptors w
3537

3638
const expected = [
3739
"name;description;stage;when;allowFailure;needs",
38-
"test-job;\"Run;Tests\";test;on_success;false;[]",
40+
"test-job;\"Run;Tests\";test;on_success;false;",
3941
"build-job;\"\";build;on_success;true;[test-job]",
4042
];
4143
expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected));
4244
});
4345

46+
47+
test.concurrent("list-csv-case --list-csv no description column when unused", async () => {
48+
const writeStreams = new WriteStreamsMock();
49+
await handler({
50+
cwd: "tests/test-cases/list-csv-case/",
51+
file: ".gitlab-ci-no-description.yml",
52+
listCsv: true,
53+
stateDir: ".gitlab-ci-local-list-csv-case-list-csv-no-description",
54+
}, writeStreams);
55+
56+
const expected = [
57+
"name;stage;when;allowFailure;needs",
58+
"test-job;test;on_success;false;",
59+
"build-job;build;on_success;true;[test-job]",
60+
];
61+
expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected));
62+
expect(writeStreams.stdoutLines.join("\n")).not.toContain("description");
63+
});
64+

0 commit comments

Comments
 (0)