diff --git a/.changeset/many-vans-thank.md b/.changeset/many-vans-thank.md new file mode 100644 index 0000000000..16f8c8a69b --- /dev/null +++ b/.changeset/many-vans-thank.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-jenkins': patch +'@backstage/plugin-jenkins-backend': patch +--- + +feature: added support for multiple branches to the `JenkinsApi` diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index e6c5d22e53..948907a1e2 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.test.ts b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts index 29da414470..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, @@ -134,6 +133,35 @@ 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..ae2fa0f7b5 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.ts @@ -30,6 +30,12 @@ import { import { jenkinsExecutePermission } from '@backstage/plugin-jenkins-common'; import { NotAllowedError } from '@backstage/errors'; +if (!Promise.any) { + (async () => { + await import('promise-any-polyfill'); + })(); +} + export class JenkinsApiImpl { private static readonly lastBuildTreeSpec = `lastBuild[ number, @@ -70,18 +76,21 @@ 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) { - // we have been asked to filter to a single 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 client.job.get({ - name: `${jenkinsInfo.jobFullName}/${branch}`, - tree: JenkinsApiImpl.jobTreeSpec.replace(/\s/g, ''), - }); + const job = await Promise.any( + branches.map(branch => + client.job.get({ + name: `${jenkinsInfo.jobFullName}/${branch}`, + tree: JenkinsApiImpl.jobTreeSpec.replace(/\s/g, ''), + }), + ), + ); projects.push(this.augmentProject(job)); } else { // We aren't filtering 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, 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 = ( - + {/* ... */} diff --git a/yarn.lock b/yarn.lock index 987cbcd644..4082736f9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6787,7 +6787,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== @@ -21223,6 +21223,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://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" resolved "https://registry.npmjs.org/promise-call-limit/-/promise-call-limit-1.0.1.tgz#4bdee03aeb85674385ca934da7114e9bcd3c6e24" @@ -26308,7 +26313,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==