Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions workspaces/jenkins/.changeset/great-spies-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@backstage-community/plugin-scaffolder-backend-module-jenkins': minor
'@backstage-community/plugin-jenkins-backend': minor
'@backstage-community/plugin-jenkins-common': minor
---

Replace the deprecated `jenkins` NPM package with a built-in, light-weight client.
2 changes: 0 additions & 2 deletions workspaces/jenkins/plugins/jenkins-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
"@types/express": "^4.17.6",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"jenkins": "^1.0.0",
"node-fetch": "^2.6.7",
"yn": "^4.0.0"
},
Expand All @@ -61,7 +60,6 @@
"@backstage/cli": "backstage:^",
"@backstage/plugin-auth-backend": "backstage:^",
"@backstage/plugin-auth-backend-module-guest-provider": "backstage:^",
"@types/jenkins": "^1.0.0",
"@types/node-fetch": "^2.5.12",
"@types/supertest": "^6.0.0"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,30 @@
*/

import { JenkinsApiImpl } from './jenkinsApi';
import jenkins from 'jenkins';
import {
Jenkins,
type JenkinsBuild,
} from '@backstage-community/plugin-jenkins-common';
import { JenkinsInfo } from './jenkinsInfoProvider';
import { JenkinsBuild, JenkinsProject } from '../types';
import { JenkinsProject } from '../types';
import { AuthorizeResult } from '@backstage/plugin-permission-common';
import fetch, { Response } from 'node-fetch';
import { mockServices } from '@backstage/backend-test-utils';

jest.mock('jenkins');
jest.mock('@backstage-community/plugin-jenkins-common');
jest.mock('node-fetch');
const mockedJenkinsClient = {
job: {
get: jest.fn(),
build: jest.fn(),
getBuilds: jest.fn(),
},
build: {
get: jest.fn(),
getConsoleText: jest.fn(),
},
};
const mockedJenkins = jenkins as jest.Mocked<any>;
const mockedJenkins = Jenkins as jest.Mocked<any>;
mockedJenkins.mockReturnValue(mockedJenkinsClient);

const resourceRef = 'component:default/example-component';
Expand Down Expand Up @@ -777,9 +782,9 @@ describe('JenkinsApi', () => {
json: async () => {},
} as Response);
await jenkinsApi.getJobBuilds(jenkinsInfo, jobs);
expect(mockFetch).toHaveBeenCalledWith(
'https://jenkins.example.com/job/example-jobName/job/foo/api/json?tree=name,description,url,fullName,displayName,fullDisplayName,inQueue,builds[*]',
{ headers: { headerName: 'headerValue' }, method: 'get' },
expect(mockedJenkinsClient.job.getBuilds).toHaveBeenCalledWith(
['example-jobName', 'foo'],
'name,description,url,fullName,displayName,fullDisplayName,inQueue,builds[*]',
);
});

