From 8acfc0d801a1671b4a86cae6bb7cd0d3a8b97f30 Mon Sep 17 00:00:00 2001 From: Maron <98313426+MaronHatoum@users.noreply.github.com> Date: Mon, 29 Aug 2022 23:36:14 +0300 Subject: [PATCH] feat(dependency dashboard): add option to open all prs (#16959) Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> Co-authored-by: Michael Kriese <michael.kriese@visualon.de> --- .../dependency-dashboard-with-8-PR.txt | 6 +- .../dependency-dashboard.spec.ts.snap | 1 + .../repository/dependency-dashboard.spec.ts | 172 ++++++++++++++++++ .../repository/dependency-dashboard.ts | 79 +++++++- .../repository/update/branch/index.spec.ts | 64 +++++++ lib/workers/repository/update/branch/index.ts | 11 +- 6 files changed, 325 insertions(+), 8 deletions(-) diff --git a/lib/workers/repository/__fixtures__/dependency-dashboard-with-8-PR.txt b/lib/workers/repository/__fixtures__/dependency-dashboard-with-8-PR.txt index 9402c5d054..a357ade8c8 100644 --- a/lib/workers/repository/__fixtures__/dependency-dashboard-with-8-PR.txt +++ b/lib/workers/repository/__fixtures__/dependency-dashboard-with-8-PR.txt @@ -6,6 +6,7 @@ These branches will be created by Renovate only once you click their checkbox be - [ ] <!-- approve-branch=branchName1 -->pr1 - [ ] <!-- approve-branch=branchName2 -->pr2 + - [ ] <!-- approve-all-pending-prs -->🔠**Create all pending approval PRs at once** 🔠## Awaiting Schedule @@ -14,12 +15,13 @@ These updates are awaiting their schedule. Click on a checkbox to get an update - [ ] <!-- unschedule-branch=branchName3 -->pr3 - [ ] <!-- unschedule-branch=branchName4 -->pr4 -## Rate Limited +## Rate-Limited -These updates are currently rate limited. Click on a checkbox below to force their creation now. +These updates are currently rate-limited. Click on a checkbox below to force their creation now. - [ ] <!-- unlimit-branch=branchName5 -->pr5 - [ ] <!-- unlimit-branch=branchName6 -->pr6 + - [ ] <!-- create-all-rate-limited-prs -->🔠**Create all rate-limited PRs at once** 🔠## Errored diff --git a/lib/workers/repository/__snapshots__/dependency-dashboard.spec.ts.snap b/lib/workers/repository/__snapshots__/dependency-dashboard.spec.ts.snap index 2974a346ed..9bd3295445 100644 --- a/lib/workers/repository/__snapshots__/dependency-dashboard.spec.ts.snap +++ b/lib/workers/repository/__snapshots__/dependency-dashboard.spec.ts.snap @@ -509,6 +509,7 @@ These branches will be created by Renovate only once you click their checkbox be - [ ] <!-- approve-branch=branchName1 -->pr1 - [ ] <!-- approve-branch=branchName2 -->pr2 + - [ ] <!-- approve-all-pending-prs -->🔠**Create all pending approval PRs at once** 🔠## Awaiting Schedule diff --git a/lib/workers/repository/dependency-dashboard.spec.ts b/lib/workers/repository/dependency-dashboard.spec.ts index 3aa3921801..712f4a3659 100644 --- a/lib/workers/repository/dependency-dashboard.spec.ts +++ b/lib/workers/repository/dependency-dashboard.spec.ts @@ -19,6 +19,7 @@ import { GitHubMaxPrBodyLen, massageMarkdown, } from '../../modules/platform/github'; +import { regEx } from '../../util/regex'; import { BranchConfig, BranchResult, BranchUpgradeConfig } from '../types'; import * as dependencyDashboard from './dependency-dashboard'; import { PackageFiles } from './package-files'; @@ -92,6 +93,8 @@ describe('workers/repository/dependency-dashboard', () => { }); await dependencyDashboard.readDashboardBody(conf); expect(conf).toEqual({ + dependencyDashboardAllPending: false, + dependencyDashboardAllRateLimited: false, dependencyDashboardChecks: { branchName1: 'approve', }, @@ -101,6 +104,58 @@ describe('workers/repository/dependency-dashboard', () => { prCreation: 'approval', }); }); + + it('reads dashboard body all pending approval', async () => { + const conf: RenovateConfig = {}; + conf.prCreation = 'approval'; + platform.findIssue.mockResolvedValueOnce({ + title: '', + number: 1, + body: Fixtures.get('dependency-dashboard-with-8-PR.txt').replace( + '- [ ] <!-- approve-all-pending-prs -->', + '- [x] <!-- approve-all-pending-prs -->' + ), + }); + await dependencyDashboard.readDashboardBody(conf); + expect(conf).toEqual({ + dependencyDashboardChecks: { + branchName1: 'approve', + branchName2: 'approve', + }, + dependencyDashboardIssue: 1, + dependencyDashboardRebaseAllOpen: false, + dependencyDashboardTitle: 'Dependency Dashboard', + prCreation: 'approval', + dependencyDashboardAllPending: true, + dependencyDashboardAllRateLimited: false, + }); + }); + + it('reads dashboard body open all rate-limited', async () => { + const conf: RenovateConfig = {}; + conf.prCreation = 'approval'; + platform.findIssue.mockResolvedValueOnce({ + title: '', + number: 1, + body: Fixtures.get('dependency-dashboard-with-8-PR.txt').replace( + '- [ ] <!-- create-all-rate-limited-prs -->', + '- [x] <!-- create-all-rate-limited-prs -->' + ), + }); + await dependencyDashboard.readDashboardBody(conf); + expect(conf).toEqual({ + dependencyDashboardChecks: { + branchName5: 'unlimit', + branchName6: 'unlimit', + }, + dependencyDashboardIssue: 1, + dependencyDashboardRebaseAllOpen: false, + dependencyDashboardTitle: 'Dependency Dashboard', + prCreation: 'approval', + dependencyDashboardAllPending: false, + dependencyDashboardAllRateLimited: true, + }); + }); }); describe('ensureDependencyDashboard()', () => { @@ -527,6 +582,123 @@ describe('workers/repository/dependency-dashboard', () => { expect(platform.ensureIssue.mock.calls[0][0].body).toMatchSnapshot(); }); + it('dependency Dashboard All Pending Approval', async () => { + const branches: BranchConfig[] = [ + { + ...mock<BranchConfig>(), + prTitle: 'pr1', + upgrades: [{ ...mock<BranchUpgradeConfig>(), depName: 'dep1' }], + result: BranchResult.NeedsApproval, + branchName: 'branchName1', + }, + { + ...mock<BranchConfig>(), + prTitle: 'pr2', + upgrades: [{ ...mock<BranchUpgradeConfig>(), depName: 'dep2' }], + result: BranchResult.NeedsApproval, + branchName: 'branchName2', + }, + ]; + config.dependencyDashboard = true; + config.dependencyDashboardChecks = { + branchName1: 'approve-branch', + branchName2: 'approve-branch', + }; + config.dependencyDashboardIssue = 1; + jest.spyOn(platform, 'getIssue').mockResolvedValueOnce({ + title: 'Dependency Dashboard', + body: `This issue contains a list of Renovate updates and their statuses. + + ## Pending Approval + + These branches will be created by Renovate only once you click their checkbox below. + + - [ ] <!-- approve-branch=branchName1 -->pr1 + - [ ] <!-- approve-branch=branchName2 -->pr2 + - [x] <!-- approve-all-pending-prs -->🔠**Create all pending approval PRs at once** ðŸ”`, + }); + await dependencyDashboard.ensureDependencyDashboard(config, branches); + const checkApprovePendingSelectAll = regEx( + / - \[ ] <!-- approve-all-pending-prs -->/g + ); + const checkApprovePendingBranch1 = regEx( + / - \[ ] <!-- approve-branch=branchName1 -->pr1/g + ); + const checkApprovePendingBranch2 = regEx( + / - \[ ] <!-- approve-branch=branchName2 -->pr2/g + ); + expect( + checkApprovePendingSelectAll.test( + platform.ensureIssue.mock.calls[0][0].body + ) + ).toBeTrue(); + expect( + checkApprovePendingBranch1.test( + platform.ensureIssue.mock.calls[0][0].body + ) + ).toBeTrue(); + expect( + checkApprovePendingBranch2.test( + platform.ensureIssue.mock.calls[0][0].body + ) + ).toBeTrue(); + }); + + it('dependency Dashboard Open All rate-limited', async () => { + const branches: BranchConfig[] = [ + { + ...mock<BranchConfig>(), + prTitle: 'pr1', + upgrades: [{ ...mock<BranchUpgradeConfig>(), depName: 'dep1' }], + result: BranchResult.BranchLimitReached, + branchName: 'branchName1', + }, + { + ...mock<BranchConfig>(), + prTitle: 'pr2', + upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep2' }], + result: BranchResult.PrLimitReached, + branchName: 'branchName2', + }, + ]; + config.dependencyDashboard = true; + config.dependencyDashboardChecks = { + branchName1: 'unlimit-branch', + branchName2: 'unlimit-branch', + }; + config.dependencyDashboardIssue = 1; + jest.spyOn(platform, 'getIssue').mockResolvedValueOnce({ + title: 'Dependency Dashboard', + body: `This issue contains a list of Renovate updates and their statuses. + ## Rate-limited + These updates are currently rate-limited. Click on a checkbox below to force their creation now. + - [x] <!-- create-all-rate-limited-prs -->**Open all rate-limited PRs** + - [ ] <!-- unlimit-branch=branchName1 -->pr1 + - [ ] <!-- unlimit-branch=branchName2 -->pr2`, + }); + await dependencyDashboard.ensureDependencyDashboard(config, branches); + const checkRateLimitedSelectAll = regEx( + / - \[ ] <!-- create-all-rate-limited-prs -->/g + ); + const checkRateLimitedBranch1 = regEx( + / - \[ ] <!-- unlimit-branch=branchName1 -->pr1/g + ); + const checkRateLimitedBranch2 = regEx( + / - \[ ] <!-- unlimit-branch=branchName2 -->pr2/g + ); + expect( + checkRateLimitedSelectAll.test( + platform.ensureIssue.mock.calls[0][0].body + ) + ).toBeTrue(); + expect( + checkRateLimitedBranch1.test(platform.ensureIssue.mock.calls[0][0].body) + ).toBeTrue(); + expect( + checkRateLimitedBranch2.test(platform.ensureIssue.mock.calls[0][0].body) + ).toBeTrue(); + }); + it('rechecks branches', async () => { const branches: BranchConfig[] = [ { diff --git a/lib/workers/repository/dependency-dashboard.ts b/lib/workers/repository/dependency-dashboard.ts index ffa718a4d8..0d1919cd13 100644 --- a/lib/workers/repository/dependency-dashboard.ts +++ b/lib/workers/repository/dependency-dashboard.ts @@ -16,27 +16,86 @@ import { PackageFiles } from './package-files'; interface DependencyDashboard { dependencyDashboardChecks: Record<string, string>; dependencyDashboardRebaseAllOpen: boolean; + dependencyDashboardAllPending: boolean; + dependencyDashboardAllRateLimited: boolean; +} + +const rateLimitedRe = regEx( + ' - \\[ \\] <!-- unlimit-branch=([^\\s]+) -->', + 'g' +); +const pendingApprovalRe = regEx( + ' - \\[ \\] <!-- approve-branch=([^\\s]+) -->', + 'g' +); +const generalBranchRe = regEx(' <!-- ([a-zA-Z]+)-branch=([^\\s]+) -->'); +const markedBranchesRe = regEx( + ' - \\[x\\] <!-- ([a-zA-Z]+)-branch=([^\\s]+) -->', + 'g' +); + +function checkOpenAllRateLimitedPR(issueBody: string): boolean { + return issueBody.includes(' - [x] <!-- create-all-rate-limited-prs -->'); +} + +function checkApproveAllPendingPR(issueBody: string): boolean { + return issueBody.includes(' - [x] <!-- approve-all-pending-prs -->'); } function checkRebaseAll(issueBody: string): boolean { return issueBody.includes(' - [x] <!-- rebase-all-open-prs -->'); } +function selectAllRelevantBranches(issueBody: string): string[] { + const checkedBranches = []; + if (checkOpenAllRateLimitedPR(issueBody)) { + for (const match of issueBody.matchAll(rateLimitedRe)) { + checkedBranches.push(match[0]); + } + } + if (checkApproveAllPendingPR(issueBody)) { + for (const match of issueBody.matchAll(pendingApprovalRe)) { + checkedBranches.push(match[0]); + } + } + return checkedBranches; +} + +function getAllSelectedBranches( + issueBody: string, + dependencyDashboardChecks: Record<string, string> +): Record<string, string> { + const allRelevantBranches = selectAllRelevantBranches(issueBody); + for (const branch of allRelevantBranches) { + const [, type, branchName] = generalBranchRe.exec(branch)!; + dependencyDashboardChecks[branchName] = type; + } + return dependencyDashboardChecks; +} + function getCheckedBranches(issueBody: string): Record<string, string> { - const checkMatch = /- \[x\] <!-- ([a-zA-Z]+)-branch=([^\s]+) -->/g; - const dependencyDashboardChecks: Record<string, string> = {}; - for (const [, type, branchName] of issueBody.matchAll(regEx(checkMatch))) { + let dependencyDashboardChecks: Record<string, string> = {}; + for (const [, type, branchName] of issueBody.matchAll(markedBranchesRe)) { dependencyDashboardChecks[branchName] = type; } + dependencyDashboardChecks = getAllSelectedBranches( + issueBody, + dependencyDashboardChecks + ); return dependencyDashboardChecks; } function parseDashboardIssue(issueBody: string): DependencyDashboard { const dependencyDashboardChecks = getCheckedBranches(issueBody); const dependencyDashboardRebaseAllOpen = checkRebaseAll(issueBody); + const dependencyDashboardAllPending = checkApproveAllPendingPR(issueBody); + const dependencyDashboardAllRateLimited = + checkOpenAllRateLimitedPR(issueBody); return { dependencyDashboardChecks, dependencyDashboardRebaseAllOpen, + dependencyDashboardAllPending, + dependencyDashboardAllRateLimited, }; } @@ -175,6 +234,11 @@ export async function ensureDependencyDashboard( for (const branch of pendingApprovals) { issueBody += getListItem(branch, 'approve'); } + if (pendingApprovals.length > 1) { + issueBody += ' - [ ] '; + issueBody += '<!-- approve-all-pending-prs -->'; + issueBody += '🔠**Create all pending approval PRs at once** ðŸ”\n'; + } issueBody += '\n'; } const awaitingSchedule = branches.filter( @@ -196,12 +260,17 @@ export async function ensureDependencyDashboard( branch.result === BranchResult.CommitLimitReached ); if (rateLimited.length) { - issueBody += '## Rate Limited\n\n'; + issueBody += '## Rate-Limited\n\n'; issueBody += - 'These updates are currently rate limited. Click on a checkbox below to force their creation now.\n\n'; + 'These updates are currently rate-limited. Click on a checkbox below to force their creation now.\n\n'; for (const branch of rateLimited) { issueBody += getListItem(branch, 'unlimit'); } + if (rateLimited.length > 1) { + issueBody += ' - [ ] '; + issueBody += '<!-- create-all-rate-limited-prs -->'; + issueBody += '🔠**Create all rate-limited PRs at once** ðŸ”\n'; + } issueBody += '\n'; } const errorList = branches.filter( diff --git a/lib/workers/repository/update/branch/index.spec.ts b/lib/workers/repository/update/branch/index.spec.ts index 9f8d8e6f6d..75be7c8be6 100644 --- a/lib/workers/repository/update/branch/index.spec.ts +++ b/lib/workers/repository/update/branch/index.spec.ts @@ -1818,5 +1818,69 @@ describe('workers/repository/update/branch/index', () => { 'No package files need updating' ); }); + + it('Dependency Dashboard All Pending approval', async () => { + jest.spyOn(getUpdated, 'getUpdatedPackageFiles').mockResolvedValueOnce({ + updatedPackageFiles: [{}], + artifactErrors: [{}], + } as PackageFilesResult); + npmPostExtract.getAdditionalFiles.mockResolvedValueOnce({ + artifactErrors: [], + updatedArtifacts: [partial<FileChange>({})], + } as WriteExistingFilesResult); + git.branchExists.mockReturnValue(true); + platform.getBranchPr.mockResolvedValueOnce({ + title: 'pending!', + state: PrState.Open, + bodyStruct: { + hash: hashBody(`- [x] <!-- approve-all-pending-prs -->`), + rebaseRequested: false, + }, + } as Pr); + git.getBranchCommit.mockReturnValue('123test'); + expect( + await branchWorker.processBranch({ + ...config, + dependencyDashboardAllPending: true, + }) + ).toEqual({ + branchExists: true, + commitSha: '123test', + prNo: undefined, + result: 'done', + }); + }); + + it('Dependency Dashboard open all rate-limited', async () => { + jest.spyOn(getUpdated, 'getUpdatedPackageFiles').mockResolvedValueOnce({ + updatedPackageFiles: [{}], + artifactErrors: [{}], + } as PackageFilesResult); + npmPostExtract.getAdditionalFiles.mockResolvedValueOnce({ + artifactErrors: [], + updatedArtifacts: [partial<FileChange>({})], + } as WriteExistingFilesResult); + git.branchExists.mockReturnValue(true); + platform.getBranchPr.mockResolvedValueOnce({ + title: 'unlimited!', + state: PrState.Open, + bodyStruct: { + hash: hashBody(`- [x] <!-- create-all-rate-limited-prs -->`), + rebaseRequested: false, + }, + } as Pr); + git.getBranchCommit.mockReturnValue('123test'); + expect( + await branchWorker.processBranch({ + ...config, + dependencyDashboardAllRateLimited: true, + }) + ).toEqual({ + branchExists: true, + commitSha: '123test', + prNo: undefined, + result: 'done', + }); + }); }); }); diff --git a/lib/workers/repository/update/branch/index.ts b/lib/workers/repository/update/branch/index.ts index 5b91b64490..d9d7082c49 100644 --- a/lib/workers/repository/update/branch/index.ts +++ b/lib/workers/repository/update/branch/index.ts @@ -365,10 +365,19 @@ export async function processBranch( dependencyDashboardCheck === 'rebase' || !!config.dependencyDashboardRebaseAllOpen || !!config.rebaseRequested; - + const userApproveAllPendingPR = !!config.dependencyDashboardAllPending; + const userOpenAllRateLimtedPR = !!config.dependencyDashboardAllRateLimited; if (userRebaseRequested) { logger.debug('Manual rebase requested via Dependency Dashboard'); config.reuseExistingBranch = false; + } else if (userApproveAllPendingPR) { + logger.debug( + 'A user manually approved all pending PRs via the Dependency Dashboard.' + ); + } else if (userOpenAllRateLimtedPR) { + logger.debug( + 'A user manually approved all rate-limited PRs via the Dependency Dashboard.' + ); } else if (branchExists && config.rebaseWhen === 'never') { logger.debug('rebaseWhen=never so skipping branch update check'); return { -- GitLab