Skip to content

Commit 9953a15

Browse files
committed
Implented needs tag
1 parent 4ead952 commit 9953a15

File tree

5 files changed

+140
-64
lines changed

5 files changed

+140
-64
lines changed

src/index.ts

Lines changed: 66 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -36,48 +36,73 @@ if (firstArg === "manual") {
3636
const parser = new Parser(cwd);
3737

3838
const runJobs = async () => {
39-
const stages = parser.getStages();
40-
for (const [stageName, stage] of stages) {
41-
const promises: Array<Promise<any>> = [];
42-
const jobs = stage.getJobs();
43-
44-
if (stage.getJobs().length === 0) {
45-
console.log(`=> ${c.yellow(`${stageName}`)} has no jobs`);
46-
console.log("");
47-
continue;
39+
const jobs = Array.from(parser.getJobs().values());
40+
const stages = Array.from(parser.getStages().values());
41+
42+
let stage = stages.shift();
43+
while (stage !== undefined) {
44+
const jobsInStage = stage.getJobs();
45+
const stageName = stage.name;
46+
47+
if (!stage.isRunning()) {
48+
console.log(`=> ${c.yellow(`${stageName}`)} <=`);
49+
if (jobsInStage.length === 0 && !stage.isRunning()) {
50+
console.log(`=> ${c.yellow(`${stageName}`)} has no jobs`);
51+
}
4852
}
4953

50-
const jobNames = `${jobs.map((j) => j.name).join(" ")}`;
51-
console.log(`=> ${c.yellow(`${stageName}`)} > ${c.blueBright(`${jobNames}`)} ${c.magentaBright("starting")}...`);
52-
for (const job of jobs) {
53-
if (job.isManual() && !manualArgs.includes(job.name)) {
54+
for (const job of jobsInStage) {
55+
if (job.isManual() && !manualArgs.includes(job.name) && !job.isFinished()) {
5456
console.log(`${job.getJobNameString()} skipped. when:manual`);
57+
job.setFinished(true);
5558
continue;
5659
}
5760

58-
if (job.isNever()) {
61+
if (job.isNever() && !job.isFinished()) {
5962
console.log(`${job.getJobNameString()} skipped. when:never`);
63+
job.setFinished(true);
6064
continue;
6165
}
6266

63-
const jobPromise = job.start();
64-
promises.push(jobPromise);
67+
if (!job.isRunning() && !job.isFinished()) {
68+
/* tslint:disable */
69+
// noinspection ES6MissingAwait
70+
job.start();
71+
/* tslint:enabled */
72+
}
6573
}
6674

67-
try {
68-
await Promise.all(promises);
69-
console.log("");
70-
} catch (e) {
71-
if (e !== "") {
72-
console.error(e);
75+
// Find jobs that can be started, because their needed jobs have finished
76+
for (const job of jobs) {
77+
if (job.isRunning() || job.isFinished() || job.needs.length === 0) {
78+
continue;
79+
}
80+
81+
const finishedJobNames = jobs.filter((e) => e.isFinished()).map((j) => j.name);
82+
const needsConditionMet = job.needs.every((v) => (finishedJobNames.indexOf(v) >= 0));
83+
if (needsConditionMet) {
84+
/* tslint:disable */
85+
// noinspection ES6MissingAwait
86+
job.start();
87+
/* tslint:enabled */
7388
}
74-
process.exit(1);
89+
}
90+
91+
await new Promise((r) => setTimeout(r, 50));
92+
93+
if (stage.isFinished()) {
94+
if (!stage.isSuccess()) {
95+
process.exit(2);
96+
}
97+
console.log("");
98+
stage = stages.shift();
7599
}
76100
}
101+
77102
};
78103

79104
const runExecJobs = async () => {
80-
const promises: Array<Promise<any>> = [];
105+
const jobs = [];
81106
for (let i = 1; i < argv._.length; i += 1) {
82107
const jobName = argv._[i];
83108
const job = parser.getJobs().get(argv._[i]);
@@ -86,24 +111,27 @@ const runExecJobs = async () => {
86111
process.exit(1);
87112
}
88113

89-
const jobPromise = job.start();
90-
promises.push(jobPromise);
114+
jobs.push(job);
115+
116+
/* tslint:disable */
117+
// noinspection ES6MissingAwait
118+
job.start();
119+
/* tslint:enabled */
91120
}
92121

93-
try {
94-
await Promise.all(promises);
95-
console.log("");
96-
} catch (e) {
97-
if (e !== "") {
98-
console.error(e);
99-
}
100-
process.exit(1);
122+
while (jobs.filter((j) => j.isRunning()).length > 0) {
123+
await new Promise((r) => setTimeout(r, 50));
124+
}
125+
126+
if (jobs.filter((j) => j.isSuccess()).length !== jobs.length) {
127+
process.exit(2);
101128
}
102129
};
103130

104131
process.on("uncaughtException", (err) => {
105132
// Handle the error safely
106133
console.log(err);
134+
process.exit(5);
107135
});
108136

109137
// Ensure gitlab-ci-local working directory and assets.
@@ -114,15 +142,16 @@ if (!pipelinesState) {
114142
pipelinesState = {};
115143
}
116144
pipelinesState.pipelineId = pipelinesState.pipelineId !== undefined ? Number(pipelinesState.pipelineId) : 0;
117-
console.log(firstArg);
118145
if (["pipeline", "manual"].includes(firstArg)) {
119146
pipelinesState.pipelineId += 1;
120147
}
121148
fs.writeFileSync(pipelinesStatePath, yaml.safeDump(pipelinesState), {});
122149
process.env.CI_PIPELINE_ID = pipelinesState.pipelineId;
123150

124151
if (["pipeline", "manual"].includes(firstArg)) {
125-
runJobs().catch();
152+
// noinspection JSIgnoredPromiseFromCall
153+
runJobs();
126154
} else if (firstArg === "exec") {
127-
runExecJobs().catch();
155+
// noinspection JSIgnoredPromiseFromCall
156+
runExecJobs();
128157
}

src/job.ts

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import * as c from "ansi-colors";
22
import * as deepExtend from "deep-extend";
33
import * as fs from "fs-extra";
44
import * as glob from "glob";
5-
import * as path from "path";
65
import * as prettyHrtime from "pretty-hrtime";
76
import * as shelljs from "shelljs";
87

@@ -18,16 +17,19 @@ if (process.env.EXEPATH) {
1817

1918
export class Job {
2019
public readonly name: string;
20+
public readonly needs: string[];
2121
public readonly stage: string;
2222

2323
private readonly afterScripts: string[] = [];
2424
private readonly allowFailure: boolean;
2525
private readonly beforeScripts: string[] = [];
2626
private readonly cwd: any;
27+
private finished = false;
2728
private readonly globals: any;
2829
private readonly maxJobNameLength: number;
2930
private running = false;
3031
private readonly scripts: string[] = [];
32+
private success = true;
3133
private readonly variables: { [key: string]: string };
3234
private readonly when: string;
3335

@@ -57,20 +59,36 @@ export class Job {
5759
this.stage = jobData.stage || ".pre";
5860
this.scripts = [].concat(jobData.script || []);
5961

62+
const jobNameStr = this.getJobNameString();
63+
64+
if (this.scripts.length === 0) {
65+
console.error(`${jobNameStr} ${c.red("must have script specified")}`);
66+
process.exit(1);
67+
}
68+
6069
const ciDefault = globals.default || {};
6170
this.when = jobData.when || "on_success";
6271
this.beforeScripts = [].concat(jobData.before_script || ciDefault.before_script || globals.before_script || []);
6372
this.afterScripts = [].concat(jobData.after_script || ciDefault.after_script || globals.after_script || []);
6473
this.allowFailure = jobData.allow_failure || false;
6574
this.variables = jobData.variables || {};
75+
if (this.needs && this.needs.length === 0) {
76+
console.error(`${jobNameStr} ${c.red("'needs' cannot be empty array")}`);
77+
process.exit(1);
78+
}
79+
this.needs = jobData.needs || [];
6680
}
6781

6882
public getJobNameString() {
6983
return `${c.blueBright(`${this.name.padEnd(this.maxJobNameLength + 1)}`)}`;
7084
}
7185

7286
public getOutputFilesPath() {
73-
return `${this.cwd}/.gitlab-ci-local/output/${this.getEnvs().CI_PIPELINE_ID}/${this.name}.log`;
87+
return `${this.cwd}/.gitlab-ci-local/output/${this.name}.log`;
88+
}
89+
90+
public isFinished() {
91+
return this.finished;
7492
}
7593

7694
public isManual(): boolean {
@@ -81,38 +99,50 @@ export class Job {
8199
return this.when === "never";
82100
}
83101

84-
public async start(): Promise<void> {
85-
const jobNameStr = this.getJobNameString();
102+
public isRunning() {
103+
return this.running;
104+
}
86105

87-
this.running = true;
106+
public isSuccess() {
107+
return this.success;
108+
}
88109

89-
if (this.scripts.length === 0) {
90-
console.error(`${jobNameStr} ${c.red("must have script specified")}`);
91-
process.exit(1);
92-
}
110+
public setFinished(finished: boolean) {
111+
this.finished = finished;
112+
}
113+
114+
public async start(): Promise<void> {
115+
fs.ensureFileSync(this.getOutputFilesPath());
116+
fs.truncateSync(this.getOutputFilesPath());
117+
console.log(this.getStartingString());
118+
this.running = true;
93119

94120
const startTime = process.hrtime();
95121
const prescripts = this.beforeScripts.concat(this.scripts);
96122
const prescriptsExitCode = await this.exec(prescripts.join(" && "));
97-
98123
if (this.afterScripts.length === 0 && prescriptsExitCode > 0 && !this.allowFailure) {
99-
throw this.getExitedString(prescriptsExitCode, false);
124+
console.error(this.getExitedString(startTime, prescriptsExitCode, false));
125+
this.running = false;
126+
this.finished = true;
127+
this.success = false;
128+
129+
return;
100130
}
101131

102132
if (this.afterScripts.length === 0 && prescriptsExitCode > 0 && this.allowFailure) {
103-
console.error(this.getExitedString(prescriptsExitCode, true));
104-
console.log(this.getFinishedString(startTime));
133+
console.error(this.getExitedString(startTime, prescriptsExitCode, true));
105134
this.running = false;
135+
this.finished = true;
106136

107137
return;
108138
}
109139

110140
if (prescriptsExitCode > 0 && this.allowFailure) {
111-
console.error(this.getExitedString(prescriptsExitCode, true));
141+
console.error(this.getExitedString(startTime, prescriptsExitCode, true));
112142
}
113143

114144
if (prescriptsExitCode > 0 && !this.allowFailure) {
115-
console.error(this.getExitedString(prescriptsExitCode, false));
145+
console.error(this.getExitedString(startTime, prescriptsExitCode, false));
116146
}
117147

118148
let afterScriptsCode = 0;
@@ -121,15 +151,17 @@ export class Job {
121151
}
122152

123153
if (afterScriptsCode > 0) {
124-
console.error(this.getExitedString(prescriptsExitCode, true, " (after_script)"));
154+
console.error(this.getExitedString(startTime, prescriptsExitCode, true, " (after_script)"));
125155
}
126156

127157
if (prescriptsExitCode > 0 && !this.allowFailure) {
128-
throw "";
158+
this.success = false;
129159
}
130160

131161
console.log(this.getFinishedString(startTime));
162+
132163
this.running = false;
164+
this.finished = true;
133165

134166
return;
135167
}
@@ -139,9 +171,6 @@ export class Job {
139171
}
140172

141173
private async exec(script: string): Promise<number> {
142-
fs.ensureFileSync(this.getOutputFilesPath());
143-
fs.truncateSync(this.getOutputFilesPath());
144-
145174
return new Promise<any>((resolve, reject) => {
146175
const jobNameStr = this.getJobNameString();
147176
const outputFilesPath = this.getOutputFilesPath();
@@ -176,14 +205,13 @@ export class Job {
176205
return {...this.globals.variables || {}, ...this.variables, ...process.env};
177206
}
178207

179-
private getExitedString(code: number, warning: boolean = false, prependString: string = "") {
180-
const seeLogStr = `See ${path.resolve(this.getOutputFilesPath())}`;
181-
const jobNameStr = this.getJobNameString();
208+
private getExitedString(startTime: [number, number], code: number, warning: boolean = false, prependString: string = "") {
209+
const finishedStr = this.getFinishedString(startTime);
182210
if (warning) {
183-
return `${jobNameStr} ${c.yellowBright(`warning with code ${code}`)} ${prependString} ${seeLogStr}`;
211+
return `${finishedStr} ${c.yellowBright(`warning with code ${code}`)} ${prependString}`;
184212
}
185213

186-
return `${jobNameStr} ${c.red(`exited with code ${code}`)} ${prependString} ${seeLogStr}`;
214+
return `${finishedStr} ${c.red(`exited with code ${code}`)} ${prependString}`;
187215
}
188216

189217
private getFinishedString(startTime: [number, number]) {
@@ -193,4 +221,10 @@ export class Job {
193221

194222
return `${jobNameStr} ${c.magentaBright("finished")} in ${c.magenta(`${timeStr}`)}`;
195223
}
224+
225+
private getStartingString() {
226+
const jobNameStr = this.getJobNameString();
227+
228+
return `${jobNameStr} ${c.magentaBright("starting")}...`;
229+
}
196230
}

src/stage.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,16 @@ export class Stage {
1616
public getJobs(): ReadonlyArray<Job> {
1717
return this.jobs;
1818
}
19+
20+
public isFinished(): boolean {
21+
return this.jobs.filter((j) => j.isFinished()).length === this.jobs.length;
22+
}
23+
24+
public isRunning(): boolean {
25+
return this.jobs.filter((j) => j.isRunning()).length > 0;
26+
}
27+
28+
public isSuccess(): boolean {
29+
return this.jobs.filter((j) => j.isSuccess()).length === this.jobs.length;
30+
}
1931
}

tests/test-yml-spec/.ci-include.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ new_job_from_include:
2323
stage: startup
2424
script:
2525
- echo "I get run, because i'm not specified anywhere else. This is the CI_PIPELINE_ID ${CI_PIPELINE_ID}"
26+
- sleep 4

tests/test-yml-spec/.gitlab-ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ echo_project_name:
3131
<<: *tags
3232
stage: startup
3333
script:
34-
- sleep 1
34+
- sleep 2
3535
- echo "Project Name is '${PROJECT_NAME}'"
3636

3737
project_context:
@@ -56,7 +56,7 @@ var_in_another_file:
5656
after_script_test:
5757
<<: *tags
5858
stage: last
59-
needs: [var_in_another_file]
59+
needs: [var_in_another_file, echo_project_name]
6060
script:
6161
- echo "Throwing an error"
6262
- exit 1

0 commit comments

Comments
 (0)