From 874782422123e8b83f5ed428489e2fe10f24c859 Mon Sep 17 00:00:00 2001 From: planeiii Date: Thu, 16 Jun 2022 14:49:36 -0500 Subject: [PATCH 1/6] feat(jenkins-backend): add multiple branch support Signed-off-by: planeiii --- .changeset/many-vans-thank.md | 8 ++++++ .../src/service/jenkinsApi.test.ts | 28 +++++++++++++++++++ .../jenkins-backend/src/service/jenkinsApi.ts | 13 +++++---- plugins/jenkins/README.md | 7 +++-- 4 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 .changeset/many-vans-thank.md diff --git a/.changeset/many-vans-thank.md b/.changeset/many-vans-thank.md new file mode 100644 index 0000000000..942aa4cc7c --- /dev/null +++ b/.changeset/many-vans-thank.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-jenkins': patch +'@backstage/plugin-jenkins-backend': patch +--- + +--- + +feature: added support for multiple branches to the `JenkinsApi` diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts index 29da414470..ad1a09a40f 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts @@ -134,6 +134,34 @@ describe('JenkinsApi', () => { }); expect(result).toHaveLength(1); }); + + it('supports multiple branches', async () => { + mockedJenkinsClient.job.get.mockResolvedValue(project); + + const result = await jenkinsApi.getProjects( + jenkinsInfo, + 'foo,bar,catpants', + ); + + expect(mockedJenkins).toHaveBeenCalledWith({ + baseUrl: jenkinsInfo.baseUrl, + headers: jenkinsInfo.headers, + promisify: true, + }); + expect(mockedJenkinsClient.job.get).toBeCalledWith({ + name: `${jenkinsInfo.jobFullName}/foo`, + tree: expect.anything(), + }); + expect(mockedJenkinsClient.job.get).toBeCalledWith({ + name: `${jenkinsInfo.jobFullName}/bar`, + tree: expect.anything(), + }); + expect(mockedJenkinsClient.job.get).toBeCalledWith({ + name: `${jenkinsInfo.jobFullName}/catpants`, + tree: expect.anything(), + }); + expect(result).toHaveLength(1); + }); }); describe('augmented', () => { const projectWithScmActions: JenkinsProject = { diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.ts b/plugins/jenkins-backend/src/service/jenkinsApi.ts index 8069f57692..07469dc83a 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.ts @@ -75,13 +75,16 @@ export class JenkinsApiImpl { const projects: BackstageProject[] = []; if (branch) { - // we have been asked to filter to a single branch. // Assume jenkinsInfo.jobFullName is a folder which contains one job per branch. // TODO: extract a strategy interface for this - const job = await client.job.get({ - name: `${jenkinsInfo.jobFullName}/${branch}`, - tree: JenkinsApiImpl.jobTreeSpec.replace(/\s/g, ''), - }); + const job = await Promise.any( + branch.split(/,/g).map(name => + client.job.get({ + name: `${jenkinsInfo.jobFullName}/${name}`, + tree: JenkinsApiImpl.jobTreeSpec.replace(/\s/g, ''), + }), + ), + ); projects.push(this.augmentProject(job)); } else { // We aren't filtering diff --git a/plugins/jenkins/README.md b/plugins/jenkins/README.md index 80e39d2095..2a0ec451c8 100644 --- a/plugins/jenkins/README.md +++ b/plugins/jenkins/README.md @@ -19,7 +19,7 @@ yarn add --cwd packages/app @backstage/plugin-jenkins 3. Add the `EntityJenkinsContent` extension to the `CI/CD` page and `EntityLatestJenkinsRunCard` to the `overview` page in the app (or wherever you'd prefer): -Note that if you configured a custom JenkinsInfoProvider in step 2, you may need a custom isJenkinsAvailable. +Note that if you configured a custom JenkinsInfoProvider in step 2, you may need a custom isJenkinsAvailable. Also if you're transitioning to a new default branch name, you can pass multiple branch names as a comma-separated list and it will check for each branch name. ```tsx // In packages/app/src/components/catalog/EntityPage.tsx @@ -38,7 +38,10 @@ const serviceEntityPage = ( - + {/* ... */} From 4764e30cc8db91835c8a0ba6e446463e42018d8c Mon Sep 17 00:00:00 2001 From: planeiii Date: Mon, 20 Jun 2022 11:54:09 -0500 Subject: [PATCH 2/6] chore: updated getProjects to accept arrays of branches Signed-off-by: planeiii --- .changeset/many-vans-thank.md | 2 -- .../jenkins-backend/src/service/jenkinsApi.test.ts | 14 +++++++------- plugins/jenkins-backend/src/service/jenkinsApi.ts | 8 ++++---- plugins/jenkins-backend/src/service/router.ts | 8 ++++---- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/.changeset/many-vans-thank.md b/.changeset/many-vans-thank.md index 942aa4cc7c..16f8c8a69b 100644 --- a/.changeset/many-vans-thank.md +++ b/.changeset/many-vans-thank.md @@ -3,6 +3,4 @@ '@backstage/plugin-jenkins-backend': patch --- ---- - feature: added support for multiple branches to the `JenkinsApi` diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts index ad1a09a40f..bada5e1c29 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts @@ -118,10 +118,9 @@ describe('JenkinsApi', () => { it('standard github layout', async () => { mockedJenkinsClient.job.get.mockResolvedValueOnce(project); - const result = await jenkinsApi.getProjects( - jenkinsInfo, + const result = await jenkinsApi.getProjects(jenkinsInfo, [ 'testBranchName', - ); + ]); expect(mockedJenkins).toHaveBeenCalledWith({ baseUrl: jenkinsInfo.baseUrl, @@ -138,10 +137,11 @@ describe('JenkinsApi', () => { it('supports multiple branches', async () => { mockedJenkinsClient.job.get.mockResolvedValue(project); - const result = await jenkinsApi.getProjects( - jenkinsInfo, - 'foo,bar,catpants', - ); + const result = await jenkinsApi.getProjects(jenkinsInfo, [ + 'foo', + 'bar', + 'catpants', + ]); expect(mockedJenkins).toHaveBeenCalledWith({ baseUrl: jenkinsInfo.baseUrl, diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.ts b/plugins/jenkins-backend/src/service/jenkinsApi.ts index 07469dc83a..334be5dfa9 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.ts @@ -70,17 +70,17 @@ export class JenkinsApiImpl { * Get a list of projects for the given JenkinsInfo. * @see ../../../jenkins/src/api/JenkinsApi.ts#getProjects */ - async getProjects(jenkinsInfo: JenkinsInfo, branch?: string) { + async getProjects(jenkinsInfo: JenkinsInfo, branches?: string[]) { const client = await JenkinsApiImpl.getClient(jenkinsInfo); const projects: BackstageProject[] = []; - if (branch) { + if (branches) { // Assume jenkinsInfo.jobFullName is a folder which contains one job per branch. // TODO: extract a strategy interface for this const job = await Promise.any( - branch.split(/,/g).map(name => + branches.map(branch => client.job.get({ - name: `${jenkinsInfo.jobFullName}/${name}`, + name: `${jenkinsInfo.jobFullName}/${branch}`, tree: JenkinsApiImpl.jobTreeSpec.replace(/\s/g, ''), }), ), diff --git a/plugins/jenkins-backend/src/service/router.ts b/plugins/jenkins-backend/src/service/router.ts index 2c9e268165..b4c06314e1 100644 --- a/plugins/jenkins-backend/src/service/router.ts +++ b/plugins/jenkins-backend/src/service/router.ts @@ -64,12 +64,12 @@ export async function createRouter( request.header('authorization'), ); const branch = request.query.branch; - let branchStr: string | undefined; + let branches: string[] | undefined; if (branch === undefined) { - branchStr = undefined; + branches = undefined; } else if (typeof branch === 'string') { - branchStr = branch; + branches = branch.split(/,/g); } else { // this was passed in as something weird -> 400 // https://evanhahn.com/gotchas-with-express-query-parsing-and-how-to-avoid-them/ @@ -88,7 +88,7 @@ export async function createRouter( }, backstageToken: token, }); - const projects = await jenkinsApi.getProjects(jenkinsInfo, branchStr); + const projects = await jenkinsApi.getProjects(jenkinsInfo, branches); response.json({ projects: projects, From 119f075cd8bbedae665fb9d8bfe224c4d7974f0f Mon Sep 17 00:00:00 2001 From: planeiii Date: Wed, 22 Jun 2022 10:12:28 -0500 Subject: [PATCH 3/6] chore: added Promise.any polyfill for Node v14 Signed-off-by: planeiii --- .../jenkins-backend/src/service/jenkinsApi.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.ts b/plugins/jenkins-backend/src/service/jenkinsApi.ts index 334be5dfa9..8de2609037 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.ts @@ -30,6 +30,23 @@ import { import { jenkinsExecutePermission } from '@backstage/plugin-jenkins-common'; import { NotAllowedError } from '@backstage/errors'; +// polyfill Promise.any for Node v14 +const anyPromise = (any => { + if (any) { + // bind is required by tsc + return any.bind(Promise); + } + + // reverse promise resolution (rejects become resolves and visa versa) + const reverse = (promise: Promise) => + new Promise((resolve, reject) => + Promise.resolve(promise).then(reject, resolve), + ); + + return (iterable: Promise[]) => + reverse(Promise.all([...iterable].map(reverse))); +})(Promise.any); + export class JenkinsApiImpl { private static readonly lastBuildTreeSpec = `lastBuild[ number, @@ -77,7 +94,7 @@ export class JenkinsApiImpl { if (branches) { // Assume jenkinsInfo.jobFullName is a folder which contains one job per branch. // TODO: extract a strategy interface for this - const job = await Promise.any( + const job = await anyPromise( branches.map(branch => client.job.get({ name: `${jenkinsInfo.jobFullName}/${branch}`, From 64af1b47563c0ef8e70a80098788cb0595dd5656 Mon Sep 17 00:00:00 2001 From: planeiii Date: Fri, 24 Jun 2022 08:56:30 -0500 Subject: [PATCH 4/6] chore: added types to Promise.any polyfill Signed-off-by: planeiii --- plugins/jenkins-backend/src/service/jenkinsApi.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.ts b/plugins/jenkins-backend/src/service/jenkinsApi.ts index 8de2609037..eaeff9096c 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.ts @@ -38,13 +38,18 @@ const anyPromise = (any => { } // reverse promise resolution (rejects become resolves and visa versa) - const reverse = (promise: Promise) => - new Promise((resolve, reject) => + const reverse = (promise: Promise): Promise => + new Promise((resolve, reject) => Promise.resolve(promise).then(reject, resolve), ); - return (iterable: Promise[]) => - reverse(Promise.all([...iterable].map(reverse))); + return ( + iterable: Iterable>, + ): Promise => + Promise.all([...iterable].map(reverse)).then( + results => reverse(Promise.resolve(results[0])), + reverse, + ); })(Promise.any); export class JenkinsApiImpl { From 5a36f20cc655ea5b4e42646fd7808d059e027680 Mon Sep 17 00:00:00 2001 From: planeiii Date: Tue, 28 Jun 2022 09:07:28 -0500 Subject: [PATCH 5/6] chore: added standards-compliant Promise.any implementation Signed-off-by: planeiii --- plugins/jenkins-backend/package.json | 1 + .../jenkins-backend/src/service/jenkinsApi.ts | 28 ++++--------------- yarn.lock | 5 ++++ 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index 1ab98c3255..46e9ed78a0 100644 --- a/plugins/jenkins-backend/package.json +++ b/plugins/jenkins-backend/package.json @@ -37,6 +37,7 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "jenkins": "^0.28.1", + "promise-any-polyfill": "^1.0.1", "winston": "^3.2.1", "yn": "^4.0.0" }, diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.ts b/plugins/jenkins-backend/src/service/jenkinsApi.ts index eaeff9096c..ae2fa0f7b5 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.ts @@ -30,27 +30,11 @@ import { import { jenkinsExecutePermission } from '@backstage/plugin-jenkins-common'; import { NotAllowedError } from '@backstage/errors'; -// polyfill Promise.any for Node v14 -const anyPromise = (any => { - if (any) { - // bind is required by tsc - return any.bind(Promise); - } - - // reverse promise resolution (rejects become resolves and visa versa) - const reverse = (promise: Promise): Promise => - new Promise((resolve, reject) => - Promise.resolve(promise).then(reject, resolve), - ); - - return ( - iterable: Iterable>, - ): Promise => - Promise.all([...iterable].map(reverse)).then( - results => reverse(Promise.resolve(results[0])), - reverse, - ); -})(Promise.any); +if (!Promise.any) { + (async () => { + await import('promise-any-polyfill'); + })(); +} export class JenkinsApiImpl { private static readonly lastBuildTreeSpec = `lastBuild[ @@ -99,7 +83,7 @@ export class JenkinsApiImpl { if (branches) { // Assume jenkinsInfo.jobFullName is a folder which contains one job per branch. // TODO: extract a strategy interface for this - const job = await anyPromise( + const job = await Promise.any( branches.map(branch => client.job.get({ name: `${jenkinsInfo.jobFullName}/${branch}`, diff --git a/yarn.lock b/yarn.lock index bd2b1ff7bc..8d7fe5fa30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20902,6 +20902,11 @@ promise-all-reject-late@^1.0.0: resolved "https://registry.npmjs.org/promise-all-reject-late/-/promise-all-reject-late-1.0.1.tgz#f8ebf13483e5ca91ad809ccc2fcf25f26f8643c2" integrity sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw== +promise-any-polyfill@^1.0.1: + version "1.0.1" + resolved "https://artifactory.thousandeyes.com/artifactory/api/npm/npm/promise-any-polyfill/-/promise-any-polyfill-1.0.1.tgz#563872dd0abd370bfa8039e63980469b4b8b0d53" + integrity sha1-Vjhy3Qq9Nwv6gDnmOYBGm0uLDVM= + promise-call-limit@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/promise-call-limit/-/promise-call-limit-1.0.1.tgz#4bdee03aeb85674385ca934da7114e9bcd3c6e24" From 94a4c24fed612732e4559c169ea8c08d865551b3 Mon Sep 17 00:00:00 2001 From: planeiii Date: Thu, 30 Jun 2022 09:52:56 -0500 Subject: [PATCH 6/6] chore: correct yarn.lock registry entry Signed-off-by: planeiii --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8d7fe5fa30..6c67be2830 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6502,7 +6502,7 @@ resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== -"@types/react-dom@*", "@types/react-dom@<18.0.0", "@types/react-dom@^17": +"@types/react-dom@*", "@types/react-dom@<18.0.0": version "17.0.17" resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.17.tgz#2e3743277a793a96a99f1bf87614598289da68a1" integrity sha512-VjnqEmqGnasQKV0CWLevqMTXBYG9GbwuE6x3VetERLh0cq2LTptFE73MrQi2S7GkKXCf2GgwItB/melLnxfnsg== @@ -20904,8 +20904,8 @@ promise-all-reject-late@^1.0.0: promise-any-polyfill@^1.0.1: version "1.0.1" - resolved "https://artifactory.thousandeyes.com/artifactory/api/npm/npm/promise-any-polyfill/-/promise-any-polyfill-1.0.1.tgz#563872dd0abd370bfa8039e63980469b4b8b0d53" - integrity sha1-Vjhy3Qq9Nwv6gDnmOYBGm0uLDVM= + resolved "https://registry.npmjs.org/promise-any-polyfill/-/promise-any-polyfill-1.0.1.tgz#563872dd0abd370bfa8039e63980469b4b8b0d53" + integrity sha512-51Tj+hOoV3LOeCHItmWBklN9FYkyV53uIIjjzgIchitGneTvl6+ivOxWXhxraeUzzS3YFRpXRPPbv7+KD3NzYQ== promise-call-limit@^1.0.1: version "1.0.1" @@ -26012,7 +26012,7 @@ ws@^7.3.1, ws@^7.4.6: resolved "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz#9e0ac77ee50af70d58326ecff7e85eb3fa375e67" integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A== -ws@^8.0.0, ws@^8.3.0: +ws@^8.3.0: version "8.8.0" resolved "https://registry.npmjs.org/ws/-/ws-8.8.0.tgz#8e71c75e2f6348dbf8d78005107297056cb77769" integrity sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==