0
0
Fork 0
mirror of https://github.com/renovatebot/renovate.git synced 2025-03-16 00:54:53 +00:00

feat(dependency dashboard): add option to open all prs ()

Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com>
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
This commit is contained in:
Maron 2022-08-29 23:36:14 +03:00 committed by GitHub
parent 70a49def74
commit 8acfc0d801
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 326 additions and 9 deletions

View file

@ -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

View file

@ -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

View file

@ -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[] = [
{

View file

@ -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 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))) {
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> {
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(

View file

@ -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',
});
});
});
});

View file

@ -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 {