mirror of
https://github.com/renovatebot/renovate.git
synced 2025-03-16 17:13:37 +00:00
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>
This commit is contained in:
parent
70a49def74
commit
8acfc0d801
6 changed files with 326 additions and 9 deletions
lib/workers/repository
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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[] = [
|
||||
{
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue