Extract Jenkins logic from router and test

Signed-off-by: Andrew Shirley <andrew.shirley@sainsburys.co.uk>
Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
Andrew Shirley
2021-06-16 17:41:40 +01:00
committed by blam
parent 8e9f167582
commit fef7cd6cad
4 changed files with 733 additions and 238 deletions
@@ -0,0 +1,421 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JenkinsApiImpl } from './jenkinsApi';
import jenkins from 'jenkins';
import { JenkinsInfo } from './jenkinsInfoProvider';
import { JenkinsBuild, JenkinsProject } from '../types';
jest.mock('jenkins');
const mockedJenkinsClient = {
job: {
get: jest.fn(),
build: jest.fn(),
},
build: {
get: jest.fn(),
},
};
const mockedJenkins = jenkins as jest.Mocked<any>;
mockedJenkins.mockReturnValue(mockedJenkinsClient);
const jobName = 'example-jobName/foo';
const buildNumber = 19;
const jenkinsInfo: JenkinsInfo = {
baseUrl: 'https://jenkins.example.com',
headers: { headerName: 'headerValue' },
jobName: 'example-jobName',
};
describe('JenkinsApi', () => {
const jenkinsApi = new JenkinsApiImpl();
describe('getProjects', () => {
const project: JenkinsProject = {
actions: [],
displayName: 'Example Build',
fullDisplayName: 'Example jobName » Example Build',
fullName: 'example-jobName/exampleBuild',
inQueue: false,
lastBuild: {
actions: [],
timestamp: 1,
building: false,
duration: 10,
result: 'success',
displayName: '#7',
fullDisplayName: 'Example jobName » Example Build #7',
url: 'https://jenkins.example.com/job/example-jobName/job/exampleBuild',
number: 7,
},
};
describe('unfiltered', () => {
it('standard github layout', async () => {
mockedJenkinsClient.job.get.mockResolvedValueOnce({ jobs: [project] });
const result = await jenkinsApi.getProjects(jenkinsInfo);
expect(mockedJenkins).toHaveBeenCalledWith({
baseUrl: jenkinsInfo.baseUrl,
headers: jenkinsInfo.headers,
promisify: true,
});
expect(mockedJenkinsClient.job.get).toBeCalledWith({
name: jenkinsInfo.jobName,
tree: expect.anything(),
});
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
actions: [],
displayName: 'Example Build',
fullDisplayName: 'Example jobName » Example Build',
fullName: 'example-jobName/exampleBuild',
inQueue: false,
lastBuild: {
actions: [],
timestamp: 1,
building: false,
duration: 10,
result: 'success',
displayName: '#7',
fullDisplayName: 'Example jobName » Example Build #7',
url:
'https://jenkins.example.com/job/example-jobName/job/exampleBuild',
number: 7,
status: 'success',
source: {},
},
status: 'success',
});
});
it.skip('3-layer nesting', async () => {
mockedJenkinsClient.job.get.mockResolvedValueOnce({
jobs: [{ jobs: [project] }],
});
const result = await jenkinsApi.getProjects(jenkinsInfo);
expect(mockedJenkins).toHaveBeenCalledWith({
baseUrl: jenkinsInfo.baseUrl,
headers: jenkinsInfo.headers,
promisify: true,
});
expect(mockedJenkinsClient.job.get).toBeCalledWith({
name: jenkinsInfo.jobName,
tree: expect.anything(),
});
expect(result).toHaveLength(1);
expect(result[0].fullName).toEqual('example-jobName/exampleBuild');
});
it.skip('start at project', async () => {
mockedJenkinsClient.job.get.mockResolvedValueOnce(project);
const result = await jenkinsApi.getProjects(jenkinsInfo);
expect(mockedJenkins).toHaveBeenCalledWith({
baseUrl: jenkinsInfo.baseUrl,
headers: jenkinsInfo.headers,
promisify: true,
});
expect(mockedJenkinsClient.job.get).toBeCalledWith({
name: jenkinsInfo.jobName,
tree: expect.anything(),
});
expect(result).toHaveLength(1);
expect(result[0].fullName).toEqual('example-jobName/exampleBuild');
});
});
describe('filtered by branch', () => {
it('standard github layout', async () => {
mockedJenkinsClient.job.get.mockResolvedValueOnce(project);
const result = await jenkinsApi.getProjects(
jenkinsInfo,
'testBranchName',
);
expect(mockedJenkins).toHaveBeenCalledWith({
baseUrl: jenkinsInfo.baseUrl,
headers: jenkinsInfo.headers,
promisify: true,
});
expect(mockedJenkinsClient.job.get).toBeCalledWith({
name: `${jenkinsInfo.jobName}/testBranchName`,
tree: expect.anything(),
});
expect(result).toHaveLength(1);
});
});
describe('augmented', () => {
const projectWithScmActions: JenkinsProject = {
actions: [
{},
{},
{},
{},
{
_class: 'jenkins.scm.api.metadata.ContributorMetadataAction',
contributor: 'testuser',
contributorDisplayName: 'Mr. T User',
contributorEmail: null,
},
{},
{
_class: 'jenkins.scm.api.metadata.ObjectMetadataAction',
objectDescription: '',
objectDisplayName: 'Add LICENSE, CoC etc',
objectUrl: 'https://github.com/backstage/backstage/pull/1',
},
{},
{},
{
_class: 'com.cloudbees.plugins.credentials.ViewCredentialsAction',
stores: {},
},
],
displayName: 'Example Build',
fullDisplayName: 'Example jobName » Example Build',
fullName: 'example-jobName/exampleBuild',
inQueue: false,
lastBuild: {
actions: [
{
_class: 'hudson.model.CauseAction',
causes: [
{
_class: 'jenkins.branch.BranchIndexingCause',
shortDescription: 'Branch indexing',
},
],
},
{},
{},
{},
{
_class: 'org.jenkinsci.plugins.workflow.cps.EnvActionImpl',
environment: {},
},
{},
{},
{},
{},
{},
{
_class: 'hudson.plugins.git.util.BuildData',
buildsByBranchName: {
'PR-1': {
_class: 'hudson.plugins.git.util.Build',
buildNumber: 5,
buildResult: null,
marked: {
SHA1: '14d31bde346fcad64ab939f82d195db36701cfcb',
branch: [
{
SHA1: '14d31bde346fcad64ab939f82d195db36701cfcb',
name: 'PR-1',
},
],
},
revision: {
SHA1: '6c6b34c0fb91cf077a01fe62d3e8e996b4ea5861',
branch: [
{
SHA1: '14d31bde346fcad64ab939f82d195db36701cfcb',
name: 'PR-1',
},
],
},
},
},
lastBuiltRevision: {
SHA1: '6c6b34c0fb91cf077a01fe62d3e8e996b4ea5861',
branch: [
{
SHA1: '14d31bde346fcad64ab939f82d195db36701cfcb',
name: 'PR-1',
},
],
},
remoteUrls: ['https://github.com/backstage/backstage.git'],
scmName: '',
},
{
_class: 'hudson.plugins.git.util.BuildData',
buildsByBranchName: {
master: {
_class: 'hudson.plugins.git.util.Build',
buildNumber: 5,
buildResult: null,
marked: {
SHA1: '14d31bde346fcad64ab939f82d195db36701cfcb',
branch: [
{
SHA1: '14d31bde346fcad64ab939f82d195db36701cfcb',
name: 'master',
},
],
},
revision: {
SHA1: '6c6b34c0fb91cf077a01fe62d3e8e996b4ea5861',
branch: [
{
SHA1: '14d31bde346fcad64ab939f82d195db36701cfcb',
name: 'master',
},
],
},
},
},
lastBuiltRevision: {
SHA1: '6c6b34c0fb91cf077a01fe62d3e8e996b4ea5861',
branch: [
{
SHA1: '14d31bde346fcad64ab939f82d195db36701cfcb',
name: 'master',
},
],
},
remoteUrls: ['https://github.com/backstage/backstage.git'],
scmName: '',
},
{},
{},
{
_class: 'hudson.tasks.junit.TestResultAction',
failCount: 2,
skipCount: 1,
totalCount: 635,
urlName: 'testReport',
},
{},
{},
{
_class:
'org.jenkinsci.plugins.pipeline.modeldefinition.actions.RestartDeclarativePipelineAction',
restartEnabled: false,
restartableStages: [],
},
{},
],
timestamp: 1,
building: false,
duration: 10,
result: 'success',
displayName: '#7',
fullDisplayName: 'Example jobName » Example Build #7',
url:
'https://jenkins.example.com/job/example-jobName/job/exampleBuild/7/',
number: 7,
},
};
it('augments project', async () => {
mockedJenkinsClient.job.get.mockResolvedValueOnce({
jobs: [projectWithScmActions],
});
const result = await jenkinsApi.getProjects(jenkinsInfo);
expect(result).toHaveLength(1);
expect(result[0].status).toEqual('success');
});
it('augments build', async () => {
mockedJenkinsClient.job.get.mockResolvedValueOnce({
jobs: [projectWithScmActions],
});
const result = await jenkinsApi.getProjects(jenkinsInfo);
expect(result).toHaveLength(1);
// TODO: I am really just asserting the previous behaviour wth no understanding here.
// In my 2 Jenkins instances, 1 returns a lot of different and confusing BuildData sections and 1 returns none ☹️
expect(result[0].lastBuild.source).toEqual({
branchName: 'master',
commit: {
hash: '14d31bde',
},
url: 'https://github.com/backstage/backstage/pull/1',
displayName: 'Add LICENSE, CoC etc',
author: 'Mr. T User',
});
});
it('finds test report', async () => {
mockedJenkinsClient.job.get.mockResolvedValueOnce({
jobs: [projectWithScmActions],
});
const result = await jenkinsApi.getProjects(jenkinsInfo);
expect(result).toHaveLength(1);
expect(result[0].lastBuild.tests).toEqual({
total: 635,
passed: 632,
skipped: 1,
failed: 2,
testUrl:
'https://jenkins.example.com/job/example-jobName/job/exampleBuild/7/testReport/',
});
});
});
});
it('getBuild', async () => {
const project: JenkinsProject = {
actions: [],
displayName: 'Example Build',
fullDisplayName: 'Example jobName » Example Build',
fullName: 'example-jobName/exampleBuild',
inQueue: false,
lastBuild: {
actions: [],
timestamp: 1,
building: false,
duration: 10,
result: 'success',
displayName: '#7',
fullDisplayName: 'Example jobName » Example Build #7',
url: 'https://jenkins.example.com/job/example-jobName/job/exampleBuild',
number: 7,
},
};
const build: JenkinsBuild = {
actions: [],
timestamp: 1,
building: false,
duration: 10,
result: 'success',
fullDisplayName: 'example-jobName/exampleBuild',
displayName: 'exampleBuild',
url: `https://jenkins.example.com/job/example-jobName/job/exampleBuild/build/${buildNumber}`,
number: buildNumber,
};
mockedJenkinsClient.job.get.mockResolvedValueOnce(project);
mockedJenkinsClient.build.get.mockResolvedValueOnce(build);
await jenkinsApi.getBuild(jenkinsInfo, jobName, buildNumber);
expect(mockedJenkins).toHaveBeenCalledWith({
baseUrl: jenkinsInfo.baseUrl,
headers: jenkinsInfo.headers,
promisify: true,
});
expect(mockedJenkinsClient.job.get).toBeCalledWith({
name: jobName,
depth: 1,
});
expect(mockedJenkinsClient.build.get).toBeCalledWith(jobName, buildNumber);
});
it('buildProject', () => {});
});
@@ -0,0 +1,283 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JenkinsInfo } from './jenkinsInfoProvider';
import jenkins from 'jenkins';
import {
BackstageBuild,
BackstageProject,
JenkinsBuild,
JenkinsProject,
ScmDetails,
} from '../types';
export class JenkinsApiImpl {
private static readonly lastBuildTreeSpec = `lastBuild[
number,
url,
fullDisplayName,
displayName,
building,
result,
timestamp,
duration,
actions[
*[
*[
*[
*
]
]
]
]
],`;
private static readonly jobTreeSpec = `actions[*],
${JenkinsApiImpl.lastBuildTreeSpec}
jobs{0,1},
name,
fullName,
displayName,
fullDisplayName,
inQueue`;
private static readonly jobsTreeSpec = `jobs[
${JenkinsApiImpl.jobTreeSpec}
]{0,50}`;
/**
* Get a list of projects for the given JenkinsInfo.
* @see ../../../jenkins/src/api/JenkinsApi.ts#getProjects
*/
async getProjects(jenkinsInfo: JenkinsInfo, branch?: string) {
const client = await JenkinsApiImpl.getClient(jenkinsInfo);
const projects: BackstageProject[] = [];
if (branch) {
// we have been asked to filter to a single branch.
// Assume jenkinsInfo.jobName is a folder which contains one job per branch.
// TODO: extract a strategy interface for this
const job = await client.job.get({
name: `${jenkinsInfo.jobName}/${branch}`,
tree: JenkinsApiImpl.jobTreeSpec.replace(/\s/g, ''),
});
projects.push(this.augmentProject(job));
} else {
// We aren't filtering
// Assume jenkinsInfo.jobName is a folder which contains one job per branch.
const folder = await client.job.get({
name: jenkinsInfo.jobName,
// Filter only be the information we need, instead of loading all fields.
// Limit to only show the latest build for each job and only load 50 jobs
// at all.
// Whitespaces are only included for readablity here and stripped out
// before sending to Jenkins
tree: JenkinsApiImpl.jobsTreeSpec.replace(/\s/g, ''),
});
// TODO: support this being a project itself.
for (const jobDetails of folder.jobs) {
// for each branch (we assume)
if (jobDetails?.jobs) {
// skipping folders inside folders for now
// TODO: recurse
} else {
projects.push(this.augmentProject(jobDetails));
}
}
}
return projects;
}
/**
* Get a single build.
* @see ../../../jenkins/src/api/JenkinsApi.ts#getBuild
*/
async getBuild(
jenkinsInfo: JenkinsInfo,
jobName: string,
buildNumber: number,
) {
const client = await JenkinsApiImpl.getClient(jenkinsInfo);
const project = await client.job.get({
name: jobName,
depth: 1,
});
const build = await client.build.get(jobName, buildNumber);
const jobScmInfo = JenkinsApiImpl.extractScmDetailsFromJob(project);
return this.augmentBuild(build, jobScmInfo);
}
/**
* Trigger a build of a project
* @see ../../../jenkins/src/api/JenkinsApi.ts#retry
*/
async buildProject(jenkinsInfo: JenkinsInfo, jobName: string) {
const client = await JenkinsApiImpl.getClient(jenkinsInfo);
// looks like the current SDK only supports triggering a new build
// can't see any support for replay (re-running the specific build with the same SCM info)
// Note Jenkins itself has concepts of rebuild and replay on a job.
// The latter should be possible to trigger with a POST to /replay/rebuild
await client.job.build(jobName);
}
// private helper methods
private static async getClient(jenkinsInfo: JenkinsInfo) {
// The typings for the jenkins library are out of date so just cast to any
return jenkins({
baseUrl: jenkinsInfo.baseUrl,
headers: jenkinsInfo.headers,
promisify: true,
}) as any;
}
private augmentProject(project: JenkinsProject): BackstageProject {
let status: string;
if (project.inQueue) {
status = 'queued';
} else if (project.lastBuild.building) {
status = 'running';
} else if (!project.lastBuild.result) {
status = 'unknown';
} else {
status = project.lastBuild.result;
}
const jobScmInfo = JenkinsApiImpl.extractScmDetailsFromJob(project);
return {
...project,
lastBuild: this.augmentBuild(project.lastBuild, jobScmInfo),
status,
// actions: undefined,
};
}
private augmentBuild(
build: JenkinsBuild,
jobScmInfo: ScmDetails | undefined,
): BackstageBuild {
const source =
build.actions
.filter(
(action: any) =>
action._class === 'hudson.plugins.git.util.BuildData',
)
.map((action: any) => {
const [first]: any = Object.values(action.buildsByBranchName);
const branch = first.revision.branch[0];
return {
branchName: branch.name,
commit: {
hash: branch.SHA1.substring(0, 8),
},
};
})
.pop() || {};
if (jobScmInfo) {
source.url = jobScmInfo.url;
source.displayName = jobScmInfo.displayName;
source.author = jobScmInfo.author;
}
let status: string;
if (build.building) {
status = 'running';
} else if (!build.result) {
status = 'unknown';
} else {
status = build.result;
}
return {
...build,
status,
source: source,
tests: this.getTestReport(build),
};
}
private static extractScmDetailsFromJob(
project: JenkinsProject,
): ScmDetails | undefined {
const scmInfo: ScmDetails | undefined = project.actions
.filter(
(action: any) =>
action._class === 'jenkins.scm.api.metadata.ObjectMetadataAction',
)
.map((action: any) => {
return {
url: action?.objectUrl,
// https://javadoc.jenkins.io/plugin/scm-api/jenkins/scm/api/metadata/ObjectMetadataAction.html
// branch name for regular builds, pull request title on pull requests
displayName: action?.objectDisplayName,
};
})
.pop();
if (!scmInfo) {
return undefined;
}
const author = project.actions
.filter(
(action: any) =>
action._class ===
'jenkins.scm.api.metadata.ContributorMetadataAction',
)
.map((action: any) => {
return action.contributorDisplayName;
})
.pop();
if (author) {
scmInfo.author = author;
}
return scmInfo;
}
private getTestReport(
build: JenkinsBuild,
): {
total: number;
passed: number;
skipped: number;
failed: number;
testUrl: string;
} {
return build.actions
.filter(
(action: any) =>
action._class === 'hudson.tasks.junit.TestResultAction',
)
.map((action: any) => {
return {
total: action.totalCount,
passed: action.totalCount - action.failCount - action.skipCount,
skipped: action.skipCount,
failed: action.failCount,
testUrl: `${build.url}${action.urlName}/`,
};
})
.pop();
}
}
+27 -236
View File
@@ -18,48 +18,8 @@ import { errorHandler } from '@backstage/backend-common';
import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import jenkins from 'jenkins';
import {
BackstageBuild,
BackstageProject,
JenkinsBuild,
JenkinsProject,
ScmDetails,
} from '../types';
import { JenkinsInfo, JenkinsInfoProvider } from './jenkinsInfoProvider';
const lastBuildTreeSpec = `lastBuild[
number,
url,
fullDisplayName,
displayName,
building,
result,
timestamp,
duration,
actions[
*[
*[
*[
*
]
]
]
]
],`;
const jobTreeSpec = `actions[*],
${lastBuildTreeSpec}
jobs{0,1},
name,
fullName,
displayName,
fullDisplayName,
inQueue`;
const jobsTreeSpec = `jobs[
${jobTreeSpec}
]{0,50}`;
import { JenkinsInfoProvider } from './jenkinsInfoProvider';
import { JenkinsApiImpl } from './jenkinsApi';
export interface RouterOptions {
logger: Logger;
@@ -71,6 +31,8 @@ export async function createRouter(
): Promise<express.Router> {
const { jenkinsInfoProvider } = options;
const jenkinsApi = new JenkinsApiImpl();
const router = Router();
router.use(express.json());
@@ -79,6 +41,21 @@ export async function createRouter(
async (request, response) => {
const { namespace, kind, name } = request.params;
const branch = request.query.branch;
let branchStr: string | undefined;
if (branch === undefined) {
branchStr = undefined;
} else if (typeof branch === 'string') {
branchStr = branch;
} else {
// this was passed in as something weird -> 400
// https://evanhahn.com/gotchas-with-express-query-parsing-and-how-to-avoid-them/
response
.status(400)
.send('Something was unexpected about the branch queryString');
return;
}
const jenkinsInfo = await jenkinsInfoProvider.getInstance({
entityRef: {
@@ -87,43 +64,7 @@ export async function createRouter(
name,
},
});
const client = await getClient(jenkinsInfo);
const projects: BackstageProject[] = [];
if (branch) {
// we have been asked to filter to a single branch.
// Assume jenkinsInfo.jobName is a folder which contains one job per branch.
// TODO: extract a strategy interface for this
const job = await client.job.get({
name: `${jenkinsInfo.jobName}/${branch}`,
tree: jobTreeSpec.replace(/\s/g, ''),
});
projects.push(augmentProject(job));
} else {
// We aren't filtering
// Assume jenkinsInfo.jobName is a folder which contains one job per branch.
const folder = await client.job.get({
name: jenkinsInfo.jobName,
// Filter only be the information we need, instead of loading all fields.
// Limit to only show the latest build for each job and only load 50 jobs
// at all.
// Whitespaces are only included for readablity here and stripped out
// before sending to Jenkins
tree: jobsTreeSpec.replace(/\s/g, ''),
});
// TODO: support this being a project itself.
for (const jobDetails of folder.jobs) {
// for each branch (we assume)
if (jobDetails?.jobs) {
// skipping folders inside folders for now
// TODO: recurse
} else {
projects.push(augmentProject(jobDetails));
}
}
}
const projects = await jenkinsApi.getProjects(jenkinsInfo, branchStr);
response.send({
projects: projects,
@@ -153,19 +94,14 @@ export async function createRouter(
jobName,
});
const client = await getClient(jenkinsInfo);
const project = await client.job.get({
name: jobName,
depth: 1,
});
const build = await client.build.get(jobName, parseInt(buildNumber, 10));
const jobScmInfo = extractScmDetailsFromJob(project);
const build = await jenkinsApi.getBuild(
jenkinsInfo,
jobName,
parseInt(buildNumber, 10),
);
response.send({
build: augmentBuild(build, jobScmInfo),
build: build,
});
},
);
@@ -184,14 +120,7 @@ export async function createRouter(
jobName,
});
const client = await getClient(jenkinsInfo);
// looks like the current SDK only supports triggering a new build
// can't see any support for replay (re-running the specific build with the same SCM info)
// Note Jenkins itself has concepts of rebuild and replay on a job.
// The latter should be possible to trigger with a POST to /replay/rebuild
await client.job.build(jobName);
await jenkinsApi.buildProject(jenkinsInfo, jobName);
// TODO: return the buildNumber which was started.
response.send({});
@@ -201,141 +130,3 @@ export async function createRouter(
router.use(errorHandler());
return router;
}
function augmentProject(project: JenkinsProject): BackstageProject {
const jobScmInfo = extractScmDetailsFromJob(project);
let status: string;
if (project.inQueue) {
status = 'queued';
} else if (project.lastBuild.building) {
status = 'running';
} else if (!project.lastBuild.result) {
status = 'unknown';
} else {
status = project.lastBuild.result;
}
return {
...project,
lastBuild: augmentBuild(project.lastBuild, jobScmInfo),
status,
// actions: undefined,
};
}
function augmentBuild(
build: JenkinsBuild,
jobScmInfo: ScmDetails | undefined,
): BackstageBuild {
const source =
build.actions
.filter(
(action: any) => action._class === 'hudson.plugins.git.util.BuildData',
)
.map((action: any) => {
const [first]: any = Object.values(action.buildsByBranchName);
const branch = first.revision.branch[0];
return {
branchName: branch.name,
commit: {
hash: branch.SHA1.substring(0, 8),
},
};
})
.pop() || {};
if (jobScmInfo) {
source.url = jobScmInfo.url;
source.displayName = jobScmInfo.displayName;
source.author = jobScmInfo.author;
}
let status: string;
if (build.building) {
status = 'running';
} else if (!build.result) {
status = 'unknown';
} else {
status = build.result;
}
return {
...build,
status,
source: source,
tests: getTestReport(build),
};
}
function extractScmDetailsFromJob(
project: JenkinsProject,
): ScmDetails | undefined {
const scmInfo: ScmDetails | undefined = project.actions
.filter(
(action: any) =>
action._class === 'jenkins.scm.api.metadata.ObjectMetadataAction',
)
.map((action: any) => {
return {
url: action?.objectUrl,
// https://javadoc.jenkins.io/plugin/scm-api/jenkins/scm/api/metadata/ObjectMetadataAction.html
// branch name for regular builds, pull request title on pull requests
displayName: action?.objectDisplayName,
};
})
.pop();
if (!scmInfo) {
return undefined;
}
const author = project.actions
.filter(
(action: any) =>
action._class === 'jenkins.scm.api.metadata.ContributorMetadataAction',
)
.map((action: any) => {
return action.contributorDisplayName;
})
.pop();
if (author) {
scmInfo.author = author;
}
return scmInfo;
}
function getTestReport(
build: JenkinsBuild,
): {
total: number;
passed: number;
skipped: number;
failed: number;
testUrl: string;
} {
return build.actions
.filter(
(action: any) => action._class === 'hudson.tasks.junit.TestResultAction',
)
.map((action: any) => {
return {
total: action.totalCount,
passed: action.totalCount - action.failCount - action.skipCount,
skipped: action.skipCount,
failed: action.failCount,
testUrl: `${build.url}${action.urlName}/`,
};
})
.pop();
}
async function getClient(jenkinsInfo: JenkinsInfo) {
// The typings for the jenkins library are out of date so just cast to any
return jenkins({
baseUrl: jenkinsInfo.baseUrl,
headers: jenkinsInfo.headers,
promisify: true,
}) as any;
}
+2 -2
View File
@@ -67,7 +67,7 @@ export interface CommonProject {
displayName: string;
fullDisplayName: string;
fullName: string;
inQueue: string;
inQueue: boolean;
}
export interface JenkinsProject extends CommonProject {
@@ -75,7 +75,7 @@ export interface JenkinsProject extends CommonProject {
lastBuild: JenkinsBuild;
// read by us from jenkins but not passed to frontend
actions: any;
actions: object[];
}
export interface BackstageProject extends CommonProject {