Skip to content

Commit ad11430

Browse files
tkyisagar1312
andauthoredNov 20, 2023
feat(2767): Add stage api (#2951)
* feat(2767): Add endpoint for getting stageBuilds * feat: Add stage workflow logic * fix: Remove eventId from pipelines listStages * fix: Add events list stageBuilds endpoint * fix: Address minor comments * feat(2767): Remove 'workflowGraph' from stageBuilds (#2933) * fix: Push local changes * feat: Redo stage logic, missing tests * fix: Working tests * feat: Logic for no join working with tests, missing logic for failed build in middle of stage * fix: Add stage API * fix: Update plugins/pipelines/README.md Co-authored-by: Dayanand Sagar <[email protected]> * fix: Update plugins/helper.js Co-authored-by: Dayanand Sagar <[email protected]> * fix: Update plugins/helper.js Co-authored-by: Dayanand Sagar <[email protected]> * fix: Address comments * fix: pull latest models --------- Co-authored-by: Dayanand Sagar <[email protected]>
1 parent 576f303 commit ad11430

15 files changed

+406
-9
lines changed
 

‎bin/server

+5
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,10 @@ const stageFactory = Models.StageFactory.getInstance({
216216
datastore,
217217
datastoreRO
218218
});
219+
const stageBuildFactory = Models.StageBuildFactory.getInstance({
220+
datastore,
221+
datastoreRO
222+
});
219223
const triggerFactory = Models.TriggerFactory.getInstance({
220224
datastore,
221225
datastoreRO
@@ -253,6 +257,7 @@ datastore.setup(datastoreConfig.ddlSyncEnabled).then(() =>
253257
eventFactory,
254258
collectionFactory,
255259
stageFactory,
260+
stageBuildFactory,
256261
triggerFactory,
257262
banners: authConfig,
258263
builds: {

‎lib/server.js

+2
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ function prettyPrintErrors(request, h) {
7575
* @param {Factory} config.eventFactory Event Factory instance
7676
* @param {Factory} config.collectionFactory Collection Factory instance
7777
* @param {Factory} config.stageFactory Stage Factory instance
78+
* @param {Factory} config.stageBuildFactory Stage Build Factory instance
7879
* @param {Factory} config.triggerFactory Trigger Factory instance
7980
* @param {Object} config.builds Config to include for builds plugin
8081
* @param {Object} config.builds.ecosystem List of hosts in the ecosystem
@@ -126,6 +127,7 @@ module.exports = async config => {
126127
templateFactory: config.templateFactory,
127128
templateTagFactory: config.templateTagFactory,
128129
stageFactory: config.stageFactory,
130+
stageBuildFactory: config.stageBuildFactory,
129131
triggerFactory: config.triggerFactory,
130132
pipelineFactory: config.pipelineFactory,
131133
jobFactory: config.jobFactory,

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@
114114
"screwdriver-executor-queue": "^4.0.0",
115115
"screwdriver-executor-router": "^3.0.0",
116116
"screwdriver-logger": "^2.0.0",
117-
"screwdriver-models": "^29.7.0",
117+
"screwdriver-models": "^29.11.0",
118118
"screwdriver-notifications-email": "^3.0.0",
119119
"screwdriver-notifications-slack": "^5.0.0",
120120
"screwdriver-request": "^2.0.1",

‎plugins/events/index.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const boom = require('@hapi/boom');
44
const createRoute = require('./create');
55
const getRoute = require('./get');
66
const listBuildsRoute = require('./listBuilds');
7+
const listStageBuildsRoute = require('./listStageBuilds');
78
const stopBuildsRoute = require('./stopBuilds');
89
const metricsRoute = require('./metrics');
910

@@ -51,7 +52,14 @@ const eventsPlugin = {
5152
return Promise.resolve();
5253
});
5354

54-
server.route([createRoute(), getRoute(), listBuildsRoute(), stopBuildsRoute(), metricsRoute()]);
55+
server.route([
56+
createRoute(),
57+
getRoute(),
58+
listBuildsRoute(),
59+
listStageBuildsRoute(),
60+
stopBuildsRoute(),
61+
metricsRoute()
62+
]);
5563
}
5664
};
5765

‎plugins/events/listStageBuilds.js

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use strict';
2+
3+
const boom = require('@hapi/boom');
4+
const joi = require('joi');
5+
const schema = require('screwdriver-data-schema');
6+
const stageBuildListSchema = joi.array().items(schema.models.stageBuild.get).label('List of stage builds');
7+
const eventIdSchema = schema.models.event.base.extract('id');
8+
9+
module.exports = () => ({
10+
method: 'GET',
11+
path: '/events/{id}/stageBuilds',
12+
options: {
13+
description: 'Get stage builds for a given event',
14+
notes: 'Returns stage builds for a given event',
15+
tags: ['api', 'events', 'stageBuilds'],
16+
auth: {
17+
strategies: ['token'],
18+
scope: ['user', 'pipeline']
19+
},
20+
21+
handler: async (request, h) => {
22+
const { eventFactory } = request.server.app;
23+
const event = await eventFactory.get(request.params.id);
24+
25+
if (!event) {
26+
throw boom.notFound('Event does not exist');
27+
}
28+
29+
return event
30+
.getStageBuilds()
31+
.then(stageBuilds => h.response(stageBuilds.map(c => c.toJson())))
32+
.catch(err => {
33+
throw err;
34+
});
35+
},
36+
response: {
37+
schema: stageBuildListSchema
38+
},
39+
validate: {
40+
params: joi.object({
41+
id: eventIdSchema
42+
})
43+
}
44+
}
45+
});

‎plugins/pipelines/README.md

-3
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,6 @@ Only PR events of specified PR number will be searched when `prNum` is set
121121
#### Get all stages for a single pipeline
122122

123123
`GET /pipelines/{id}/stages`
124-
Will get latest commit event's stages if no event ID or group event ID is provided
125-
126-
`GET /pipelines/{id}/stages?groupEventId={groupEventId}`
127124

128125
`GET /pipelines/{id}/stages?eventId={eventId}`
129126

‎plugins/pipelines/listStages.js

-2
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,6 @@ module.exports = () => ({
4949
throw boom.notFound(`Latest event does not exist for pipeline ${pipelineId}`);
5050
}
5151

52-
config.params.eventId = latestCommitEvents[0].id;
53-
5452
return stageFactory.list(config);
5553
})
5654
.then(stages => h.response(stages.map(s => s.toJson())))

‎plugins/stages/README.md

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Stages Plugin
2+
> API stages plugin for the Screwdriver API
3+
4+
## Usage
5+
6+
### Register plugin
7+
8+
```javascript
9+
const Hapi = require('@hapi/hapi');
10+
const server = new Hapi.Server();
11+
const stagesPlugin = require('./');
12+
13+
server.connection({ port: 3000 });
14+
15+
server.register({
16+
register: stagesPlugin,
17+
options: {}
18+
}, () => {
19+
server.start((err) => {
20+
if (err) {
21+
throw err;
22+
}
23+
console.log('Server running at:', server.info.uri);
24+
});
25+
});
26+
```
27+
28+
### Routes
29+
30+
#### Get a listing of all stage builds for a stage
31+
32+
`GET /stages/{id}/stageBuilds`

‎plugins/stages/index.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use strict';
2+
3+
const getStageBuildsRoute = require('./stageBuilds/list');
4+
5+
/**
6+
* Stage API Plugin
7+
* @method register
8+
* @param {Hapi} server Hapi Server
9+
* @param {Object} options Configuration
10+
* @param {Function} next Function to call when done
11+
*/
12+
const stagesPlugin = {
13+
name: 'stages',
14+
async register(server) {
15+
server.route([getStageBuildsRoute()]);
16+
}
17+
};
18+
19+
module.exports = stagesPlugin;

‎plugins/stages/stageBuilds/list.js

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
'use strict';
2+
3+
const boom = require('@hapi/boom');
4+
const joi = require('joi');
5+
const schema = require('screwdriver-data-schema');
6+
const listSchema = joi.array().items(schema.models.stageBuild.get).label('List of stage builds for a stage');
7+
const idSchema = schema.models.stage.base.extract('id');
8+
const eventIdSchema = schema.models.stageBuild.base.extract('eventId');
9+
10+
module.exports = () => ({
11+
method: 'GET',
12+
path: '/stages/{id}/stageBuilds',
13+
options: {
14+
description: 'Get stage builds for a stage',
15+
notes: 'Returns all stage builds for a stage',
16+
tags: ['api', 'stageBuilds'],
17+
auth: {
18+
strategies: ['token'],
19+
scope: ['user', '!guest']
20+
},
21+
22+
handler: async (request, h) => {
23+
const { stageFactory, stageBuildFactory } = request.server.app;
24+
const { page, count } = request.query;
25+
const config = {
26+
sort: request.query.sort,
27+
params: {
28+
stageId: request.params.id
29+
}
30+
};
31+
32+
return stageFactory.get(config.params.stageId).then(async stage => {
33+
if (!stage) {
34+
throw boom.notFound(`Stage ${config.params.stageId} does not exist`);
35+
}
36+
37+
if (page || count) {
38+
config.paginate = { page, count };
39+
}
40+
41+
// Set eventId if provided
42+
if (request.query.eventId) {
43+
config.params.eventId = request.query.eventId;
44+
}
45+
46+
return stageBuildFactory
47+
.list(config)
48+
.then(stageBuilds => h.response(stageBuilds.map(c => c.toJson())))
49+
.catch(err => {
50+
throw err;
51+
});
52+
});
53+
},
54+
response: {
55+
schema: listSchema
56+
},
57+
validate: {
58+
params: joi.object({
59+
id: idSchema
60+
}),
61+
query: schema.api.pagination.concat(
62+
joi.object({
63+
eventId: eventIdSchema
64+
})
65+
)
66+
}
67+
}
68+
});

‎test/plugins/data/stage.json

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"id": 1234,
3+
"pipelineId": 555,
4+
"name": "deploy",
5+
"jobIds": [1, 2, 3, 4],
6+
"description": "Deploys canary job",
7+
"setup": [222],
8+
"teardown": [333],
9+
"archived": false
10+
}

‎test/plugins/data/stageBuilds.json

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[{
2+
"id": 220,
3+
"stageId": 1234,
4+
"eventId": 220,
5+
"status": "SUCCESS"
6+
}]

‎test/plugins/events.test.js

+61
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const hapi = require('@hapi/hapi');
88
const hoek = require('@hapi/hoek');
99
const testBuild = require('./data/build.json');
1010
const testBuilds = require('./data/builds.json');
11+
const testStageBuilds = require('./data/stageBuilds.json');
1112
const testEvent = require('./data/events.json')[0];
1213
const testEventPr = require('./data/eventsPr.json')[0];
1314

@@ -23,6 +24,14 @@ const decorateBuildMock = build => {
2324
return mock;
2425
};
2526

27+
const decorateStageBuildMock = stageBuild => {
28+
const mock = hoek.clone(stageBuild);
29+
30+
mock.toJson = sinon.stub().returns(stageBuild);
31+
32+
return mock;
33+
};
34+
2635
const getBuildMocks = builds => {
2736
if (Array.isArray(builds)) {
2837
return builds.map(decorateBuildMock);
@@ -31,10 +40,19 @@ const getBuildMocks = builds => {
3140
return decorateBuildMock(builds);
3241
};
3342

43+
const getStageBuildMocks = stageBuilds => {
44+
if (Array.isArray(stageBuilds)) {
45+
return stageBuilds.map(decorateStageBuildMock);
46+
}
47+
48+
return decorateStageBuildMock(stageBuilds);
49+
};
50+
3451
const getEventMock = event => {
3552
const decorated = hoek.clone(event);
3653

3754
decorated.getBuilds = sinon.stub();
55+
decorated.getStageBuilds = sinon.stub();
3856
decorated.toJson = sinon.stub().returns(event);
3957

4058
return decorated;
@@ -223,6 +241,49 @@ describe('event plugin test', () => {
223241
});
224242
});
225243

244+
describe('GET /events/{id}/stageBuilds', () => {
245+
const id = 12345;
246+
let options;
247+
let event;
248+
let stageBuilds;
249+
250+
beforeEach(() => {
251+
options = {
252+
method: 'GET',
253+
url: `/events/${id}/stageBuilds`
254+
};
255+
256+
event = getEventMock(testEvent);
257+
stageBuilds = getStageBuildMocks(testStageBuilds);
258+
259+
eventFactoryMock.get.withArgs(id).resolves(event);
260+
event.getStageBuilds.resolves(stageBuilds);
261+
});
262+
263+
it('returns 404 if event does not exist', () => {
264+
eventFactoryMock.get.withArgs(id).resolves(null);
265+
266+
return server.inject(options).then(reply => {
267+
assert.equal(reply.statusCode, 404);
268+
});
269+
});
270+
271+
it('returns 200 for getting stage builds', () =>
272+
server.inject(options).then(reply => {
273+
assert.equal(reply.statusCode, 200);
274+
assert.calledWith(event.getStageBuilds);
275+
assert.deepEqual(reply.result, testStageBuilds);
276+
}));
277+
278+
it('returns 500 when the datastore returns an error', () => {
279+
event.getStageBuilds.rejects(new Error('icantdothatdave'));
280+
281+
return server.inject(options).then(reply => {
282+
assert.equal(reply.statusCode, 500);
283+
});
284+
});
285+
});
286+
226287
describe('POST /events', () => {
227288
const parentEventId = 12345;
228289
let options;

‎test/plugins/pipelines.test.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -1002,8 +1002,7 @@ describe('pipeline plugin test', () => {
10021002
assert.equal(reply.statusCode, 200);
10031003
assert.calledWith(stageFactoryMock.list, {
10041004
params: {
1005-
pipelineId: id,
1006-
eventId: 12345
1005+
pipelineId: id
10071006
}
10081007
});
10091008
assert.calledWith(eventFactoryMock.list, {

‎test/plugins/stages.test.js

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
'use strict';
2+
3+
const { assert } = require('chai');
4+
const sinon = require('sinon');
5+
const hapi = require('@hapi/hapi');
6+
const hoek = require('@hapi/hoek');
7+
const testStage = require('./data/stage.json');
8+
const testStageBuilds = require('./data/stageBuilds.json');
9+
10+
sinon.assert.expose(assert, { prefix: '' });
11+
12+
const decorateObj = obj => {
13+
const mock = hoek.clone(obj);
14+
15+
mock.toJson = sinon.stub().returns(obj);
16+
17+
return mock;
18+
};
19+
20+
const getStageBuildMocks = stageBuilds => {
21+
if (Array.isArray(stageBuilds)) {
22+
return stageBuilds.map(decorateObj);
23+
}
24+
25+
return decorateObj(stageBuilds);
26+
};
27+
28+
describe('stage plugin test', () => {
29+
let stageFactoryMock;
30+
let stageBuildFactoryMock;
31+
let plugin;
32+
let server;
33+
34+
beforeEach(async () => {
35+
stageFactoryMock = {
36+
get: sinon.stub()
37+
};
38+
stageBuildFactoryMock = {
39+
list: sinon.stub()
40+
};
41+
42+
/* eslint-disable global-require */
43+
plugin = require('../../plugins/stages');
44+
/* eslint-enable global-require */
45+
server = new hapi.Server({
46+
port: 1234
47+
});
48+
server.app = {
49+
stageFactory: stageFactoryMock,
50+
stageBuildFactory: stageBuildFactoryMock
51+
};
52+
53+
server.auth.scheme('custom', () => ({
54+
authenticate: (request, h) =>
55+
h.authenticated({
56+
credentials: {
57+
scope: ['user']
58+
}
59+
})
60+
}));
61+
server.auth.strategy('token', 'custom');
62+
63+
await server.register({ plugin });
64+
});
65+
66+
afterEach(() => {
67+
server = null;
68+
});
69+
70+
it('registers the plugin', () => {
71+
assert.isOk(server.registrations.stages);
72+
});
73+
74+
describe('GET /stages/id/stageBuilds', () => {
75+
let options;
76+
77+
beforeEach(() => {
78+
options = {
79+
method: 'GET',
80+
url: '/stages/1234/stageBuilds'
81+
};
82+
});
83+
84+
it('returns 200 and stageBuilds when given the stage id', () => {
85+
stageFactoryMock.get.resolves(testStage);
86+
stageBuildFactoryMock.list.resolves(getStageBuildMocks(testStageBuilds));
87+
88+
return server.inject(options).then(reply => {
89+
assert.deepEqual(reply.result, testStageBuilds);
90+
assert.calledWith(stageFactoryMock.get, 1234);
91+
assert.calledWith(stageBuildFactoryMock.list, { sort: 'descending', params: { stageId: 1234 } });
92+
assert.equal(reply.statusCode, 200);
93+
});
94+
});
95+
96+
it('returns 200 and stageBuilds when given the stage id with request query params', () => {
97+
const expectedStageBuildArgs = {
98+
sort: 'descending',
99+
params: {
100+
stageId: 1234,
101+
eventId: 220
102+
},
103+
paginate: {
104+
page: 1,
105+
count: 1
106+
}
107+
};
108+
109+
options.url = '/stages/1234/stageBuilds?page=1&count=1&eventId=220';
110+
111+
stageFactoryMock.get.resolves(testStage);
112+
stageBuildFactoryMock.list.resolves(getStageBuildMocks(testStageBuilds));
113+
114+
return server.inject(options).then(reply => {
115+
assert.deepEqual(reply.result, testStageBuilds);
116+
assert.calledWith(stageFactoryMock.get, 1234);
117+
assert.calledWith(stageBuildFactoryMock.list, expectedStageBuildArgs);
118+
assert.equal(reply.statusCode, 200);
119+
});
120+
});
121+
122+
it('returns 404 when stage does not exist', () => {
123+
stageFactoryMock.get.resolves(null);
124+
125+
return server.inject(options).then(reply => {
126+
assert.equal(reply.statusCode, 404);
127+
});
128+
});
129+
130+
it('returns 500 when datastore fails for stageFactory', () => {
131+
stageFactoryMock.get.rejects(new Error('some error'));
132+
133+
return server.inject(options).then(reply => {
134+
assert.equal(reply.statusCode, 500);
135+
});
136+
});
137+
138+
it('returns 500 when datastore fails for stageBuildFactory', () => {
139+
stageFactoryMock.get.resolves(testStage);
140+
stageBuildFactoryMock.list.resolves(new Error('some error'));
141+
142+
return server.inject(options).then(reply => {
143+
assert.equal(reply.statusCode, 500);
144+
});
145+
});
146+
});
147+
});

0 commit comments

Comments
 (0)
Please sign in to comment.