Skip to content

Commit 2cf5792

Browse files
authoredSep 30, 2024··
fix(3209): Honor freeze windows for virtual jobs (#3210)
1 parent 394e2f1 commit 2cf5792

13 files changed

+417
-92
lines changed
 

‎plugins/builds/index.js

+16-22
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const {
2828
createEvent,
2929
parseJobInfo,
3030
ensureStageTeardownBuildExists,
31-
getJobId,
31+
getJob,
3232
isOrTrigger,
3333
extractExternalJoinData,
3434
extractCurrentPipelineJoinData,
@@ -94,9 +94,9 @@ async function triggerNextJobs(config, app) {
9494

9595
const downstreamOfNextJobsToBeProcessed = [];
9696

97-
for (const [nextJobName, nextJob] of Object.entries(currentPipelineNextJobs)) {
98-
const nextJobId = nextJob.id || (await getJobId(nextJobName, currentPipeline.id, jobFactory));
99-
const { isVirtual: isNextJobVirtual, stageName: nextJobStageName } = nextJob;
97+
for (const [nextJobName, nextJobInfo] of Object.entries(currentPipelineNextJobs)) {
98+
const nextJob = await getJob(nextJobName, currentPipeline.id, jobFactory);
99+
const { isVirtual: isNextJobVirtual, stageName: nextJobStageName } = nextJobInfo;
100100
const resource = `pipeline:${currentPipeline.id}:groupEvent:${currentEvent.groupEventId}`;
101101
let lock;
102102
let nextBuild;
@@ -125,15 +125,13 @@ async function triggerNextJobs(config, app) {
125125
nextBuild = await orTrigger.execute(
126126
currentEvent,
127127
currentPipeline.id,
128-
nextJobName,
129-
nextJobId,
128+
nextJob,
130129
parentBuilds,
131130
isNextJobVirtual
132131
);
133132
} else {
134133
nextBuild = await andTrigger.execute(
135-
nextJobName,
136-
nextJobId,
134+
nextJob,
137135
parentBuilds,
138136
joinListNames,
139137
isNextJobVirtual,
@@ -145,7 +143,7 @@ async function triggerNextJobs(config, app) {
145143
downstreamOfNextJobsToBeProcessed.push({
146144
build: nextBuild,
147145
event: currentEvent,
148-
job: await nextBuild.job,
146+
job: nextJob,
149147
pipeline: currentPipeline,
150148
scmContext: config.scmContext,
151149
username: config.username
@@ -244,9 +242,9 @@ async function triggerNextJobs(config, app) {
244242

245243
// Skip trigger process if createExternalEvent fails
246244
if (externalEvent) {
247-
for (const [nextJobName, nextJob] of Object.entries(joinedPipeline.jobs)) {
248-
const nextJobId = nextJob.id || (await getJobId(nextJobName, currentPipeline.id, jobFactory));
249-
const { isVirtual: isNextJobVirtual, stageName: nextJobStageName } = nextJob;
245+
for (const [nextJobName, nextJobInfo] of Object.entries(joinedPipeline.jobs)) {
246+
const nextJob = await getJob(nextJobName, joinedPipelineId, jobFactory);
247+
const { isVirtual: isNextJobVirtual, stageName: nextJobStageName } = nextJobInfo;
250248

251249
const { parentBuilds } = parseJobInfo({
252250
joinObj: joinedPipeline.jobs,
@@ -266,23 +264,21 @@ async function triggerNextJobs(config, app) {
266264
nextBuild = await remoteTrigger.execute(
267265
externalEvent,
268266
externalEvent.pipelineId,
269-
nextJobName,
270-
nextJobId,
267+
nextJob,
271268
parentBuilds,
272269
isNextJobVirtual
273270
);
274271
} else {
275272
// Re get join list when first time remote trigger since external event was empty and cannot get workflow graph then
276273
const joinList =
277-
nextJob.join.length > 0
278-
? nextJob.join
274+
nextJobInfo.join.length > 0
275+
? nextJobInfo.join
279276
: workflowParser.getSrcForJoin(externalEvent.workflowGraph, { jobName: nextJobName });
280277
const joinListNames = joinList.map(j => j.name);
281278

282279
nextBuild = await remoteJoin.execute(
283280
externalEvent,
284-
nextJobName,
285-
nextJobId,
281+
nextJob,
286282
parentBuilds,
287283
groupEventBuilds,
288284
joinListNames,
@@ -292,13 +288,11 @@ async function triggerNextJobs(config, app) {
292288
}
293289

294290
if (isNextJobVirtual && nextBuild.status === Status.SUCCESS) {
295-
const nextJobModel = await nextBuild.job;
296-
297291
downstreamOfNextJobsToBeProcessed.push({
298292
build: nextBuild,
299293
event: currentEvent,
300-
job: nextJobModel,
301-
pipeline: await nextJobModel.pipeline,
294+
job: nextJob,
295+
pipeline: await nextJob.pipeline,
302296
scmContext: config.scmContext,
303297
username: config.username
304298
});

‎plugins/builds/triggers/and.js

+6-8
Original file line numberDiff line numberDiff line change
@@ -52,29 +52,28 @@ class AndTrigger extends JoinBase {
5252

5353
/**
5454
* Trigger the next jobs of the current job
55-
* @param {String} nextJobName
56-
* @param {String} nextJobId
55+
* @param {Job} nextJob
5756
* @param {Record<String, Object>} parentBuilds
5857
* @param {String[]} joinListNames
5958
* @param {Boolean} isNextJobVirtual
6059
* @param {String} nextJobStageName
6160
* @returns {Promise<Build>}
6261
*/
63-
async execute(nextJobName, nextJobId, parentBuilds, joinListNames, isNextJobVirtual, nextJobStageName) {
62+
async execute(nextJob, parentBuilds, joinListNames, isNextJobVirtual, nextJobStageName) {
6463
logger.info(`Fetching finished builds for event ${this.currentEvent.id}`);
6564

6665
const relatedBuilds = await this.fetchRelatedBuilds();
6766

6867
// Find the next build from the related builds for this event
69-
let nextBuild = relatedBuilds.find(b => b.jobId === nextJobId && b.eventId === this.currentEvent.id);
68+
let nextBuild = relatedBuilds.find(b => b.jobId === nextJob.id && b.eventId === this.currentEvent.id);
7069

7170
if (!nextBuild) {
7271
// If the build to join fails and it succeeds on restart, depending on the timing, the latest build will be that of a child event.
7372
// In that case, `nextBuild` will be null and will not be triggered even though there is a build that should be triggered.
7473
// Now we need to check for the existence of a build that should be triggered in its own event.
7574
nextBuild = await this.buildFactory.get({
7675
eventId: this.currentEvent.id,
77-
jobId: nextJobId
76+
jobId: nextJob.id
7877
});
7978

8079
if (nextBuild) {
@@ -84,7 +83,7 @@ class AndTrigger extends JoinBase {
8483

8584
if (!nextBuild) {
8685
// If the build to join is in the child event, its event id is greater than current event.
87-
nextBuild = relatedBuilds.find(b => b.jobId === nextJobId && b.eventId > this.currentEvent.id);
86+
nextBuild = relatedBuilds.find(b => b.jobId === nextJob.id && b.eventId > this.currentEvent.id);
8887
}
8988
const newParentBuilds = mergeParentBuilds(parentBuilds, relatedBuilds, this.currentEvent, undefined);
9089
let nextEvent = this.currentEvent;
@@ -97,8 +96,7 @@ class AndTrigger extends JoinBase {
9796
pipelineId: this.pipelineId,
9897
event: nextEvent,
9998
nextBuild,
100-
nextJobName,
101-
nextJobId,
99+
nextJob,
102100
parentBuilds: newParentBuilds,
103101
parentBuildId: this.currentBuild.id,
104102
joinListNames,

‎plugins/builds/triggers/helpers.js

+12-12
Original file line numberDiff line numberDiff line change
@@ -599,13 +599,13 @@ async function getParentBuildStatus({ newBuild, joinListNames, pipelineId, build
599599
* @param {Boolean} arg.done If the build is done or not
600600
* @param {Boolean} arg.hasFailure If the build has a failure or not
601601
* @param {Build} arg.newBuild Next build
602-
* @param {String|undefined} arg.jobName Job name
602+
* @param {Job} arg.job Next job
603603
* @param {String|undefined} arg.pipelineId Pipeline ID
604604
* @param {String|undefined} arg.stageName Stage name
605605
* @param {Boolean} arg.isVirtualJob If the job is virtual or not
606606
* @returns {Promise<Build|null>} The newly updated/created build
607607
*/
608-
async function handleNewBuild({ done, hasFailure, newBuild, jobName, pipelineId, stageName, isVirtualJob }) {
608+
async function handleNewBuild({ done, hasFailure, newBuild, job, pipelineId, stageName, isVirtualJob }) {
609609
if (!done || Status.isStarted(newBuild.status)) {
610610
return null;
611611
}
@@ -615,9 +615,9 @@ async function handleNewBuild({ done, hasFailure, newBuild, jobName, pipelineId,
615615
const stageTeardownName = stageName ? getFullStageJobName({ stageName, jobName: 'teardown' }) : '';
616616

617617
// New build is not stage teardown job
618-
if (jobName !== stageTeardownName) {
618+
if (job.name !== stageTeardownName) {
619619
logger.info(
620-
`Failure occurred in upstream job, removing new build - build:${newBuild.id} pipeline:${pipelineId}-${jobName} event:${newBuild.eventId} `
620+
`Failure occurred in upstream job, removing new build - build:${newBuild.id} pipeline:${pipelineId}-${job.name} event:${newBuild.eventId} `
621621
);
622622
await newBuild.remove();
623623
}
@@ -626,7 +626,9 @@ async function handleNewBuild({ done, hasFailure, newBuild, jobName, pipelineId,
626626
}
627627

628628
// Bypass execution of the build if the job is virtual
629-
if (isVirtualJob) {
629+
const hasFreezeWindows = job.permutations[0].freezeWindows && job.permutations[0].freezeWindows.length > 0;
630+
631+
if (isVirtualJob && !hasFreezeWindows) {
630632
newBuild.status = Status.SUCCESS;
631633

632634
return newBuild.update();
@@ -1035,19 +1037,17 @@ function extractExternalJoinData(joinedPipelines, currentPipelineId) {
10351037
}
10361038

10371039
/**
1038-
* Get job id from job name
1040+
* Get job from job name
10391041
* @param {String} jobName Job name
10401042
* @param {String} pipelineId Pipeline id
10411043
* @param {JobFactory} jobFactory Job factory
1042-
* @returns {Promise<Number>}
1044+
* @returns {Promise<Job>}
10431045
*/
1044-
async function getJobId(jobName, pipelineId, jobFactory) {
1045-
const job = await jobFactory.get({
1046+
async function getJob(jobName, pipelineId, jobFactory) {
1047+
return jobFactory.get({
10461048
name: jobName,
10471049
pipelineId
10481050
});
1049-
1050-
return job.id;
10511051
}
10521052

10531053
/**
@@ -1152,7 +1152,7 @@ module.exports = {
11521152
strToInt,
11531153
createEvent,
11541154
deleteBuild,
1155-
getJobId,
1155+
getJob,
11561156
isOrTrigger,
11571157
extractCurrentPipelineJoinData,
11581158
extractExternalJoinData,

‎plugins/builds/triggers/joinBase.js

+6-8
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ class JoinBase {
3939
* @param {Number} pipelineId
4040
* @param {Event} event
4141
* @param {Build} nextBuild
42-
* @param {String} nextJobName
43-
* @param {String} nextJobId
42+
* @param {Job} nextJob
4443
* @param {import('./helpers').ParentBuilds} parentBuilds
4544
* @param {String} parentBuildId
4645
* @param {String[]} joinListNames
@@ -52,8 +51,7 @@ class JoinBase {
5251
pipelineId,
5352
event,
5453
nextBuild,
55-
nextJobName,
56-
nextJobId,
54+
nextJob,
5755
parentBuilds,
5856
parentBuildId,
5957
joinListNames,
@@ -68,8 +66,8 @@ class JoinBase {
6866
jobFactory: this.jobFactory,
6967
buildFactory: this.buildFactory,
7068
pipelineId,
71-
jobName: nextJobName,
72-
jobId: nextJobId,
69+
jobName: nextJob.name,
70+
jobId: nextJob.id,
7371
username: this.username,
7472
scmContext: this.scmContext,
7573
event, // this is the parentBuild for the next build
@@ -87,7 +85,7 @@ class JoinBase {
8785
}
8886

8987
if (!newBuild) {
90-
logger.error(`No build found for ${pipelineId}:${nextJobName}`);
88+
logger.error(`No build found for ${pipelineId}:${nextJob.name}`);
9189

9290
return null;
9391
}
@@ -104,7 +102,7 @@ class JoinBase {
104102
done,
105103
hasFailure,
106104
newBuild,
107-
jobName: nextJobName,
105+
job: nextJob,
108106
pipelineId,
109107
isVirtualJob: isNextJobVirtual,
110108
stageName: nextJobStageName

‎plugins/builds/triggers/or.js

+3-4
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@ class OrTrigger extends OrBase {
1212
* Trigger the next jobs of the current job
1313
* @param {Event} event
1414
* @param {Number} pipelineId
15-
* @param {String} nextJobName
16-
* @param {Number} nextJobId
15+
* @param {Job} nextJob
1716
* @param {import('./helpers').ParentBuilds} parentBuilds
1817
* @param {Boolean} isNextJobVirtual
1918
* @return {Promise<Build|null>}
2019
*/
21-
async execute(event, pipelineId, nextJobName, nextJobId, parentBuilds, isNextJobVirtual) {
22-
return this.trigger(event, pipelineId, nextJobName, nextJobId, parentBuilds, isNextJobVirtual);
20+
async execute(event, pipelineId, nextJob, parentBuilds, isNextJobVirtual) {
21+
return this.trigger(event, pipelineId, nextJob, parentBuilds, isNextJobVirtual);
2322
}
2423
}
2524

‎plugins/builds/triggers/orBase.js

+11-9
Original file line numberDiff line numberDiff line change
@@ -36,25 +36,27 @@ class OrBase {
3636
* Trigger the next jobs of the current job
3737
* @param {Event} event
3838
* @param {Number} pipelineId
39-
* @param {String} nextJobName
40-
* @param {Number} nextJobId
39+
* @param {Job} nextJob
4140
* @param {import('./helpers').ParentBuilds} parentBuilds
4241
* @param {Boolean} isNextJobVirtual
4342
* @return {Promise<Build|null>}
4443
*/
45-
async trigger(event, pipelineId, nextJobName, nextJobId, parentBuilds, isNextJobVirtual) {
44+
async trigger(event, pipelineId, nextJob, parentBuilds, isNextJobVirtual) {
4645
let nextBuild = await this.buildFactory.get({
4746
eventId: event.id,
48-
jobId: nextJobId
47+
jobId: nextJob.id
4948
});
5049

50+
const hasFreezeWindows =
51+
nextJob.permutations[0].freezeWindows && nextJob.permutations[0].freezeWindows.length > 0;
52+
5153
if (nextBuild !== null) {
5254
if (Status.isStarted(nextBuild.status)) {
5355
return nextBuild;
5456
}
5557

5658
// Bypass execution of the build if the job is virtual
57-
if (isNextJobVirtual) {
59+
if (isNextJobVirtual && !hasFreezeWindows) {
5860
nextBuild.status = Status.SUCCESS;
5961

6062
return nextBuild.update();
@@ -70,19 +72,19 @@ class OrBase {
7072
jobFactory: this.jobFactory,
7173
buildFactory: this.buildFactory,
7274
pipelineId,
73-
jobName: nextJobName,
74-
jobId: nextJobId,
75+
jobName: nextJob.name,
76+
jobId: nextJob.id,
7577
username: this.username,
7678
scmContext: this.scmContext,
7779
event,
7880
baseBranch: event.baseBranch || null,
7981
parentBuilds,
8082
parentBuildId: this.currentBuild.id,
81-
start: !isNextJobVirtual
83+
start: hasFreezeWindows || !isNextJobVirtual
8284
});
8385

8486
// Bypass execution of the build if the job is virtual
85-
if (isNextJobVirtual) {
87+
if (isNextJobVirtual && !hasFreezeWindows) {
8688
nextBuild.status = Status.SUCCESS;
8789

8890
nextBuild = nextBuild.update();

‎plugins/builds/triggers/remoteJoin.js

+4-7
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ class RemoteJoin extends JoinBase {
2323
/**
2424
* Trigger the next external jobs of the current job
2525
* @param {Event} externalEvent Downstream pipeline's event
26-
* @param {String} nextJobName
27-
* @param {Number} nextJobId
26+
* @param {Job} nextJob
2827
* @param {import('./helpers').ParentBuilds} parentBuilds
2928
* @param {Build[]} groupEventBuilds Builds of the downstream pipeline, where only the latest ones for each job are included that have the same groupEventId as the externalEvent
3029
* @param {String[]} joinListNames
@@ -34,16 +33,15 @@ class RemoteJoin extends JoinBase {
3433
*/
3534
async execute(
3635
externalEvent,
37-
nextJobName,
38-
nextJobId,
36+
nextJob,
3937
parentBuilds,
4038
groupEventBuilds,
4139
joinListNames,
4240
isNextJobVirtual,
4341
nextJobStageName
4442
) {
4543
// When restart case, should we create a new build ?
46-
const nextBuild = groupEventBuilds.find(b => b.jobId === nextJobId && b.eventId === externalEvent.id);
44+
const nextBuild = groupEventBuilds.find(b => b.jobId === nextJob.id && b.eventId === externalEvent.id);
4745

4846
const newParentBuilds = mergeParentBuilds(parentBuilds, groupEventBuilds, this.currentEvent, externalEvent);
4947

@@ -58,8 +56,7 @@ class RemoteJoin extends JoinBase {
5856
pipelineId: externalEvent.pipelineId,
5957
event: externalEvent,
6058
nextBuild,
61-
nextJobName,
62-
nextJobId,
59+
nextJob,
6360
parentBuilds: newParentBuilds,
6461
parentBuildId,
6562
joinListNames,

‎plugins/builds/triggers/remoteTrigger.js

+3-4
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@ class RemoteTrigger extends OrBase {
1212
* Trigger the next jobs of the current job
1313
* @param {Event} event
1414
* @param {Number} pipelineId
15-
* @param {String} nextJobName
16-
* @param {String} nextJobId
15+
* @param {Job} nextJob
1716
* @param {import('./helpers').ParentBuilds} parentBuilds
1817
* @param {Boolean} isNextJobVirtual
1918
* @returns {Promise<Build|null>}
2019
*/
21-
async execute(event, pipelineId, nextJobName, nextJobId, parentBuilds, isNextJobVirtual) {
22-
return this.trigger(event, pipelineId, nextJobName, nextJobId, parentBuilds, isNextJobVirtual);
20+
async execute(event, pipelineId, nextJob, parentBuilds, isNextJobVirtual) {
21+
return this.trigger(event, pipelineId, nextJob, parentBuilds, isNextJobVirtual);
2322
}
2423
}
2524

‎test/plugins/builds.test.js

+203-9
Original file line numberDiff line numberDiff line change
@@ -1512,7 +1512,14 @@ describe('build plugin test', () => {
15121512
id: publishJobId,
15131513
pipelineId,
15141514
state: 'ENABLED',
1515-
parsePRJobName: sinon.stub().returns('publish')
1515+
parsePRJobName: sinon.stub().returns('publish'),
1516+
permutations: [
1517+
{
1518+
settings: {
1519+
email: 'foo@bar.com'
1520+
}
1521+
}
1522+
]
15161523
};
15171524

15181525
beforeEach(() => {
@@ -2332,12 +2339,26 @@ describe('build plugin test', () => {
23322339
id: 2,
23332340
pipelineId,
23342341
state: 'ENABLED',
2335-
parsePRJobName: sinon.stub().returns('b')
2342+
parsePRJobName: sinon.stub().returns('b'),
2343+
permutations: [
2344+
{
2345+
settings: {
2346+
email: 'foo@bar.com'
2347+
}
2348+
}
2349+
]
23362350
};
23372351
const jobC = {
23382352
...jobB,
23392353
id: 3,
2340-
parsePRJobName: sinon.stub().returns('c')
2354+
parsePRJobName: sinon.stub().returns('c'),
2355+
permutations: [
2356+
{
2357+
settings: {
2358+
email: 'foo@bar.com'
2359+
}
2360+
}
2361+
]
23412362
};
23422363
let buildMocks;
23432364
let jobBconfig;
@@ -2787,7 +2808,14 @@ describe('build plugin test', () => {
27872808
id: 2,
27882809
pipelineId,
27892810
state: 'ENABLED',
2790-
parsePRJobName: sinon.stub().returns('b')
2811+
parsePRJobName: sinon.stub().returns('b'),
2812+
permutations: [
2813+
{
2814+
settings: {
2815+
email: 'foo@bar.com'
2816+
}
2817+
}
2818+
]
27912819
};
27922820
const jobC = {
27932821
...jobB,
@@ -2804,6 +2832,10 @@ describe('build plugin test', () => {
28042832
),
28052833
parsePRJobName: sinon.stub().returns('c')
28062834
};
2835+
const jobD = {
2836+
...jobB,
2837+
id: 4
2838+
};
28072839
const externalEventBuilds = [
28082840
{
28092841
id: 555,
@@ -2887,10 +2919,12 @@ describe('build plugin test', () => {
28872919
eventFactoryMock.get.withArgs(8888).resolves(parentEventMock);
28882920
jobFactoryMock.get.withArgs(jobB.id).resolves(jobB);
28892921
jobFactoryMock.get.withArgs(jobC.id).resolves(jobC);
2922+
jobFactoryMock.get.withArgs(jobD.id).resolves(jobD);
28902923
jobFactoryMock.get.withArgs(6).resolves(jobC);
28912924
jobFactoryMock.get.withArgs(3).resolves(jobC);
28922925
jobFactoryMock.get.withArgs({ pipelineId, name: 'b' }).resolves(jobB);
28932926
jobFactoryMock.get.withArgs({ pipelineId, name: 'c' }).resolves(jobC);
2927+
jobFactoryMock.get.withArgs({ pipelineId, name: 'd' }).resolves(jobD);
28942928
jobFactoryMock.list.resolves([{ id: 5555 }]);
28952929
jobMock.name = 'a';
28962930
buildMock.eventId = '8888';
@@ -2989,6 +3023,24 @@ describe('build plugin test', () => {
29893023
});
29903024
});
29913025
it('triggers next job as external when user used external syntax for same pipeline', () => {
3026+
const pipeline2JobB = {
3027+
id: 2,
3028+
pipelineId: 123,
3029+
name: 'b',
3030+
state: 'ENABLED',
3031+
parsePRJobName: sinon.stub().returns('b'),
3032+
permutations: [
3033+
{
3034+
settings: {
3035+
email: 'foo@bar.com'
3036+
}
3037+
}
3038+
]
3039+
};
3040+
3041+
jobFactoryMock.get.withArgs({ pipelineId: '123', name: 'b' }).resolves(pipeline2JobB);
3042+
jobFactoryMock.get.withArgs(pipeline2JobB.id).resolves(pipeline2JobB);
3043+
29923044
const expectedEventArgs = {
29933045
pipelineId: '123',
29943046
startFrom: '~sd@123:a',
@@ -3038,6 +3090,24 @@ describe('build plugin test', () => {
30383090
});
30393091

30403092
it('triggers next next job when next job is external', () => {
3093+
const pipeline2JobA = {
3094+
id: 2,
3095+
pipelineId: 2,
3096+
name: 'a',
3097+
state: 'ENABLED',
3098+
parsePRJobName: sinon.stub().returns('a'),
3099+
permutations: [
3100+
{
3101+
settings: {
3102+
email: 'foo@bar.com'
3103+
}
3104+
}
3105+
]
3106+
};
3107+
3108+
jobFactoryMock.get.withArgs({ pipelineId: '2', name: 'a' }).resolves(pipeline2JobA);
3109+
jobFactoryMock.get.withArgs(pipeline2JobA.id).resolves(pipeline2JobA);
3110+
30413111
const expectedEventArgs = {
30423112
pipelineId: '2',
30433113
startFrom: '~sd@123:a',
@@ -3536,6 +3606,24 @@ describe('build plugin test', () => {
35363606
it('triggers if all jobs in external join are done and updates join job', () => {
35373607
// re-entry case
35383608
// join-job exist
3609+
const pipeline2JobC = {
3610+
id: 3,
3611+
pipelineId: 2,
3612+
name: 'c',
3613+
state: 'ENABLED',
3614+
parsePRJobName: sinon.stub().returns('c'),
3615+
permutations: [
3616+
{
3617+
settings: {
3618+
email: 'foo@bar.com'
3619+
}
3620+
}
3621+
]
3622+
};
3623+
3624+
jobFactoryMock.get.withArgs({ pipelineId: '2', name: 'c' }).resolves(pipeline2JobC);
3625+
jobFactoryMock.get.withArgs(pipeline2JobC.id).resolves(pipeline2JobC);
3626+
35393627
eventMock.workflowGraph = {
35403628
nodes: [
35413629
{ name: '~pr' },
@@ -3669,6 +3757,24 @@ describe('build plugin test', () => {
36693757
// ~sd@2:a -> a -> sd@2:c
36703758
// If user is at `a`, it should trigger `sd@2:c`
36713759
// No join-job, so create
3760+
const pipeline2JobC = {
3761+
id: 6,
3762+
pipelineId: 2,
3763+
name: 'c',
3764+
state: 'ENABLED',
3765+
parsePRJobName: sinon.stub().returns('c'),
3766+
permutations: [
3767+
{
3768+
settings: {
3769+
email: 'foo@bar.com'
3770+
}
3771+
}
3772+
]
3773+
};
3774+
3775+
jobFactoryMock.get.withArgs({ pipelineId: '2', name: 'c' }).resolves(pipeline2JobC);
3776+
jobFactoryMock.get.withArgs(pipeline2JobC.id).resolves(pipeline2JobC);
3777+
36723778
eventMock.workflowGraph = {
36733779
nodes: [
36743780
{ name: '~pr' },
@@ -3705,7 +3811,7 @@ describe('build plugin test', () => {
37053811
baseBranch: 'master',
37063812
configPipelineSha: 'abc123',
37073813
eventId: 8887,
3708-
jobId: 3,
3814+
jobId: 6,
37093815
parentBuildId: [12345],
37103816
parentBuilds: {
37113817
123: { eventId: '8888', jobs: { a: 12345 } },
@@ -3787,6 +3893,24 @@ describe('build plugin test', () => {
37873893
// ~sd@2:b ------➚
37883894
// If user is at `a`, it should trigger `sd@2:c`
37893895
// ~sd@123:a is or trigger, so create
3896+
const pipeline2JobC = {
3897+
id: 6,
3898+
pipelineId: 2,
3899+
name: 'c',
3900+
state: 'ENABLED',
3901+
parsePRJobName: sinon.stub().returns('c'),
3902+
permutations: [
3903+
{
3904+
settings: {
3905+
email: 'foo@bar.com'
3906+
}
3907+
}
3908+
]
3909+
};
3910+
3911+
jobFactoryMock.get.withArgs({ pipelineId: '2', name: 'c' }).resolves(pipeline2JobC);
3912+
jobFactoryMock.get.withArgs(pipeline2JobC.id).resolves(pipeline2JobC);
3913+
37903914
eventMock.workflowGraph = {
37913915
nodes: [
37923916
{ name: '~pr' },
@@ -3819,11 +3943,11 @@ describe('build plugin test', () => {
38193943
parentBuilds,
38203944
start: sinon.stub().resolves()
38213945
});
3822-
const jobCConfig = {
3946+
const pipeline2JobCConfig = {
38233947
baseBranch: 'master',
38243948
configPipelineSha: 'abc123',
38253949
eventId: 8887,
3826-
jobId: 3,
3950+
jobId: 6,
38273951
parentBuildId: 12345,
38283952
parentBuilds: {
38293953
123: { eventId: '8888', jobs: { a: 12345 } },
@@ -3844,7 +3968,7 @@ describe('build plugin test', () => {
38443968
pr: {},
38453969
id: 8887,
38463970
configPipelineSha: 'abc123',
3847-
pipelineId: 123,
3971+
pipelineId: 2,
38483972
baseBranch: 'master',
38493973
builds: [
38503974
{
@@ -3896,11 +4020,45 @@ describe('build plugin test', () => {
38964020
assert.notCalled(eventFactoryMock.create);
38974021
assert.calledOnce(buildFactoryMock.getLatestBuilds);
38984022
assert.calledOnce(buildFactoryMock.create);
3899-
assert.calledWith(buildFactoryMock.create, jobCConfig);
4023+
assert.calledWith(buildFactoryMock.create, pipeline2JobCConfig);
39004024
});
39014025
});
39024026

39034027
it('starts multiple builds with the existing downstream event', () => {
4028+
const pipeline2JobC = {
4029+
id: 6,
4030+
pipelineId: 2,
4031+
name: 'c',
4032+
state: 'ENABLED',
4033+
parsePRJobName: sinon.stub().returns('c'),
4034+
permutations: [
4035+
{
4036+
settings: {
4037+
email: 'foo@bar.com'
4038+
}
4039+
}
4040+
]
4041+
};
4042+
const pipeline2JobD = {
4043+
id: 7,
4044+
pipelineId: 2,
4045+
name: 'd',
4046+
state: 'ENABLED',
4047+
parsePRJobName: sinon.stub().returns('d'),
4048+
permutations: [
4049+
{
4050+
settings: {
4051+
email: 'foo@bar.com'
4052+
}
4053+
}
4054+
]
4055+
};
4056+
4057+
jobFactoryMock.get.withArgs({ pipelineId: '2', name: 'c' }).resolves(pipeline2JobC);
4058+
jobFactoryMock.get.withArgs(pipeline2JobC.id).resolves(pipeline2JobC);
4059+
jobFactoryMock.get.withArgs({ pipelineId: '2', name: 'd' }).resolves(pipeline2JobD);
4060+
jobFactoryMock.get.withArgs(pipeline2JobD.id).resolves(pipeline2JobD);
4061+
39044062
eventMock.workflowGraph = {
39054063
nodes: [
39064064
{ name: '~pr' },
@@ -4081,6 +4239,24 @@ describe('build plugin test', () => {
40814239
});
40824240

40834241
it('creates without starting join job in external join when fork not done', () => {
4242+
const pipeline2JobC = {
4243+
id: 3,
4244+
pipelineId: 2,
4245+
name: 'c',
4246+
state: 'ENABLED',
4247+
parsePRJobName: sinon.stub().returns('c'),
4248+
permutations: [
4249+
{
4250+
settings: {
4251+
email: 'foo@bar.com'
4252+
}
4253+
}
4254+
]
4255+
};
4256+
4257+
jobFactoryMock.get.withArgs({ pipelineId: '2', name: 'c' }).resolves(pipeline2JobC);
4258+
jobFactoryMock.get.withArgs(pipeline2JobC.id).resolves(pipeline2JobC);
4259+
40844260
eventMock.workflowGraph = {
40854261
nodes: [
40864262
{ name: '~pr' },
@@ -4703,6 +4879,24 @@ describe('build plugin test', () => {
47034879
// d
47044880
//
47054881
// If user restarts `123:a`, it should get `2:c`'s parent event status and trigger `c`
4882+
const pipeline2JobC = {
4883+
id: 3,
4884+
pipelineId: 2,
4885+
name: 'c',
4886+
state: 'ENABLED',
4887+
parsePRJobName: sinon.stub().returns('c'),
4888+
permutations: [
4889+
{
4890+
settings: {
4891+
email: 'foo@bar.com'
4892+
}
4893+
}
4894+
]
4895+
};
4896+
4897+
jobFactoryMock.get.withArgs({ pipelineId: '2', name: 'c' }).resolves(pipeline2JobC);
4898+
jobFactoryMock.get.withArgs(pipeline2JobC.id).resolves(pipeline2JobC);
4899+
47064900
eventMock.workflowGraph = {
47074901
nodes: [
47084902
{ name: '~pr' },
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
shared:
2+
image: node:20
3+
steps:
4+
- test: echo 'test'
5+
6+
jobs:
7+
hub:
8+
requires: [ ~commit, ~pr ]
9+
a:
10+
requires: [ ~hub ]
11+
b:
12+
requires: [ ~hub ]
13+
c:
14+
requires: [ ~hub ]
15+
d1:
16+
requires: [ ~a ]
17+
freezeWindows: ['* 10-21 ? * *']
18+
annotations:
19+
screwdriver.cd/virtualJob: true
20+
d2:
21+
requires: [ ~a, ~b ]
22+
freezeWindows: ['* 10-21 ? * *']
23+
annotations:
24+
screwdriver.cd/virtualJob: true
25+
d3:
26+
requires: [ ~a, b ]
27+
freezeWindows: ['* 10-21 ? * *']
28+
annotations:
29+
screwdriver.cd/virtualJob: true
30+
d4:
31+
requires: [ a, ~b ]
32+
freezeWindows: ['* 10-21 ? * *']
33+
annotations:
34+
screwdriver.cd/virtualJob: true
35+
d5:
36+
requires: [ a, b ]
37+
freezeWindows: ['* 10-21 ? * *']
38+
annotations:
39+
screwdriver.cd/virtualJob: true
40+
d6:
41+
requires: [ a, ~b, c ]
42+
freezeWindows: ['* 10-21 ? * *']
43+
annotations:
44+
screwdriver.cd/virtualJob: true
45+
d7:
46+
requires: [ a, b, c ]
47+
freezeWindows: ['* 10-21 ? * *']
48+
annotations:
49+
screwdriver.cd/virtualJob: true
50+
51+
target1:
52+
requires: [ ~d1 ]
53+
freezeWindows: ['* 10-21 ? * *']
54+
annotations:
55+
screwdriver.cd/virtualJob: true
56+
target2:
57+
requires: [ ~d1 ]

‎test/plugins/trigger.helper.test.js

+50-5
Original file line numberDiff line numberDiff line change
@@ -1189,6 +1189,7 @@ describe('handleNewBuild function', () => {
11891189
const handleNewBuild = RewiredTriggerHelper.__get__('handleNewBuild');
11901190

11911191
let newBuildMock;
1192+
let jobMock;
11921193

11931194
beforeEach(() => {
11941195
newBuildMock = {
@@ -1200,6 +1201,12 @@ describe('handleNewBuild function', () => {
12001201
remove: sinon.stub().resolves()
12011202
};
12021203

1204+
jobMock = {
1205+
id: 23,
1206+
name: 'main',
1207+
permutations: [{}]
1208+
};
1209+
12031210
sinon.stub(logger, 'info');
12041211
});
12051212

@@ -1211,7 +1218,8 @@ describe('handleNewBuild function', () => {
12111218
const result = await handleNewBuild({
12121219
done: false,
12131220
hasFailure: false,
1214-
newBuild: newBuildMock
1221+
newBuild: newBuildMock,
1222+
job: jobMock
12151223
});
12161224

12171225
assert.isNull(result);
@@ -1226,7 +1234,8 @@ describe('handleNewBuild function', () => {
12261234
const result = await handleNewBuild({
12271235
done: true,
12281236
hasFailure: false,
1229-
newBuild: newBuildMock
1237+
newBuild: newBuildMock,
1238+
job: jobMock
12301239
});
12311240

12321241
assert.strictEqual(result.status, Status.QUEUED);
@@ -1240,7 +1249,7 @@ describe('handleNewBuild function', () => {
12401249
done: true,
12411250
hasFailure: true,
12421251
newBuild: newBuildMock,
1243-
jobName: 'main',
1252+
job: jobMock,
12441253
pipelineId: 1,
12451254
stage: { name: 'deploy' }
12461255
});
@@ -1253,11 +1262,13 @@ describe('handleNewBuild function', () => {
12531262
});
12541263

12551264
it('should not remove new build if there is a failure and it is a stage teardown job', async () => {
1265+
jobMock.name = 'stage@deploy:teardown';
1266+
12561267
const result = await handleNewBuild({
12571268
done: true,
12581269
hasFailure: true,
12591270
newBuild: newBuildMock,
1260-
jobName: 'stage@deploy:teardown',
1271+
job: jobMock,
12611272
pipelineId: 1,
12621273
stageName: 'deploy'
12631274
});
@@ -1273,7 +1284,8 @@ describe('handleNewBuild function', () => {
12731284
const result = await handleNewBuild({
12741285
done: true,
12751286
hasFailure: false,
1276-
newBuild: newBuildMock
1287+
newBuild: newBuildMock,
1288+
job: jobMock
12771289
});
12781290

12791291
assert.strictEqual(result.status, Status.QUEUED);
@@ -1286,13 +1298,46 @@ describe('handleNewBuild function', () => {
12861298
done: true,
12871299
hasFailure: false,
12881300
newBuild: newBuildMock,
1301+
job: jobMock,
1302+
isVirtualJob: true
1303+
});
1304+
1305+
assert.strictEqual(newBuildMock.status, Status.SUCCESS);
1306+
sinon.assert.calledOnce(newBuildMock.update);
1307+
sinon.assert.notCalled(newBuildMock.start);
1308+
});
1309+
1310+
it('should skip the execution of virtual job when freeze windows is empty', async () => {
1311+
jobMock.permutations[0].freezeWindows = [];
1312+
1313+
await handleNewBuild({
1314+
done: true,
1315+
hasFailure: false,
1316+
newBuild: newBuildMock,
1317+
job: jobMock,
12891318
isVirtualJob: true
12901319
});
12911320

12921321
assert.strictEqual(newBuildMock.status, Status.SUCCESS);
12931322
sinon.assert.calledOnce(newBuildMock.update);
12941323
sinon.assert.notCalled(newBuildMock.start);
12951324
});
1325+
1326+
it('should add virtual job to the execution queue when the job has freeze windows', async () => {
1327+
jobMock.permutations[0].freezeWindows = ['* 10-21 ? * *'];
1328+
1329+
await handleNewBuild({
1330+
done: true,
1331+
hasFailure: false,
1332+
newBuild: newBuildMock,
1333+
job: jobMock,
1334+
isVirtualJob: true
1335+
});
1336+
1337+
assert.strictEqual(newBuildMock.status, Status.QUEUED);
1338+
sinon.assert.calledOnce(newBuildMock.update);
1339+
sinon.assert.calledOnce(newBuildMock.start);
1340+
});
12961341
});
12971342

12981343
describe('extractExternalJoinData function', () => {

‎test/plugins/trigger.test.helper.js

+11-4
Original file line numberDiff line numberDiff line change
@@ -187,11 +187,12 @@ class PipelineFactoryMock {
187187

188188
const { jobs, stages, workflowGraph } = pipeline;
189189

190-
Object.keys(jobs).forEach(name => {
190+
Object.entries(jobs).forEach(([name, jobConfigArr]) => {
191191
jobs[name] = this.server.app.jobFactory.create({
192192
name,
193193
pipeline,
194-
pipelineId: pipeline.id
194+
pipelineId: pipeline.id,
195+
permutations: jobConfigArr
195196
});
196197
});
197198

@@ -254,7 +255,14 @@ class PipelineFactoryMock {
254255
jobs[prJobName] = this.server.app.jobFactory.create({
255256
name: prJobName,
256257
pipeline,
257-
pipelineId: pipeline.id
258+
pipelineId: pipeline.id,
259+
permutations: [
260+
{
261+
settings: {
262+
email: 'foo@bar.com'
263+
}
264+
}
265+
]
258266
});
259267
}
260268
});
@@ -755,7 +763,6 @@ class JobFactoryMock {
755763
create(config) {
756764
const job = {
757765
...config,
758-
permutations: [{}],
759766
id: this.records.length,
760767
toJson: sinon.stub(),
761768
getLatestBuild: sinon.stub().resolves([]),

‎test/plugins/trigger.test.js

+35
Original file line numberDiff line numberDiff line change
@@ -3195,6 +3195,41 @@ describe('trigger tests', () => {
31953195
assert.equal(event.getBuildOf('d7').status, 'SUCCESS');
31963196
});
31973197

3198+
it('should add virtual jobs to execution queue when they have freeze windows', async () => {
3199+
const pipeline = await pipelineFactoryMock.createFromFile('virtual-jobs-with-freeze-windows.yaml');
3200+
3201+
const event = await eventFactoryMock.create({
3202+
pipelineId: pipeline.id,
3203+
startFrom: 'hub'
3204+
});
3205+
3206+
await event.getBuildOf('hub').complete('SUCCESS');
3207+
assert.equal(event.getBuildOf('a').status, 'RUNNING');
3208+
assert.equal(event.getBuildOf('b').status, 'RUNNING');
3209+
assert.equal(event.getBuildOf('c').status, 'RUNNING');
3210+
3211+
await event.getBuildOf('a').complete('SUCCESS');
3212+
assert.equal(event.getBuildOf('d1').status, 'RUNNING');
3213+
assert.equal(event.getBuildOf('d2').status, 'RUNNING');
3214+
assert.equal(event.getBuildOf('d3').status, 'RUNNING');
3215+
assert.equal(event.getBuildOf('d4').status, 'RUNNING');
3216+
assert.equal(event.getBuildOf('d5').status, 'CREATED');
3217+
assert.equal(event.getBuildOf('d6').status, 'CREATED');
3218+
assert.equal(event.getBuildOf('d7').status, 'CREATED');
3219+
3220+
await event.getBuildOf('d1').complete('SUCCESS');
3221+
assert.equal(event.getBuildOf('target1').status, 'RUNNING');
3222+
assert.equal(event.getBuildOf('target2').status, 'RUNNING');
3223+
3224+
await event.getBuildOf('b').complete('SUCCESS');
3225+
assert.equal(event.getBuildOf('d5').status, 'RUNNING');
3226+
assert.equal(event.getBuildOf('d6').status, 'RUNNING');
3227+
assert.equal(event.getBuildOf('d7').status, 'CREATED');
3228+
3229+
await event.getBuildOf('c').complete('SUCCESS');
3230+
assert.equal(event.getBuildOf('d7').status, 'RUNNING');
3231+
});
3232+
31983233
describe('Tests for behavior not ideal', () => {
31993234
it('[ sd@2:a, sd@3:a ] is triggered when restarts a and wait for downstream restart builds', async () => {
32003235
const upstreamPipeline = await pipelineFactoryMock.createFromFile('sd@2:a_sd@3:a-upstream.yaml');

0 commit comments

Comments
 (0)
Please sign in to comment.