Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add pipelines/id/builds endpoint [2] #3070

Merged
merged 9 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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. `latest` and `groupEventId` must be set together
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
When `latest=true` and `groupEventId` is set, only latest builds in a pipeline based on groupEventId will be returned. `latest` and `groupEventId` must be set together
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`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchSteps=false will omit steps in the build response? This might be param that we can use to filter out the steps field in the build response.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes


#### 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
93 changes: 93 additions & 0 deletions plugins/pipelines/listBuilds.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use strict';

const boom = require('@hapi/boom');
const joi = require('joi');
const schema = require('screwdriver-data-schema');
const buildListSchema = joi.array().items(schema.models.build.get).label('List of builds');
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: buildListSchema
},
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