Skip to content

Commit

Permalink
feat: Add pipelines/id/builds endpoint (#3070)
Browse files Browse the repository at this point in the history
  • Loading branch information
tkyi authored Apr 3, 2024
1 parent 58053d3 commit b9ed3f4
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 12 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
"screwdriver-config-parser": "^9.0.0",
"screwdriver-coverage-bookend": "^2.0.0",
"screwdriver-coverage-sonar": "^4.1.1",
"screwdriver-data-schema": "^23.0.0",
"screwdriver-data-schema": "^23.2.0",
"screwdriver-datastore-sequelize": "^8.1.1",
"screwdriver-executor-base": "^9.0.1",
"screwdriver-executor-docker": "^6.0.0",
Expand All @@ -114,7 +114,7 @@
"screwdriver-executor-queue": "^4.0.0",
"screwdriver-executor-router": "^3.0.0",
"screwdriver-logger": "^2.0.0",
"screwdriver-models": "^29.18.0",
"screwdriver-models": "^29.18.4",
"screwdriver-notifications-email": "^3.0.0",
"screwdriver-notifications-slack": "^5.0.0",
"screwdriver-request": "^2.0.1",
Expand Down
17 changes: 9 additions & 8 deletions plugins/builds/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,16 @@ Example payload:
}
```

#### Creates a build
`POST /builds`
#### Get build statuses
`GET /builds/statuses`

Example payload:
```json
{
"jobId": "d398fb192747c9a0124e9e5b4e6e8e841cf8c71c"
}
```
`GET /builds/statuses?jobIds=1&jobIds=2&numBuilds=3&offset=0`

Arguments:

* `jobIds` - Job IDs for builds to fetch
* `numBuilds` - Number of builds to load (default 1)
* `offset` - Number of build statuses to skip (default 0)

#### Updates a build
`PUT /builds/{id}`
Expand Down
1 change: 0 additions & 1 deletion plugins/events/listBuilds.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ module.exports = () => ({
}

const config = readOnly ? { readOnly: true } : {};

const buildsModel = await event.getBuilds(config);

let data;
Expand Down
8 changes: 7 additions & 1 deletion plugins/pipelines/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ Only PR events of specified PR number will be searched when `prNum` is set

`GET /pipelines/{id}/events?page={pageNumber}&count={countNumber}&sort={sort}&prNum={prNumber}`

#### Get all pipeline builds
`page`, `count`, `sort`, `latest`, `sortBy`, `fetchSteps`, `readOnly`, and `groupEventId` are optional
When `latest=true` and `groupEventId` is set, only latest builds in a pipeline based on groupEventId will be returned. The `latest` parameter must be used in conjunction with the `groupEventId`.

`GET /pipelines/{id}/builds?page={pageNumber}&count={countNumber}&sort={sort}&latest=true&groupEventId={groupEventId}&sortBy={sortBy}&fetchSteps=false&readOnly=false`

#### Get all jobs (including pull requests jobs)
`archived` is optional and has a default value of `false`, which makes the endpoint not return archived jobs (e.g. closed pull requests)

Expand Down Expand Up @@ -380,4 +386,4 @@ Example payload:
{
"trusted": true
}
```
```
2 changes: 2 additions & 0 deletions plugins/pipelines/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const listStagesRoute = require('./listStages');
const listTriggersRoute = require('./listTriggers');
const listSecretsRoute = require('./listSecrets');
const listEventsRoute = require('./listEvents');
const listBuildsRoute = require('./listBuilds');
const startAllRoute = require('./startAll');
const createToken = require('./tokens/create');
const updateToken = require('./tokens/update');
Expand Down Expand Up @@ -249,6 +250,7 @@ const pipelinesPlugin = {
listTriggersRoute(),
listSecretsRoute(),
listEventsRoute(),
listBuildsRoute(),
startAllRoute(),
updateToken(),
refreshToken(),
Expand Down
92 changes: 92 additions & 0 deletions plugins/pipelines/listBuilds.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
'use strict';

const boom = require('@hapi/boom');
const joi = require('joi');
const schema = require('screwdriver-data-schema');
const groupEventIdSchema = schema.models.event.base.extract('groupEventId');
const pipelineIdSchema = schema.models.pipeline.base.extract('id');

module.exports = () => ({
method: 'GET',
path: '/pipelines/{id}/builds',
options: {
description: 'Get builds for this pipeline',
notes: 'Returns builds for the given pipeline',
tags: ['api', 'pipelines', 'builds'],
auth: {
strategies: ['token'],
scope: ['user', 'build', 'pipeline']
},

handler: async (request, h) => {
const factory = request.server.app.pipelineFactory;
const { sort, sortBy, page, count, fetchSteps, readOnly, groupEventId, latest } = request.query;

return factory
.get(request.params.id)
.then(pipeline => {
if (!pipeline) {
throw boom.notFound('Pipeline does not exist');
}

const config = readOnly
? { sort, sortBy: 'createTime', readOnly: true }
: { sort, sortBy: 'createTime' };

if (sortBy) {
config.sortBy = sortBy;
}

if (page || count) {
config.paginate = { page, count };
}

if (groupEventId) {
config.params = {
...config.params,
groupEventId
};

// Latest flag only works in conjunction with groupEventId
if (latest) {
config.params.latest = latest;
}
}

return pipeline.getBuilds(config);
})
.then(async builds => {
let data;

if (fetchSteps) {
data = await Promise.all(builds.map(b => b.toJsonWithSteps()));
} else {
data = await Promise.all(builds.map(b => b.toJson()));
}

return h.response(data);
})
.catch(err => {
throw err;
});
},
response: {
schema: joi.array()
},
validate: {
params: joi.object({
id: pipelineIdSchema
}),
query: schema.api.pagination.concat(
joi.object({
readOnly: joi.boolean().truthy('true').falsy('false').default(true),
fetchSteps: joi.boolean().truthy('true').falsy('false').default(true),
groupEventId: groupEventIdSchema,
latest: joi.boolean().truthy('true').falsy('false').default(false),
search: joi.forbidden(), // we don't support search for Pipeline list builds
getCount: joi.forbidden() // we don't support getCount for Pipeline list builds
})
)
}
}
});
110 changes: 110 additions & 0 deletions test/plugins/pipelines.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const decorateBuildMock = build => {
const mock = hoek.clone(build);

mock.toJsonWithSteps = sinon.stub().resolves(build);
mock.toJson = sinon.stub().returns(build);

return mock;
};
Expand Down Expand Up @@ -88,6 +89,7 @@ const decoratePipelineMock = pipeline => {
mock.jobs = sinon.stub();
mock.getJobs = sinon.stub();
mock.getEvents = sinon.stub();
mock.getBuilds = sinon.stub();
mock.remove = sinon.stub();
mock.admin = sinon.stub();
mock.getFirstAdmin = sinon.stub();
Expand Down Expand Up @@ -1481,6 +1483,114 @@ describe('pipeline plugin test', () => {
});
});

describe('GET /pipelines/{id}/builds', () => {
const id = '123';
let options;
let pipelineMock;

beforeEach(() => {
options = {
method: 'GET',
url: `/pipelines/${id}/builds`
};
pipelineMock = getPipelineMocks(testPipeline);
pipelineMock.getBuilds.resolves(getBuildMocks(testBuilds));
pipelineFactoryMock.get.resolves(pipelineMock);
});

it('returns 200 for getting builds', () => {
options.url = `/pipelines/${id}/builds`;
server.inject(options).then(reply => {
assert.calledOnce(pipelineMock.getBuilds);
assert.calledWith(pipelineMock.getBuilds, {});
assert.deepEqual(reply.result, testBuilds);
assert.equal(reply.statusCode, 200);
});
});

it('returns 200 for getting builds without steps', () => {
options.url = `/pipelines/${id}/builds?fetchSteps=false`;
server.inject(options).then(reply => {
assert.calledOnce(pipelineMock.getBuilds);
assert.calledWith(pipelineMock.getBuilds, {});
assert.deepEqual(reply.result, testBuilds);
assert.equal(reply.statusCode, 200);
});
});

it('returns 200 for getting builds with pagination', () => {
options.url = `/pipelines/${id}/builds?count=30`;
server.inject(options).then(reply => {
assert.calledOnce(pipelineMock.getBuilds);
assert.calledWith(pipelineMock.getBuilds, {
paginate: { page: undefined, count: 30 }
});
assert.deepEqual(reply.result, testBuilds);
assert.equal(reply.statusCode, 200);
});
});

it('returns 200 for getting builds with sortBy', () => {
options.url = `/pipelines/${id}/builds?sortBy=createTime&readOnly=false`;
server.inject(options).then(reply => {
assert.calledOnce(pipelineMock.getBuilds);
assert.calledWith(pipelineMock.getBuilds, {
sortBy: 'createTime',
readOnly: false
});
assert.deepEqual(reply.result, testBuilds);
assert.equal(reply.statusCode, 200);
});
});

it('returns 200 for getting builds with groupEventId', () => {
options.url = `/pipelines/${id}/builds?groupEventId=999`;
server.inject(options).then(reply => {
assert.calledOnce(pipelineMock.getBuilds);
assert.calledWith(pipelineMock.getBuilds, { params: { groupEventId: 999 } });
assert.deepEqual(reply.result, testEvents);
assert.equal(reply.statusCode, 200);
});
});

it('returns 200 and does not use latest flag if no groupEventId is set', () => {
options.url = `/pipelines/${id}/builds?latest=true`;
server.inject(options).then(reply => {
assert.calledOnce(pipelineMock.getBuilds);
assert.calledWith(pipelineMock.getBuilds, {});
assert.deepEqual(reply.result, testEvents);
assert.equal(reply.statusCode, 200);
});
});

it('returns 200 with groupEventId and latest', () => {
options.url = `/pipelines/${id}/builds?groupEventId=999&latest=true`;
server.inject(options).then(reply => {
assert.calledOnce(pipelineMock.getBuilds);
assert.calledWith(pipelineMock.getBuilds, { params: { groupEventId: 999, latest: true } });
assert.deepEqual(reply.result, testEvents);
assert.equal(reply.statusCode, 200);
});
});

it('returns 404 for pipeline that does not exist', () => {
pipelineFactoryMock.get.resolves(null);

return server.inject(options).then(reply => {
assert.equal(reply.statusCode, 404);
});
});

it('returns 500 when the datastore returns an error', () => {
pipelineFactoryMock.get.resolves(pipelineMock);
pipelineMock.getBuilds.rejects(new Error('getBuildsError'));

return server.inject(options).then(reply => {
assert.equal(reply.statusCode, 500);
});
});
});

describe('POST /pipelines/{id}/sync', () => {
const id = 123;
const scmUri = 'github.com:12345:branchName';
Expand Down

0 comments on commit b9ed3f4

Please sign in to comment.