Expand All @@ -790,21 +795,18 @@ describe('JenkinsApi', () => {
} as Response);
const fullJobName = ['test', 'folder', 'depth', 'foo'];
await jenkinsApi.getJobBuilds(jenkinsInfo, fullJobName);
expect(mockFetch).toHaveBeenCalledWith(
'https://jenkins.example.com/job/test/job/folder/job/depth/job/foo/api/json?tree=name,description,url,fullName,displayName,fullDisplayName,inQueue,builds[*]',
{ headers: { headerName: 'headerValue' }, method: 'get' },
expect(mockedJenkinsClient.job.getBuilds).toHaveBeenCalledWith(
['test', 'folder', 'depth', 'foo'],
'name,description,url,fullName,displayName,fullDisplayName,inQueue,builds[*]',
);
});
});
describe('getBuildConsoleText', () => {
it('should return the console text for a build', async () => {
const mockedConsoleText = 'Build Ran';
mockFetch.mockResolvedValueOnce({
status: 200,
text: async () => {
return mockedConsoleText;
},
} as unknown as Response);
mockedJenkinsClient.build.getConsoleText.mockResolvedValueOnce(
mockedConsoleText,
);

const consoleText = await jenkinsApi.getBuildConsoleText(
jenkinsInfo,
Expand All @@ -813,9 +815,9 @@ describe('JenkinsApi', () => {
);

expect(consoleText).toBe('Build Ran');
expect(mockFetch).toHaveBeenCalledWith(
'https://jenkins.example.com/job/example-jobName/job/foo/19/consoleText',
{ headers: { headerName: 'headerValue' }, method: 'get' },
expect(mockedJenkinsClient.build.getConsoleText).toHaveBeenCalledWith(
['example-jobName', 'foo'],
19,
);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
*/

import type { JenkinsInfo } from './jenkinsInfoProvider';
import Jenkins from 'jenkins';
import {
Jenkins,
type JenkinsBuild,
} from '@backstage-community/plugin-jenkins-common';
import type {
BackstageBuild,
BackstageProject,
JenkinsBuild,
JenkinsProject,
ScmDetails,
} from '../types';
Expand Down Expand Up @@ -225,9 +227,8 @@ export class JenkinsApiImpl {

// 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 new (Jenkins as any)({
private static async getClient(jenkinsInfo: JenkinsInfo): Promise<Jenkins> {
return new Jenkins({
baseUrl: jenkinsInfo.baseUrl,
headers: jenkinsInfo.headers,
promisify: true,
Expand Down Expand Up @@ -379,18 +380,10 @@ export class JenkinsApiImpl {
}

async getJobBuilds(jenkinsInfo: JenkinsInfo, jobs: string[]) {
const response = await fetch(
`${jenkinsInfo.baseUrl}/job/${jobs.join(
'/job/',
)}/api/json?tree=${JenkinsApiImpl.jobBuildsTreeSpec.replace(/\s/g, '')}`,
{
method: 'get',
headers: jenkinsInfo.headers as HeaderInit,
},
);
const client = await JenkinsApiImpl.getClient(jenkinsInfo);
const tree = JenkinsApiImpl.jobBuildsTreeSpec.replace(/\s/g, '');

const jobBuilds = await response.json();
return jobBuilds;
return await client.job.getBuilds(jobs, tree);
}

/**
Expand All @@ -402,14 +395,8 @@ export class JenkinsApiImpl {
jobs: string[],
buildNumber: number,
) {
const buildUrl = this.getBuildUrl(jenkinsInfo, jobs, buildNumber);

const response = await fetch(`${buildUrl}/consoleText`, {
method: 'get',
headers: jenkinsInfo.headers as HeaderInit,
});
const client = await JenkinsApiImpl.getClient(jenkinsInfo);

const consoleText = await response.text();
return consoleText;
return await client.build.getConsoleText(jobs, buildNumber);
}
}
22 changes: 5 additions & 17 deletions workspaces/jenkins/plugins/jenkins-backend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,17 @@
* limitations under the License.
*/

import type {
JenkinsBuild,
CommonBuild,
} from '@backstage-community/plugin-jenkins-common';

export interface ScmDetails {
url?: string;
displayName?: string;
author?: string;
}

interface CommonBuild {
// standard Jenkins
timestamp: number;
building: boolean;
duration: number;
result?: string;
fullDisplayName: string;
displayName: string;
url: string;
number: number;
}

export interface JenkinsBuild extends CommonBuild {
// read by us from jenkins but not passed to frontend
actions: any;
}

/**
* A build as presented by this plugin to the backstage jenkins plugin
*/
Expand Down
4 changes: 3 additions & 1 deletion workspaces/jenkins/plugins/jenkins-common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@
},
"dependencies": {
"@backstage/plugin-catalog-common": "backstage:^",
"@backstage/plugin-permission-common": "backstage:^"
"@backstage/plugin-permission-common": "backstage:^",
"form-data": "^4.0.4",
"node-fetch": "^2.6.7"
},
"devDependencies": {
"@backstage/cli": "backstage:^"
Expand Down
117 changes: 117 additions & 0 deletions workspaces/jenkins/plugins/jenkins-common/report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,128 @@
```ts
import { ResourcePermission } from '@backstage/plugin-permission-common';

// @public (undocumented)
export interface CommonBuild {
// (undocumented)
building: boolean;
// (undocumented)
displayName: string;
// (undocumented)
duration: number;
// (undocumented)
fullDisplayName: string;
// (undocumented)
number: number;
// (undocumented)
result?: string;
// (undocumented)
timestamp: number;
// (undocumented)
url: string;
}

// @public (undocumented)
export interface CrumbData {
// (undocumented)
cookies?: string[];
// (undocumented)
headerName: string;
// (undocumented)
headerValue: string;
}

// @public (undocumented)
export interface CrumbDataHeaderValues {
// (undocumented)
crumb: string;
// (undocumented)
crumbRequestField: string;
}

// @public (undocumented)
export type HeaderValue = string | string[] | undefined;

// @public (undocumented)
export class Jenkins {
constructor(opts: JenkinsClientOptions);
// (undocumented)
build: {
get: (
name: string | string[],
buildNumber: string | number,
) => Promise<JenkinsBuild>;
getConsoleText: (
name: string | string[],
buildNumber: string | number,
) => Promise<string>;
};
// (undocumented)
job: {
get: (input: JobGetOptions) => Promise<any>;
getBuilds: (name: string | string[], tree?: string) => Promise<unknown>;
build: (
name: string | string[],
opts?: JobBuildOptions | undefined,
) => Promise<unknown>;
copy: (name: string | string[], from: string) => Promise<void>;
create: (name: string | string[], xml: string) => Promise<void>;
destroy: (name: string | string[]) => Promise<void>;
enable: (name: string | string[]) => Promise<void>;
disable: (name: string | string[]) => Promise<void>;
};
// (undocumented)
readonly opts: JenkinsClientOptions;
}

// @public (undocumented)
export interface JenkinsBuild extends CommonBuild {
// (undocumented)
actions: any;
}

// @public (undocumented)
export interface JenkinsClientOptions {
// (undocumented)
baseUrl: string;
// (undocumented)
crumbIssuer?: boolean | ((client: any) => Promise<CrumbData>) | undefined;
// (undocumented)
headers?: Record<string, HeaderValue>;
// (undocumented)
promisify?: boolean;
}

// @public
export const jenkinsExecutePermission: ResourcePermission<'catalog-entity'>;

// @public (undocumented)
export type JenkinsParams =
| Record<string, unknown>
| URLSearchParams
| undefined;

// @public
export const jenkinsPermissions: ResourcePermission<'catalog-entity'>[];

// @public (undocumented)
export interface JobBuildOptions {
// (undocumented)
delay?: string;
// (undocumented)
parameters?: JenkinsParams;
// (undocumented)
token?: string;
}

// @public (undocumented)
export interface JobGetOptions {
// (undocumented)
depth?: number;
// (undocumented)
name: string | string[];
// (undocumented)
tree?: string;
}

// (No @packageDocumentation comment for this package)
```
Loading