mirror of
https://github.com/renovatebot/renovate.git
synced 2026-11-03 11:03:39 +00:00
df8c4051ff
* test(core): replace file snapshots with explicit assertions * test(core): replace inline snapshots with explicit assertions * test(core): assert on relevant sections instead of full bodies * test(core): drop assertions implied by full equality * test(core): address review feedback * test(core): use resolves instead of awaiting inside expect
2395 lines
82 KiB
TypeScript
2395 lines
82 KiB
TypeScript
import { ERROR, WARN } from 'bunyan';
|
||
import { codeBlock } from 'common-tags';
|
||
import type { MockedObject } from 'vitest';
|
||
import { vi } from 'vitest';
|
||
import { mock } from 'vitest-mock-extended';
|
||
import { Fixtures } from '~test/fixtures.ts';
|
||
import type { RenovateConfig } from '~test/util.ts';
|
||
import { logger, platform } from '~test/util.ts';
|
||
import { getConfig } from '../../config/defaults.ts';
|
||
import { GlobalConfig } from '../../config/global.ts';
|
||
import { pkg } from '../../expose.ts';
|
||
import type {
|
||
PackageDependency,
|
||
PackageFile,
|
||
} from '../../modules/manager/types.ts';
|
||
import { massageMarkdown } from '../../modules/platform/github/index.ts';
|
||
import type { Platform } from '../../modules/platform/index.ts';
|
||
import { clone } from '../../util/clone.ts';
|
||
import { emojify } from '../../util/emoji.ts';
|
||
import { asTimestamp } from '../../util/timestamp.ts';
|
||
import type { BranchConfig, BranchUpgradeConfig } from '../types.ts';
|
||
import * as dependencyDashboard from './dependency-dashboard.ts';
|
||
import { getDashboardMarkdownVulnerabilities } from './dependency-dashboard.ts';
|
||
import { PackageFiles } from './package-files.ts';
|
||
|
||
const createVulnerabilitiesMock = vi.fn();
|
||
vi.mock('./process/vulnerabilities.ts', () => {
|
||
return {
|
||
__esModule: true,
|
||
Vulnerabilities: class {
|
||
static create() {
|
||
return createVulnerabilitiesMock();
|
||
}
|
||
},
|
||
};
|
||
});
|
||
|
||
type PrUpgrade = BranchUpgradeConfig;
|
||
|
||
const massageMdSpy = platform.massageMarkdown;
|
||
const getIssueSpy = platform.getIssue;
|
||
|
||
let config: BranchConfig;
|
||
|
||
beforeEach(() => {
|
||
GlobalConfig.reset();
|
||
massageMdSpy.mockImplementation(massageMarkdown);
|
||
platform.maxBodyLength.mockReturnValue(60000); // Github Limit
|
||
config = getConfig() as BranchConfig;
|
||
config.errors = [];
|
||
config.warnings = [];
|
||
});
|
||
|
||
function genRandString(length: number): string {
|
||
let result = '';
|
||
const chars = 'abcdefghijklmnopqrstuvwxyz';
|
||
const charsLen = chars.length;
|
||
for (let i = 0; i < length; i++) {
|
||
result += chars.charAt(Math.floor(Math.random() * charsLen));
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function genRandPackageFile(
|
||
depsNum: number,
|
||
depNameLen: number,
|
||
): Record<string, PackageFile[]> {
|
||
const deps: PackageDependency[] = [];
|
||
for (let i = 0; i < depsNum; i++) {
|
||
deps.push({
|
||
depName: genRandString(depNameLen),
|
||
currentValue: '1.0.0',
|
||
});
|
||
}
|
||
return { npm: [{ packageFile: 'package.json', deps }] };
|
||
}
|
||
|
||
async function dryRun(
|
||
branches: BranchConfig[],
|
||
platform: MockedObject<Platform>,
|
||
ensureIssueClosingCalls: number,
|
||
ensureIssueCalls: number,
|
||
) {
|
||
GlobalConfig.set({ dryRun: 'full' });
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssueClosing).toHaveBeenCalledTimes(
|
||
ensureIssueClosingCalls,
|
||
);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(ensureIssueCalls);
|
||
}
|
||
|
||
describe('workers/repository/dependency-dashboard', () => {
|
||
describe('readDashboardBody()', () => {
|
||
it('parses invalid dashboard body without throwing error', async () => {
|
||
const conf: RenovateConfig = {};
|
||
conf.prCreation = 'approval';
|
||
platform.findIssue.mockResolvedValueOnce({
|
||
title: '',
|
||
number: 1,
|
||
body: null as never,
|
||
});
|
||
await dependencyDashboard.readDashboardBody(conf);
|
||
expect(conf).toEqual({
|
||
dependencyDashboardChecks: {
|
||
configMigrationCheckboxState: 'no-checkbox',
|
||
},
|
||
dependencyDashboardAllAwaitingSchedule: false,
|
||
dependencyDashboardAllPending: false,
|
||
dependencyDashboardAllRateLimited: false,
|
||
dependencyDashboardIssue: 1,
|
||
dependencyDashboardRebaseAllOpen: false,
|
||
dependencyDashboardTitle: 'Dependency Dashboard',
|
||
prCreation: 'approval',
|
||
});
|
||
});
|
||
|
||
it('reads dashboard body', async () => {
|
||
const conf: RenovateConfig = {};
|
||
conf.prCreation = 'approval';
|
||
platform.findIssue.mockResolvedValueOnce({
|
||
title: '',
|
||
number: 1,
|
||
body: `${Fixtures.get('dependency-dashboard-with-10-PR.txt').replace(
|
||
'- [ ]',
|
||
'- [x]',
|
||
)}\n\n - [x] <!-- rebase-all-open-prs -->`,
|
||
});
|
||
await dependencyDashboard.readDashboardBody(conf);
|
||
expect(conf).toEqual({
|
||
dependencyDashboardAllAwaitingSchedule: false,
|
||
dependencyDashboardAllPending: false,
|
||
dependencyDashboardAllRateLimited: false,
|
||
dependencyDashboardChecks: {
|
||
branchName1: 'approve',
|
||
configMigrationCheckboxState: 'no-checkbox',
|
||
},
|
||
dependencyDashboardIssue: 1,
|
||
dependencyDashboardRebaseAllOpen: true,
|
||
dependencyDashboardTitle: 'Dependency Dashboard',
|
||
prCreation: 'approval',
|
||
});
|
||
});
|
||
|
||
it('reads dashboard body and apply checkedBranches', async () => {
|
||
GlobalConfig.set({ checkedBranches: ['branch1', 'branch2'] });
|
||
const conf: RenovateConfig = {};
|
||
conf.prCreation = 'approval';
|
||
platform.findIssue.mockResolvedValueOnce({
|
||
title: '',
|
||
number: 1,
|
||
body: Fixtures.get('dependency-dashboard-with-10-PR.txt'),
|
||
});
|
||
await dependencyDashboard.readDashboardBody(conf);
|
||
expect(conf).toEqual({
|
||
dependencyDashboardAllAwaitingSchedule: false,
|
||
dependencyDashboardAllPending: false,
|
||
dependencyDashboardAllRateLimited: false,
|
||
dependencyDashboardChecks: {
|
||
branch1: 'global-config',
|
||
branch2: 'global-config',
|
||
configMigrationCheckboxState: 'no-checkbox',
|
||
},
|
||
dependencyDashboardIssue: 1,
|
||
dependencyDashboardRebaseAllOpen: false,
|
||
dependencyDashboardTitle: '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-10-PR.txt').replace(
|
||
'- [ ] <!-- approve-all-pending-prs -->',
|
||
'- [x] <!-- approve-all-pending-prs -->',
|
||
),
|
||
});
|
||
await dependencyDashboard.readDashboardBody(conf);
|
||
expect(conf).toEqual({
|
||
dependencyDashboardChecks: {
|
||
branchName1: 'approve',
|
||
branchName2: 'approve',
|
||
configMigrationCheckboxState: 'no-checkbox',
|
||
},
|
||
dependencyDashboardIssue: 1,
|
||
dependencyDashboardAllAwaitingSchedule: false,
|
||
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-10-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',
|
||
configMigrationCheckboxState: 'no-checkbox',
|
||
},
|
||
dependencyDashboardIssue: 1,
|
||
dependencyDashboardAllAwaitingSchedule: false,
|
||
dependencyDashboardRebaseAllOpen: false,
|
||
dependencyDashboardTitle: 'Dependency Dashboard',
|
||
prCreation: 'approval',
|
||
dependencyDashboardAllPending: false,
|
||
dependencyDashboardAllRateLimited: true,
|
||
});
|
||
});
|
||
|
||
it('reads dashboard body open all awaiting schedule', async () => {
|
||
const conf: RenovateConfig = {};
|
||
conf.prCreation = 'approval';
|
||
platform.findIssue.mockResolvedValueOnce({
|
||
title: '',
|
||
number: 1,
|
||
body: Fixtures.get('dependency-dashboard-with-10-PR.txt').replace(
|
||
'- [ ] <!-- create-all-awaiting-schedule-prs -->',
|
||
'- [x] <!-- create-all-awaiting-schedule-prs -->',
|
||
),
|
||
});
|
||
await dependencyDashboard.readDashboardBody(conf);
|
||
expect(conf).toEqual({
|
||
dependencyDashboardChecks: {
|
||
branchName3: 'unschedule',
|
||
branchName4: 'unschedule',
|
||
configMigrationCheckboxState: 'no-checkbox',
|
||
},
|
||
dependencyDashboardIssue: 1,
|
||
dependencyDashboardAllAwaitingSchedule: true,
|
||
dependencyDashboardRebaseAllOpen: false,
|
||
dependencyDashboardTitle: 'Dependency Dashboard',
|
||
prCreation: 'approval',
|
||
dependencyDashboardAllPending: false,
|
||
dependencyDashboardAllRateLimited: false,
|
||
});
|
||
});
|
||
|
||
it('reads dashboard body and config migration checkbox - checked', async () => {
|
||
const conf: RenovateConfig = {};
|
||
conf.prCreation = 'approval';
|
||
platform.findIssue.mockResolvedValueOnce({
|
||
title: '',
|
||
number: 1,
|
||
body: '\n\n - [x] <!-- create-config-migration-pr -->',
|
||
});
|
||
await dependencyDashboard.readDashboardBody(conf);
|
||
expect(conf.dependencyDashboardChecks).toEqual({
|
||
configMigrationCheckboxState: 'checked',
|
||
});
|
||
});
|
||
|
||
it('reads dashboard body and config migration checkbox - unchecked', async () => {
|
||
const conf: RenovateConfig = {};
|
||
conf.prCreation = 'approval';
|
||
platform.findIssue.mockResolvedValueOnce({
|
||
title: '',
|
||
number: 1,
|
||
body: '\n\n - [ ] <!-- create-config-migration-pr -->',
|
||
});
|
||
await dependencyDashboard.readDashboardBody(conf);
|
||
expect(conf.dependencyDashboardChecks).toEqual({
|
||
configMigrationCheckboxState: 'unchecked',
|
||
});
|
||
});
|
||
|
||
it('reads dashboard body and config migration pr link', async () => {
|
||
const conf: RenovateConfig = {};
|
||
conf.prCreation = 'approval';
|
||
platform.findIssue.mockResolvedValueOnce({
|
||
title: '',
|
||
number: 1,
|
||
body: '\n\n <!-- config-migration-pr-info -->',
|
||
});
|
||
await dependencyDashboard.readDashboardBody(conf);
|
||
expect(conf.dependencyDashboardChecks).toEqual({
|
||
configMigrationCheckboxState: 'migration-pr-exists',
|
||
});
|
||
});
|
||
|
||
it('does not read dashboard body but applies checkedBranches regardless', async () => {
|
||
GlobalConfig.set({ checkedBranches: ['branch1', 'branch2'] });
|
||
const conf: RenovateConfig = {};
|
||
conf.dependencyDashboard = false;
|
||
await dependencyDashboard.readDashboardBody(conf);
|
||
expect(conf).toEqual({
|
||
dependencyDashboard: false,
|
||
dependencyDashboardAllAwaitingSchedule: false,
|
||
dependencyDashboardAllPending: false,
|
||
dependencyDashboardAllRateLimited: false,
|
||
dependencyDashboardChecks: {
|
||
branch1: 'global-config',
|
||
branch2: 'global-config',
|
||
},
|
||
dependencyDashboardRebaseAllOpen: false,
|
||
});
|
||
});
|
||
|
||
it('applies rebaseAllOpenBranches without dashboard', async () => {
|
||
GlobalConfig.set({ rebaseAllOpenBranches: true });
|
||
const conf: RenovateConfig = {};
|
||
conf.dependencyDashboard = false;
|
||
await dependencyDashboard.readDashboardBody(conf);
|
||
expect(conf).toEqual({
|
||
dependencyDashboard: false,
|
||
dependencyDashboardAllAwaitingSchedule: false,
|
||
dependencyDashboardAllPending: false,
|
||
dependencyDashboardAllRateLimited: false,
|
||
dependencyDashboardChecks: {},
|
||
dependencyDashboardRebaseAllOpen: true,
|
||
});
|
||
});
|
||
|
||
it('reads dashboard body and group size not met branches', async () => {
|
||
const conf: RenovateConfig = {};
|
||
conf.prCreation = 'approval';
|
||
platform.findIssue.mockResolvedValueOnce({
|
||
title: '',
|
||
number: 1,
|
||
body: `
|
||
- [x] <!-- approveGroup-branch=groupedBranch1 -->
|
||
- [ ] <!-- approveGroup-branch=groupedBranch2 -->
|
||
`,
|
||
});
|
||
await dependencyDashboard.readDashboardBody(conf);
|
||
expect(conf.dependencyDashboardChecks).toMatchObject({
|
||
groupedBranch1: 'approveGroup',
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('ensureDependencyDashboard()', () => {
|
||
beforeEach(() => {
|
||
PackageFiles.add('main', null);
|
||
GlobalConfig.reset();
|
||
logger.getProblems.mockReturnValue([]);
|
||
});
|
||
|
||
it('does nothing if mode=silent', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
config.mode = 'silent';
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssueClosing).toHaveBeenCalledTimes(0);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(0);
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 0, 0);
|
||
});
|
||
|
||
it('do nothing if dependencyDashboard is disabled', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssueClosing).toHaveBeenCalledTimes(1);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(0);
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 1, 0);
|
||
});
|
||
|
||
it('do nothing if it has no dependencyDashboardApproval branches', async () => {
|
||
const branches = [
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr1',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr2',
|
||
dependencyDashboardApproval: false,
|
||
},
|
||
];
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssueClosing).toHaveBeenCalledTimes(1);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(0);
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 1, 0);
|
||
});
|
||
|
||
it('closes Dependency Dashboard when there is 0 PR opened and dependencyDashboardAutoclose is true', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
config.dependencyDashboard = true;
|
||
config.dependencyDashboardAutoclose = true;
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssueClosing).toHaveBeenCalledTimes(1);
|
||
expect(platform.ensureIssueClosing.mock.calls[0][0]).toBe(
|
||
config.dependencyDashboardTitle,
|
||
);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(0);
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 1, 0);
|
||
});
|
||
|
||
it('closes Dependency Dashboard when all branches are automerged and dependencyDashboardAutoclose is true', async () => {
|
||
const branches: BranchConfig[] = [
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr1',
|
||
result: 'automerged',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr2',
|
||
result: 'automerged',
|
||
dependencyDashboardApproval: false,
|
||
},
|
||
];
|
||
config.dependencyDashboard = true;
|
||
config.dependencyDashboardAutoclose = true;
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssueClosing).toHaveBeenCalledTimes(1);
|
||
expect(platform.ensureIssueClosing.mock.calls[0][0]).toBe(
|
||
config.dependencyDashboardTitle,
|
||
);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(0);
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 1, 0);
|
||
});
|
||
|
||
it('open or update Dependency Dashboard when all branches are closed and dependencyDashboardAutoclose is false', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
config.dependencyDashboard = true;
|
||
config.dependencyDashboardHeader = 'This is a header';
|
||
config.dependencyDashboardFooter = 'And this is a footer';
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssueClosing).toHaveBeenCalledTimes(0);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
expect(platform.ensureIssue.mock.calls[0][0].title).toBe(
|
||
config.dependencyDashboardTitle,
|
||
);
|
||
const body = platform.ensureIssue.mock.calls[0][0].body;
|
||
expect(body).toStartWith('This is a header\n');
|
||
expect(body).toContain(
|
||
'This repository currently has no open or pending branches.',
|
||
);
|
||
expect(body).toContain('## Detected Dependencies\n\nNone detected');
|
||
expect(body).toEndWith('---\nAnd this is a footer\n');
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 0, 1);
|
||
});
|
||
|
||
it('open or update Dependency Dashboard when rules contain approvals', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
config.repository = 'test';
|
||
config.packageRules = [
|
||
{
|
||
dependencyDashboardApproval: true,
|
||
},
|
||
{},
|
||
];
|
||
config.dependencyDashboardHeader =
|
||
'This is a header for platform:{{platform}}';
|
||
config.dependencyDashboardFooter =
|
||
'And this is a footer for repository:{{repository}}';
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssueClosing).toHaveBeenCalledTimes(0);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
expect(platform.ensureIssue.mock.calls[0][0].title).toBe(
|
||
config.dependencyDashboardTitle,
|
||
);
|
||
const body = platform.ensureIssue.mock.calls[0][0].body;
|
||
expect(body).toStartWith('This is a header for platform:github\n');
|
||
expect(body).toContain(
|
||
'This repository currently has no open or pending branches.',
|
||
);
|
||
expect(body).toContain('## Detected Dependencies\n\nNone detected');
|
||
expect(body).toEndWith('---\nAnd this is a footer for repository:test\n');
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 0, 1);
|
||
});
|
||
|
||
it('supports renovateVersion templating in header and footer', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
config.dependencyDashboard = true;
|
||
config.dependencyDashboardHeader =
|
||
'This is a header for renovateVersion:{{renovateVersion}}';
|
||
config.dependencyDashboardFooter =
|
||
'And this is a footer for renovateVersion:{{renovateVersion}}';
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssue.mock.calls[0][0].body).toMatch(
|
||
`renovateVersion:${pkg.version}`,
|
||
);
|
||
});
|
||
|
||
it('checks an issue with 2 Pending Approvals, 2 not scheduled, 2 pr-hourly-limit-reached, 2 in error, 1 pending automerge and 1 other', async () => {
|
||
const branches: BranchConfig[] = [
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr1',
|
||
upgrades: [{ ...mock<BranchUpgradeConfig>(), depName: 'dep1' }],
|
||
result: 'needs-approval',
|
||
branchName: 'branchName1',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr2',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep2' }],
|
||
result: 'needs-approval',
|
||
branchName: 'branchName2',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr3',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep3' }],
|
||
result: 'not-scheduled',
|
||
branchName: 'branchName3',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr4',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep4' }],
|
||
result: 'not-scheduled',
|
||
branchName: 'branchName4',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr5',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep5' }],
|
||
result: 'pr-limit-reached',
|
||
branchName: 'branchName5',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr6',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep6' }],
|
||
result: 'pr-limit-reached',
|
||
branchName: 'branchName6',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr7',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep7' }],
|
||
result: 'error',
|
||
branchName: 'branchName7',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr8',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep8' }],
|
||
result: 'error',
|
||
branchName: 'branchName8',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr9',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep9' }],
|
||
result: 'done',
|
||
prBlockedBy: 'BranchAutomerge',
|
||
branchName: 'branchName9',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr10',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep10' }],
|
||
result: 'done',
|
||
prBlockedBy: undefined,
|
||
branchName: 'branchName10',
|
||
},
|
||
];
|
||
config.dependencyDashboard = true;
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssueClosing).toHaveBeenCalledTimes(0);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
expect(platform.ensureIssue.mock.calls[0][0].title).toBe(
|
||
config.dependencyDashboardTitle,
|
||
);
|
||
expect(platform.ensureIssue.mock.calls[0][0].body).toBe(
|
||
Fixtures.get('dependency-dashboard-with-10-PR.txt'),
|
||
);
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 0, 1);
|
||
});
|
||
|
||
it('checks an issue with dependency dashboard categories', async () => {
|
||
const branches: BranchConfig[] = [
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr0',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep0' }],
|
||
result: 'needs-approval',
|
||
dependencyDashboardCategory: 'Category #2',
|
||
branchName: 'branchName0',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr1',
|
||
upgrades: [{ ...mock<BranchUpgradeConfig>(), depName: 'dep1' }],
|
||
result: 'needs-approval',
|
||
branchName: 'branchName1',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr2',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep2' }],
|
||
result: 'needs-approval',
|
||
dependencyDashboardCategory: 'Category #10',
|
||
branchName: 'branchName2',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr3',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep3' }],
|
||
result: 'not-scheduled',
|
||
branchName: 'branchName3',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr4',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep4' }],
|
||
result: 'not-scheduled',
|
||
branchName: 'branchName4',
|
||
dependencyDashboardCategory: 'Category #1',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr5',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep5' }],
|
||
result: 'pr-limit-reached',
|
||
branchName: 'branchName5',
|
||
dependencyDashboardCategory: 'Category #3',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr6',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep6' }],
|
||
result: 'pr-limit-reached',
|
||
branchName: 'branchName6',
|
||
dependencyDashboardCategory: 'Category #3',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr7',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep7' }],
|
||
result: 'error',
|
||
branchName: 'branchName7',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr8',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep8' }],
|
||
result: 'error',
|
||
branchName: 'branchName8',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr9',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep9' }],
|
||
result: 'done',
|
||
prBlockedBy: 'BranchAutomerge',
|
||
branchName: 'branchName9',
|
||
},
|
||
];
|
||
config.dependencyDashboard = true;
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssueClosing).toHaveBeenCalledTimes(0);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
const body = platform.ensureIssue.mock.calls[0][0].body;
|
||
expect(body).toContain('## Pending Approval');
|
||
expect(body).toContain(codeBlock`
|
||
### Category #2
|
||
|
||
- [ ] <!-- approve-branch=branchName0 -->pr0
|
||
|
||
### Category #10
|
||
|
||
- [ ] <!-- approve-branch=branchName2 -->pr2
|
||
|
||
### Others
|
||
|
||
- [ ] <!-- approve-branch=branchName1 -->pr1
|
||
|
||
### All
|
||
|
||
- [ ] <!-- approve-all-pending-prs -->🔐 **Create all pending approval PRs at once** 🔐
|
||
`);
|
||
expect(body).toContain('## Awaiting Schedule');
|
||
expect(body).toContain(codeBlock`
|
||
### Category #1
|
||
|
||
- [ ] <!-- unschedule-branch=branchName4 -->pr4
|
||
|
||
### Others
|
||
|
||
- [ ] <!-- unschedule-branch=branchName3 -->pr3
|
||
|
||
### All
|
||
|
||
- [ ] <!-- create-all-awaiting-schedule-prs -->🔐 **Create all awaiting schedule PRs at once** 🔐
|
||
`);
|
||
expect(body).toContain('## Rate-Limited');
|
||
expect(body).toContain(codeBlock`
|
||
### Category #3
|
||
|
||
- [ ] <!-- unlimit-branch=branchName5 -->pr5
|
||
- [ ] <!-- unlimit-branch=branchName6 -->pr6
|
||
|
||
### All
|
||
|
||
- [ ] <!-- create-all-rate-limited-prs -->🔐 **Create all rate-limited PRs at once** 🔐
|
||
`);
|
||
// branches without a category are listed directly, with no `###` heading
|
||
expect(body).toContain(codeBlock`
|
||
## Errored
|
||
|
||
The following updates encountered an error and will be retried. To force a retry now, click on a checkbox below.
|
||
|
||
- [ ] <!-- retry-branch=branchName7 -->pr7
|
||
- [ ] <!-- retry-branch=branchName8 -->pr8
|
||
`);
|
||
expect(body).toContain(codeBlock`
|
||
## Pending Branch Automerge
|
||
|
||
The following updates await pending status checks before automerging. To abort the branch automerge and create a PR instead, click on a checkbox below.
|
||
|
||
- [ ] <!-- approvePr-branch=branchName9 -->pr9
|
||
`);
|
||
expect(body).toContain('## Detected Dependencies\n\nNone detected');
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 0, 1);
|
||
});
|
||
|
||
it('checks an issue with 2 PR pr-edited', async () => {
|
||
const branches: BranchConfig[] = [
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prNo: 1,
|
||
prTitle: 'pr1',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep1' }],
|
||
result: 'pr-edited',
|
||
branchName: 'branchName1',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prNo: 2,
|
||
prTitle: 'pr2',
|
||
upgrades: [
|
||
{ ...mock<PrUpgrade>(), depName: 'dep2' },
|
||
{ ...mock<PrUpgrade>(), depName: 'dep3' },
|
||
],
|
||
result: 'pr-edited',
|
||
branchName: 'branchName2',
|
||
},
|
||
];
|
||
config.dependencyDashboard = true;
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssueClosing).toHaveBeenCalledTimes(0);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
expect(platform.ensureIssue.mock.calls[0][0].title).toBe(
|
||
config.dependencyDashboardTitle,
|
||
);
|
||
expect(platform.ensureIssue.mock.calls[0][0].body).toBe(
|
||
Fixtures.get('dependency-dashboard-with-2-PR-edited.txt'),
|
||
);
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 0, 1);
|
||
});
|
||
|
||
it('checks an issue with 3 PR in progress and rebase all option', async () => {
|
||
const branches: BranchConfig[] = [
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr1',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep1' }],
|
||
result: 'rebase',
|
||
prNo: 1,
|
||
branchName: 'branchName1',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr2',
|
||
prNo: 2,
|
||
upgrades: [
|
||
{ ...mock<PrUpgrade>(), depName: 'dep2' },
|
||
{ ...mock<PrUpgrade>(), depName: 'dep3' },
|
||
],
|
||
result: 'rebase',
|
||
branchName: 'branchName2',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr3',
|
||
prNo: 3,
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep3' }],
|
||
result: 'rebase',
|
||
branchName: 'branchName3',
|
||
},
|
||
];
|
||
config.dependencyDashboard = true;
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssueClosing).toHaveBeenCalledTimes(0);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
expect(platform.ensureIssue.mock.calls[0][0].title).toBe(
|
||
config.dependencyDashboardTitle,
|
||
);
|
||
expect(platform.ensureIssue.mock.calls[0][0].body).toBe(
|
||
Fixtures.get('dependency-dashboard-with-3-PR-in-progress.txt'),
|
||
);
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 0, 1);
|
||
});
|
||
|
||
it('checks an issue with 2 PR closed / ignored', async () => {
|
||
const branches: BranchConfig[] = [
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr1',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep1' }],
|
||
result: 'already-existed',
|
||
branchName: 'branchName1',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr2',
|
||
upgrades: [
|
||
{ ...mock<PrUpgrade>(), depName: 'dep2' },
|
||
{ ...mock<PrUpgrade>(), depName: 'dep3' },
|
||
],
|
||
result: 'already-existed',
|
||
branchName: 'branchName2',
|
||
},
|
||
];
|
||
config.dependencyDashboard = true;
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssueClosing).toHaveBeenCalledTimes(0);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
expect(platform.ensureIssue.mock.calls[0][0].title).toBe(
|
||
config.dependencyDashboardTitle,
|
||
);
|
||
expect(platform.ensureIssue.mock.calls[0][0].body).toBe(
|
||
Fixtures.get('dependency-dashboard-with-2-PR-closed-ignored.txt'),
|
||
);
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 0, 1);
|
||
});
|
||
|
||
it('checks an issue with group size not met branches', async () => {
|
||
const branches: BranchConfig[] = [
|
||
{
|
||
...mock<BranchConfig>(),
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep1' }],
|
||
result: 'minimum-group-size-not-met',
|
||
branchName: 'groupBranch1',
|
||
},
|
||
];
|
||
config.dependencyDashboard = true;
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssueClosing).toHaveBeenCalledTimes(0);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
expect(platform.ensureIssue.mock.calls[0][0].title).toBe(
|
||
config.dependencyDashboardTitle,
|
||
);
|
||
const body = platform.ensureIssue.mock.calls[0][0].body;
|
||
expect(body).toContain(codeBlock`
|
||
## Group Size Not Met
|
||
|
||
The following branches have not met their minimum group size. To create them, click on a checkbox below.
|
||
|
||
- [ ] <!-- approveGroup-branch=groupBranch1 -->undefined
|
||
`);
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 0, 1);
|
||
});
|
||
|
||
it('checks an issue with 3 PR in approval', async () => {
|
||
const branches: BranchConfig[] = [
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr1',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep1' }],
|
||
result: 'needs-pr-approval',
|
||
branchName: 'branchName1',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr2',
|
||
upgrades: [
|
||
{ ...mock<PrUpgrade>(), depName: 'dep2' },
|
||
{ ...mock<PrUpgrade>(), depName: 'dep3' },
|
||
],
|
||
result: 'needs-pr-approval',
|
||
branchName: 'branchName2',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr3',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep3' }],
|
||
result: 'needs-pr-approval',
|
||
branchName: 'branchName3',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr4',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep4' }],
|
||
result: 'pending',
|
||
branchName: 'branchName4',
|
||
},
|
||
];
|
||
config.dependencyDashboard = true;
|
||
config.dependencyDashboardPrApproval = true;
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssueClosing).toHaveBeenCalledTimes(0);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
expect(platform.ensureIssue.mock.calls[0][0].title).toBe(
|
||
config.dependencyDashboardTitle,
|
||
);
|
||
expect(platform.ensureIssue.mock.calls[0][0].body).toBe(
|
||
Fixtures.get('dependency-dashboard-with-3-PR-in-approval.txt'),
|
||
);
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 0, 1);
|
||
});
|
||
|
||
it('adds a checkbox for config migration', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
config.repository = 'test';
|
||
config.packageRules = [
|
||
{
|
||
dependencyDashboardApproval: true,
|
||
},
|
||
{},
|
||
];
|
||
config.dependencyDashboardHeader =
|
||
'This is a header for platform:{{platform}}';
|
||
config.dependencyDashboardFooter =
|
||
'And this is a footer for repository:{{repository}}';
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
undefined,
|
||
{
|
||
result: 'add-checkbox',
|
||
},
|
||
);
|
||
expect(platform.ensureIssueClosing).toHaveBeenCalledTimes(0);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
expect(platform.ensureIssue.mock.calls[0][0].title).toBe(
|
||
config.dependencyDashboardTitle,
|
||
);
|
||
expect(platform.ensureIssue.mock.calls[0][0].body).toMatch(
|
||
' - [ ] <!-- create-config-migration-pr --> Select this checkbox to let Renovate create an automated Config Migration PR.',
|
||
);
|
||
});
|
||
|
||
it('adds config migration pr link when it exists', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
config.repository = 'test';
|
||
config.packageRules = [
|
||
{
|
||
dependencyDashboardApproval: true,
|
||
},
|
||
{},
|
||
];
|
||
config.dependencyDashboardHeader =
|
||
'This is a header for platform:{{platform}}';
|
||
config.dependencyDashboardFooter =
|
||
'And this is a footer for repository:{{repository}}';
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
undefined,
|
||
{
|
||
result: 'pr-exists',
|
||
prNumber: 1,
|
||
},
|
||
);
|
||
expect(platform.ensureIssueClosing).toHaveBeenCalledTimes(0);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
expect(platform.ensureIssue.mock.calls[0][0].title).toBe(
|
||
config.dependencyDashboardTitle,
|
||
);
|
||
expect(platform.ensureIssue.mock.calls[0][0].body).toMatch(
|
||
`## Config Migration Needed\n\n<!-- config-migration-pr-info --> See Config Migration PR:`,
|
||
);
|
||
});
|
||
|
||
it('adds related text when config migration pr has been modified', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
config.repository = 'test';
|
||
config.packageRules = [
|
||
{
|
||
dependencyDashboardApproval: true,
|
||
},
|
||
{},
|
||
];
|
||
config.dependencyDashboardHeader =
|
||
'This is a header for platform:{{platform}}';
|
||
config.dependencyDashboardFooter =
|
||
'And this is a footer for repository:{{repository}}';
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
undefined,
|
||
{
|
||
result: 'pr-modified',
|
||
prNumber: 1,
|
||
},
|
||
);
|
||
expect(platform.ensureIssueClosing).toHaveBeenCalledTimes(0);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
expect(platform.ensureIssue.mock.calls[0][0].title).toBe(
|
||
config.dependencyDashboardTitle,
|
||
);
|
||
expect(platform.ensureIssue.mock.calls[0][0].body).toMatch(
|
||
'The Config Migration branch exists but has been modified by another user. Renovate will not push to this branch unless it is first deleted.',
|
||
);
|
||
});
|
||
|
||
it('does not add a config migration checkbox when not needed', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
config.repository = 'test';
|
||
config.packageRules = [
|
||
{
|
||
dependencyDashboardApproval: true,
|
||
},
|
||
{},
|
||
];
|
||
config.dependencyDashboardHeader =
|
||
'This is a header for platform:{{platform}}';
|
||
config.dependencyDashboardFooter =
|
||
'And this is a footer for repository:{{repository}}';
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssueClosing).toHaveBeenCalledTimes(0);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
expect(platform.ensureIssue.mock.calls[0][0].title).toBe(
|
||
config.dependencyDashboardTitle,
|
||
);
|
||
expect(platform.ensureIssue.mock.calls[0][0].body).not.toMatch(
|
||
'## Config Migration Needed',
|
||
);
|
||
});
|
||
|
||
it('contains logged problems', async () => {
|
||
const branches: BranchConfig[] = [
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr1',
|
||
upgrades: [
|
||
{ ...mock<PrUpgrade>(), depName: 'dep1', repository: 'repo1' },
|
||
],
|
||
result: 'pending',
|
||
branchName: 'branchName1',
|
||
},
|
||
];
|
||
logger.getProblems.mockReturnValueOnce([
|
||
{
|
||
level: ERROR,
|
||
msg: 'everything is broken',
|
||
},
|
||
{
|
||
level: WARN,
|
||
msg: 'just a bit',
|
||
},
|
||
{
|
||
level: ERROR,
|
||
msg: 'i am a duplicated problem',
|
||
},
|
||
{
|
||
level: ERROR,
|
||
msg: 'i am a duplicated problem',
|
||
},
|
||
{
|
||
level: ERROR,
|
||
msg: 'i am a non-duplicated problem',
|
||
},
|
||
{
|
||
level: WARN,
|
||
msg: 'i am a non-duplicated problem',
|
||
},
|
||
{
|
||
level: WARN,
|
||
msg: 'i am an artifact error',
|
||
artifactErrors: {},
|
||
},
|
||
]);
|
||
config.dependencyDashboard = true;
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
const body = platform.ensureIssue.mock.calls[0][0].body;
|
||
// duplicates are collapsed, and problems with artifact errors are omitted
|
||
expect(body).toContain(codeBlock`
|
||
## Repository Problems
|
||
|
||
Renovate tried to run on this repository, but found these problems.
|
||
|
||
- ❌ ERROR: everything is broken
|
||
- ⚠️ WARN: just a bit
|
||
- ❌ ERROR: i am a duplicated problem
|
||
- ❌ ERROR: i am a non-duplicated problem
|
||
- ⚠️ WARN: i am a non-duplicated problem
|
||
`);
|
||
expect(body).not.toContain('i am an artifact error');
|
||
expect(body).toContain('## Pending Status Checks');
|
||
expect(body).toContain(' - [ ] <!-- unpend-branch=branchName1 -->pr1');
|
||
});
|
||
|
||
it('contains logged problems with custom header', async () => {
|
||
const branches: BranchConfig[] = [
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr1',
|
||
upgrades: [
|
||
{ ...mock<PrUpgrade>(), depName: 'dep1', repository: 'repo1' },
|
||
],
|
||
result: 'pending',
|
||
branchName: 'branchName1',
|
||
},
|
||
];
|
||
logger.getProblems.mockReturnValueOnce([
|
||
{
|
||
level: ERROR,
|
||
msg: 'i am a non-duplicated problem',
|
||
},
|
||
]);
|
||
config.dependencyDashboard = true;
|
||
config.customizeDashboard = {
|
||
repoProblemsHeader: 'platform is {{platform}}',
|
||
};
|
||
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
const body = platform.ensureIssue.mock.calls[0][0].body;
|
||
// the templated custom header replaces the default problems header
|
||
expect(body).toContain(codeBlock`
|
||
## Repository Problems
|
||
|
||
platform is github
|
||
|
||
- ❌ ERROR: i am a non-duplicated problem
|
||
`);
|
||
expect(body).not.toContain(
|
||
'Renovate tried to run on this repository, but found these problems.',
|
||
);
|
||
expect(body).toContain('## Pending Status Checks');
|
||
expect(body).toContain(' - [ ] <!-- unpend-branch=branchName1 -->pr1');
|
||
});
|
||
|
||
it('dependency Dashboard All Pending Approval', async () => {
|
||
const branches: BranchConfig[] = [
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr1',
|
||
upgrades: [{ ...mock<BranchUpgradeConfig>(), depName: 'dep1' }],
|
||
result: 'needs-approval',
|
||
branchName: 'branchName1',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr2',
|
||
upgrades: [{ ...mock<BranchUpgradeConfig>(), depName: 'dep2' }],
|
||
result: 'needs-approval',
|
||
branchName: 'branchName2',
|
||
},
|
||
];
|
||
config.dependencyDashboard = true;
|
||
config.dependencyDashboardChecks = {
|
||
branchName1: 'approve-branch',
|
||
branchName2: 'approve-branch',
|
||
};
|
||
config.dependencyDashboardIssue = 1;
|
||
getIssueSpy.mockResolvedValueOnce({
|
||
title: 'Dependency Dashboard',
|
||
body: codeBlock`
|
||
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,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
// all checkboxes are reset to unchecked once the requested action has run
|
||
const body = platform.ensureIssue.mock.calls[0][0].body;
|
||
expect(body).toContain(' - [ ] <!-- approve-all-pending-prs -->');
|
||
expect(body).toContain(' - [ ] <!-- approve-branch=branchName1 -->pr1');
|
||
expect(body).toContain(' - [ ] <!-- approve-branch=branchName2 -->pr2');
|
||
});
|
||
|
||
it('dependency Dashboard Open All rate-limited', async () => {
|
||
const branches: BranchConfig[] = [
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr1',
|
||
upgrades: [{ ...mock<BranchUpgradeConfig>(), depName: 'dep1' }],
|
||
result: 'branch-limit-reached',
|
||
branchName: 'branchName1',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr2',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep2' }],
|
||
result: 'pr-limit-reached',
|
||
branchName: 'branchName2',
|
||
},
|
||
];
|
||
config.dependencyDashboard = true;
|
||
config.dependencyDashboardChecks = {
|
||
branchName1: 'unlimit-branch',
|
||
branchName2: 'unlimit-branch',
|
||
};
|
||
config.dependencyDashboardIssue = 1;
|
||
getIssueSpy.mockResolvedValueOnce({
|
||
title: 'Dependency Dashboard',
|
||
body: codeBlock`
|
||
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,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
// all checkboxes are reset to unchecked once the requested action has run
|
||
const body = platform.ensureIssue.mock.calls[0][0].body;
|
||
expect(body).toContain(' - [ ] <!-- create-all-rate-limited-prs -->');
|
||
expect(body).toContain(' - [ ] <!-- unlimit-branch=branchName1 -->pr1');
|
||
expect(body).toContain(' - [ ] <!-- unlimit-branch=branchName2 -->pr2');
|
||
});
|
||
|
||
it('rechecks branches', async () => {
|
||
const branches: BranchConfig[] = [
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr1',
|
||
upgrades: [{ ...mock<BranchUpgradeConfig>(), depName: 'dep1' }],
|
||
result: 'needs-approval',
|
||
branchName: 'branchName1',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr2',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep2' }],
|
||
result: 'needs-approval',
|
||
branchName: 'branchName2',
|
||
},
|
||
{
|
||
...mock<BranchConfig>(),
|
||
prTitle: 'pr3',
|
||
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep3' }],
|
||
result: 'not-scheduled',
|
||
branchName: 'branchName3',
|
||
},
|
||
];
|
||
config.dependencyDashboard = true;
|
||
config.dependencyDashboardChecks = { branchName2: 'approve-branch' };
|
||
config.dependencyDashboardIssue = 1;
|
||
vi.mocked(platform.getIssue).mockResolvedValueOnce({
|
||
title: 'Dependency Dashboard',
|
||
body: '',
|
||
});
|
||
vi.mocked(platform.getIssue).mockResolvedValueOnce({
|
||
title: 'Dependency Dashboard',
|
||
body: `This issue contains a list of Renovate updates and their statuses.
|
||
|
||
## Pending Approval
|
||
|
||
The following branches are pending approval. To create them, click on a checkbox below.
|
||
|
||
- [ ] <!-- approve-branch=branchName1 -->pr1
|
||
- [x] <!-- approve-branch=branchName2 -->pr2
|
||
|
||
## Awaiting Schedule
|
||
|
||
The following updates are awaiting their schedule. To get an update now, click on a checkbox below.
|
||
|
||
- [x] <!-- unschedule-branch=branchName3 -->pr3
|
||
|
||
- [x] <!-- rebase-all-open-prs -->'
|
||
`,
|
||
});
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
const body = platform.ensureIssue.mock.calls[0][0].body;
|
||
// the rechecked branch is unchecked again, the others keep their state
|
||
expect(body).toContain('## Pending Approval');
|
||
expect(body).toContain(' - [ ] <!-- approve-branch=branchName1 -->pr1');
|
||
expect(body).toContain(' - [ ] <!-- approve-branch=branchName2 -->pr2');
|
||
expect(body).toContain(' - [ ] <!-- approve-all-pending-prs -->');
|
||
expect(body).toContain('## Awaiting Schedule');
|
||
expect(body).toContain(
|
||
' - [x] <!-- unschedule-branch=branchName3 -->pr3',
|
||
);
|
||
expect(body).not.toContain('rebase-all-open-prs');
|
||
});
|
||
|
||
it('skips fetching issue if content unchanged', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
config.dependencyDashboard = true;
|
||
config.dependencyDashboardChecks = {};
|
||
config.dependencyDashboardIssue = 1;
|
||
vi.mocked(platform.getIssue).mockResolvedValueOnce({
|
||
title: 'Dependency Dashboard',
|
||
body: `This issue lists Renovate updates and detected dependencies. Read the [Dependency Dashboard](https://docs.renovatebot.com/key-concepts/dashboard/) docs to learn more.
|
||
|
||
This repository currently has no open or pending branches.
|
||
|
||
## Detected Dependencies
|
||
|
||
None detected
|
||
|
||
`,
|
||
});
|
||
vi.mocked(platform.getIssue).mockResolvedValueOnce({
|
||
title: 'Dependency Dashboard',
|
||
body: '',
|
||
});
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssue).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('forwards configured labels to the ensure issue call', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
config.dependencyDashboard = true;
|
||
config.dependencyDashboardLabels = ['Renovate', 'Maintenance'];
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
expect(platform.ensureIssue.mock.calls[0][0].labels).toStrictEqual([
|
||
'Renovate',
|
||
'Maintenance',
|
||
]);
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 0, 1);
|
||
});
|
||
|
||
describe('checks detected dependencies section', () => {
|
||
const packageFiles = Fixtures.getJson('./package-files.json');
|
||
const packageFilesWithDigest = Fixtures.getJson(
|
||
'./package-files-digest.json',
|
||
);
|
||
let config: RenovateConfig;
|
||
|
||
beforeAll(() => {
|
||
GlobalConfig.reset();
|
||
config = getConfig();
|
||
config.dependencyDashboard = true;
|
||
});
|
||
|
||
describe('single base branch repo', () => {
|
||
beforeEach(() => {
|
||
PackageFiles.clear();
|
||
PackageFiles.add('main', packageFiles);
|
||
});
|
||
|
||
it('add detected dependencies to the Dependency Dashboard body', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
const body = platform.ensureIssue.mock.calls[0][0].body;
|
||
expect(body).toContain('## Detected Dependencies');
|
||
// one manager block in full, to pin down the nesting
|
||
expect(body).toContain(codeBlock`
|
||
<details><summary>dockerfile (1)</summary>
|
||
<blockquote>
|
||
|
||
<details><summary>Dockerfile (1)</summary>
|
||
|
||
- \`ubuntu 20.04\` → [Updates: \`22.04\`]
|
||
|
||
</details>
|
||
|
||
</blockquote>
|
||
</details>
|
||
`);
|
||
expect(body).toContain('<details><summary>npm (1)</summary>');
|
||
expect(body).toContain(
|
||
'<details><summary>package.json (8)</summary>',
|
||
);
|
||
expect(body).toContain(' - `cookie-parser ^1.4.5`');
|
||
expect(body).toContain(' - `express ~4.17.1` → [Updates: `~4.18.0`]');
|
||
expect(body).toContain(' - `express-handlebars >=5.3.4`');
|
||
expect(body).toContain(' - `geoip-lite 1.4.*`');
|
||
expect(body).toContain(' - `nodemailer 6.7.0` → [Updates: `6.7.4`]');
|
||
expect(body).toContain(
|
||
' - `redis 3.1.2-3.4.0` → [Updates: `3.1.1`, `3.1.2`, `4.1.0`]',
|
||
);
|
||
expect(body).toContain(' - `dotenv 10.0.0` → [Updates: `16.0.0`]');
|
||
expect(body).toContain(' - `nodemon 2.0.14` → [Updates: `2.0.16`]');
|
||
expect(body).toContain('<details><summary>poetry (1)</summary>');
|
||
expect(body).toContain(
|
||
'<details><summary>pyproject.toml (1)</summary>',
|
||
);
|
||
expect(body).toContain(' - `six <=1.3.0` → [Updates: `<=1.16.0`]');
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 0, 1);
|
||
});
|
||
|
||
it('show default message in issues body when packageFiles is empty', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
PackageFiles.clear();
|
||
PackageFiles.add('main', {});
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
const body = platform.ensureIssue.mock.calls[0][0].body;
|
||
expect(body).toContain('## Detected Dependencies\n\nNone detected');
|
||
expect(body).not.toContain('<details>');
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 0, 1);
|
||
});
|
||
|
||
it('show default message in issues body when when packageFiles is null', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
PackageFiles.clear();
|
||
PackageFiles.add('main', null);
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
const body = platform.ensureIssue.mock.calls[0][0].body;
|
||
expect(body).toContain('## Detected Dependencies\n\nNone detected');
|
||
expect(body).not.toContain('<details>');
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 0, 1);
|
||
});
|
||
|
||
it('shows different combinations of version+digest for a given dependency', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
PackageFiles.add('main', packageFilesWithDigest);
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
const body = platform.ensureIssue.mock.calls[0][0].body;
|
||
expect(body).toContain('<details><summary>dockerfile (4)</summary>');
|
||
expect(body).toContain(
|
||
'<details><summary>digest-only/Dockerfile (1)</summary>',
|
||
);
|
||
expect(body).toContain(
|
||
' - `ubuntu sha256:06b5d30fabc1fc574f2ecab87375692299d45f8f190d9b71f512deb494114e1f`',
|
||
);
|
||
// a dep with neither version nor digest gets no count and no entry
|
||
expect(body).toContain(codeBlock`
|
||
<details><summary>no-ver-no-digest/Dockerfile</summary>
|
||
|
||
|
||
</details>
|
||
`);
|
||
expect(body).toContain(
|
||
'<details><summary>ver-and-digest/Dockerfile (1)</summary>',
|
||
);
|
||
expect(body).toContain(
|
||
' - `ubuntu 18.04@sha256:a7ed45c4a95fbe19f9c5fb9d1ca58b2431b2a4984754be2f50ccec99d9428b79`',
|
||
);
|
||
expect(body).toContain(
|
||
'<details><summary>ver-only/Dockerfile (1)</summary>',
|
||
);
|
||
expect(body).toContain(' - `ubuntu 20.04`');
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 0, 1);
|
||
});
|
||
|
||
it('shows deprecations', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
const packageFilesWithDeprecations = clone(packageFiles);
|
||
packageFilesWithDeprecations.npm[0].deps[0].deprecationMessage =
|
||
'some deprecation message';
|
||
packageFilesWithDeprecations.npm[0].deps[2].updates.push({
|
||
updateType: 'replacement',
|
||
newName: 'prop-types-tools',
|
||
newValue: '2.17.0',
|
||
branchName: 'renovate/airbnb-prop-types-replacement',
|
||
});
|
||
PackageFiles.add('main', packageFilesWithDeprecations);
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
packageFilesWithDeprecations,
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
expect(platform.ensureIssue.mock.calls[0][0].body).toInclude(
|
||
'The following dependencies are either deprecated or have replacements available.',
|
||
);
|
||
expect(platform.ensureIssue.mock.calls[0][0].body).toInclude(
|
||
'| npm | [cookie-parser](https://redirect.github.com/expressjs/cookie-parser) | ![Unavailable]',
|
||
);
|
||
expect(platform.ensureIssue.mock.calls[0][0].body).toInclude(
|
||
'npm | [express-handlebars](https://redirect.github.com/express-handlebars/express-handlebars) | ![Available]',
|
||
);
|
||
// same with dry run
|
||
await dryRun(branches, platform, 0, 1);
|
||
});
|
||
|
||
it('shows replacement as unavailable when target already exists as sibling', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
const packageFilesWithExistingReplacement: Record<
|
||
string,
|
||
PackageFile[]
|
||
> = {
|
||
npm: [
|
||
{
|
||
packageFile: 'package.json',
|
||
deps: [
|
||
{
|
||
depName: '@material-ui/core',
|
||
deprecationMessage: 'This package is deprecated',
|
||
updates: [
|
||
{
|
||
updateType: 'replacement',
|
||
newName: '@mui/material',
|
||
newValue: '^5.0.0',
|
||
},
|
||
],
|
||
},
|
||
{
|
||
depName: '@material-ui/icons',
|
||
updates: [
|
||
{
|
||
updateType: 'replacement',
|
||
newName: '@mui/icons-material',
|
||
newValue: '^5.0.0',
|
||
},
|
||
],
|
||
},
|
||
{
|
||
depName: '@mui/material',
|
||
updates: [
|
||
{
|
||
newValue: '^6.2.0',
|
||
updateType: 'minor',
|
||
},
|
||
],
|
||
},
|
||
{
|
||
depName: '@mui/icons-material',
|
||
updates: [
|
||
{
|
||
newValue: '^6.2.0',
|
||
updateType: 'minor',
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
],
|
||
};
|
||
PackageFiles.add('main', packageFilesWithExistingReplacement);
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
packageFilesWithExistingReplacement,
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
const body = platform.ensureIssue.mock.calls[0][0].body;
|
||
expect(body).toInclude('Deprecations / Replacements');
|
||
expect(body).toInclude('@material-ui/core');
|
||
expect(body).toInclude('@material-ui/icons');
|
||
expect(body).not.toInclude(
|
||
'',
|
||
);
|
||
expect(body).toInclude(
|
||
'',
|
||
);
|
||
});
|
||
|
||
it('handles missing version/digest values correctly', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
const packageFilesWithMissingVersions = {
|
||
npm: [
|
||
{
|
||
packageFile: 'package.json',
|
||
deps: [
|
||
{
|
||
depName: 'dep-with-version-only',
|
||
currentValue: '1.0.0',
|
||
},
|
||
{
|
||
depName: 'dep-with-digest-only',
|
||
currentDigest: 'sha256:1234567890',
|
||
},
|
||
{
|
||
depName: 'dep-with-version-and-digest',
|
||
currentValue: '2.0.0',
|
||
currentDigest: 'sha256:0987654321',
|
||
},
|
||
{
|
||
depName: 'dep-with-locked-version-only',
|
||
lockedVersion: '3.0.0',
|
||
},
|
||
{
|
||
depName: 'dep-with-no-version-info',
|
||
},
|
||
],
|
||
},
|
||
],
|
||
};
|
||
PackageFiles.add('main', packageFilesWithMissingVersions);
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
packageFilesWithMissingVersions,
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
const dashboardBody = platform.ensureIssue.mock.calls[0][0].body;
|
||
|
||
// Version only case
|
||
expect(dashboardBody).toInclude('`dep-with-version-only 1.0.0`');
|
||
|
||
// Digest only case
|
||
expect(dashboardBody).toInclude(
|
||
'`dep-with-digest-only sha256:1234567890`',
|
||
);
|
||
|
||
// Version and digest case
|
||
expect(dashboardBody).toInclude(
|
||
'`dep-with-version-and-digest 2.0.0@sha256:0987654321`',
|
||
);
|
||
|
||
// Locked version fallback case
|
||
expect(dashboardBody).toInclude(
|
||
'`dep-with-locked-version-only lock file @ 3.0.0`',
|
||
);
|
||
|
||
// No version info case
|
||
expect(dashboardBody).toInclude(
|
||
'`dep-with-no-version-info unknown version`',
|
||
);
|
||
|
||
// Verify no 'undefined' appears in the output
|
||
expect(dashboardBody).not.toInclude('undefined');
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 0, 1);
|
||
});
|
||
});
|
||
|
||
describe('multi base branch repo', () => {
|
||
beforeEach(() => {
|
||
PackageFiles.clear();
|
||
PackageFiles.add('main', packageFiles);
|
||
PackageFiles.add('dev', packageFiles);
|
||
});
|
||
|
||
it('add detected dependencies to the Dependency Dashboard body', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
const body = platform.ensureIssue.mock.calls[0][0].body;
|
||
expect(body).toContain('## Detected Dependencies');
|
||
// dependencies are grouped per base branch, base branches sorted
|
||
const devIndex = body.indexOf(
|
||
'<details><summary>Branch dev</summary>',
|
||
);
|
||
const mainIndex = body.indexOf(
|
||
'<details><summary>Branch main</summary>',
|
||
);
|
||
expect(devIndex).toBeGreaterThan(-1);
|
||
expect(mainIndex).toBeGreaterThan(devIndex);
|
||
// both base branches list the same dependencies
|
||
for (const section of [
|
||
body.slice(devIndex, mainIndex),
|
||
body.slice(mainIndex),
|
||
]) {
|
||
expect(section).toContain(
|
||
'<details><summary>dockerfile (1)</summary>',
|
||
);
|
||
expect(section).toContain(
|
||
'<details><summary>Dockerfile (1)</summary>',
|
||
);
|
||
expect(section).toContain(' - `ubuntu 20.04` → [Updates: `22.04`]');
|
||
expect(section).toContain('<details><summary>npm (1)</summary>');
|
||
expect(section).toContain(
|
||
'<details><summary>package.json (8)</summary>',
|
||
);
|
||
expect(section).toContain(' - `cookie-parser ^1.4.5`');
|
||
expect(section).toContain(
|
||
' - `express ~4.17.1` → [Updates: `~4.18.0`]',
|
||
);
|
||
expect(section).toContain(' - `express-handlebars >=5.3.4`');
|
||
expect(section).toContain(' - `geoip-lite 1.4.*`');
|
||
expect(section).toContain(
|
||
' - `nodemailer 6.7.0` → [Updates: `6.7.4`]',
|
||
);
|
||
expect(section).toContain(
|
||
' - `redis 3.1.2-3.4.0` → [Updates: `3.1.1`, `3.1.2`, `4.1.0`]',
|
||
);
|
||
expect(section).toContain(
|
||
' - `dotenv 10.0.0` → [Updates: `16.0.0`]',
|
||
);
|
||
expect(section).toContain(
|
||
' - `nodemon 2.0.14` → [Updates: `2.0.16`]',
|
||
);
|
||
expect(section).toContain('<details><summary>poetry (1)</summary>');
|
||
expect(section).toContain(
|
||
'<details><summary>pyproject.toml (1)</summary>',
|
||
);
|
||
expect(section).toContain(
|
||
' - `six <=1.3.0` → [Updates: `<=1.16.0`]',
|
||
);
|
||
}
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 0, 1);
|
||
});
|
||
|
||
it('show default message in issues body when packageFiles is empty', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
PackageFiles.add('main', {});
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
const body = platform.ensureIssue.mock.calls[0][0].body;
|
||
expect(body).toContain('## Detected Dependencies');
|
||
// the `dev` branch still lists its dependencies
|
||
expect(body).toContain('<details><summary>Branch dev</summary>');
|
||
expect(body).toContain('<details><summary>dockerfile (1)</summary>');
|
||
expect(body).toContain(' - `ubuntu 20.04` → [Updates: `22.04`]');
|
||
expect(body).toContain('<details><summary>npm (1)</summary>');
|
||
expect(body).toContain(' - `cookie-parser ^1.4.5`');
|
||
expect(body).toContain('<details><summary>poetry (1)</summary>');
|
||
expect(body).toContain(' - `six <=1.3.0` → [Updates: `<=1.16.0`]');
|
||
// only the `main` branch falls back to the default message
|
||
expect(body).toContain(codeBlock`
|
||
<details><summary>Branch main</summary>
|
||
<blockquote>
|
||
|
||
None detected
|
||
|
||
</blockquote>
|
||
</details>
|
||
`);
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 0, 1);
|
||
});
|
||
|
||
it('show default message in issues body when when packageFiles is null', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
PackageFiles.add('main', null);
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
const body = platform.ensureIssue.mock.calls[0][0].body;
|
||
expect(body).toContain('## Detected Dependencies');
|
||
// the `dev` branch still lists its dependencies
|
||
expect(body).toContain('<details><summary>Branch dev</summary>');
|
||
expect(body).toContain('<details><summary>dockerfile (1)</summary>');
|
||
expect(body).toContain(' - `ubuntu 20.04` → [Updates: `22.04`]');
|
||
expect(body).toContain('<details><summary>npm (1)</summary>');
|
||
expect(body).toContain(' - `cookie-parser ^1.4.5`');
|
||
expect(body).toContain('<details><summary>poetry (1)</summary>');
|
||
expect(body).toContain(' - `six <=1.3.0` → [Updates: `<=1.16.0`]');
|
||
// only the `main` branch falls back to the default message
|
||
expect(body).toContain(codeBlock`
|
||
<details><summary>Branch main</summary>
|
||
<blockquote>
|
||
|
||
None detected
|
||
|
||
</blockquote>
|
||
</details>
|
||
`);
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 0, 1);
|
||
});
|
||
|
||
it('truncates the body of a really big repo', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
const packageFilesBigRepo = genRandPackageFile(100, 700);
|
||
PackageFiles.clear();
|
||
PackageFiles.add('main', packageFilesBigRepo);
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
{},
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
expect(
|
||
platform.ensureIssue.mock.calls[0][0].body.length <
|
||
platform.maxBodyLength(),
|
||
).toBeTrue();
|
||
|
||
// same with dry run
|
||
await dryRun(branches, platform, 0, 1);
|
||
});
|
||
});
|
||
|
||
describe('dependency dashboard lookup warnings', () => {
|
||
beforeEach(() => {
|
||
PackageFiles.add('main', packageFiles);
|
||
PackageFiles.add('dev', packageFiles);
|
||
});
|
||
|
||
afterEach(() => {
|
||
PackageFiles.clear();
|
||
});
|
||
|
||
it('Dependency Lookup Warnings message in issues body', async () => {
|
||
const branches: BranchConfig[] = [];
|
||
PackageFiles.add('main', {
|
||
npm: [{ packageFile: 'package.json', deps: [] }],
|
||
});
|
||
const dep = [
|
||
{
|
||
warnings: [{ message: 'dependency-2', topic: '' }],
|
||
},
|
||
];
|
||
const packageFiles: Record<string, PackageFile[]> = {
|
||
npm: [{ packageFile: 'package.json', deps: dep }],
|
||
};
|
||
await dependencyDashboard.ensureDependencyDashboard(
|
||
config,
|
||
branches,
|
||
packageFiles,
|
||
{ result: 'no-migration' },
|
||
);
|
||
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
|
||
const body = platform.ensureIssue.mock.calls[0][0].body;
|
||
expect(body).toContain(codeBlock`
|
||
---
|
||
|
||
> [!WARNING]
|
||
> Renovate failed to look up the following dependencies: \`dependency-2\`.
|
||
>${' '}
|
||
> Files affected: \`package.json\`
|
||
|
||
---
|
||
`);
|
||
// same with dry run
|
||
await dryRun(branches, platform, 0, 1);
|
||
});
|
||
});
|
||
|
||
describe('PackageFiles.getDashboardMarkdown()', () => {
|
||
const note = emojify(
|
||
`> :information_source: **Note**\n> \n> Detected dependencies section has been truncated\n\n`,
|
||
);
|
||
const title = `## Detected Dependencies\n\n`;
|
||
|
||
beforeEach(() => {
|
||
PackageFiles.clear();
|
||
});
|
||
|
||
it('does not truncates as there is enough space to fit', () => {
|
||
PackageFiles.add('main', packageFiles);
|
||
const nonTruncated = PackageFiles.getDashboardMarkdown(Infinity);
|
||
const len = (title + note + nonTruncated).length;
|
||
const truncated = PackageFiles.getDashboardMarkdown(len);
|
||
const truncatedWithTitle = PackageFiles.getDashboardMarkdown(len);
|
||
expect(truncated.length === nonTruncated.length).toBeTrue();
|
||
expect(truncatedWithTitle.includes(note)).toBeFalse();
|
||
});
|
||
|
||
it('removes a branch with no managers', () => {
|
||
PackageFiles.add('main', packageFiles);
|
||
PackageFiles.add('dev', packageFilesWithDigest);
|
||
const md = PackageFiles.getDashboardMarkdown(Infinity, false);
|
||
const len = md.length;
|
||
PackageFiles.add('empty/branch', {});
|
||
const truncated = PackageFiles.getDashboardMarkdown(len, false);
|
||
expect(truncated.includes('empty/branch')).toBeFalse();
|
||
expect(truncated.length === len).toBeTrue();
|
||
});
|
||
|
||
it('removes a manager with no package files', () => {
|
||
PackageFiles.add('main', packageFiles);
|
||
const md = PackageFiles.getDashboardMarkdown(Infinity, false);
|
||
const len = md.length;
|
||
PackageFiles.add('dev', { dockerfile: [] });
|
||
const truncated = PackageFiles.getDashboardMarkdown(len, false);
|
||
expect(truncated.includes('dev')).toBeFalse();
|
||
expect(truncated.length === len).toBeTrue();
|
||
});
|
||
|
||
it('does nothing when there are no base branches left', () => {
|
||
const truncated = PackageFiles.getDashboardMarkdown(-1, false);
|
||
expect(truncated).toBe('');
|
||
});
|
||
|
||
it('removes an entire base branch', () => {
|
||
PackageFiles.add('main', packageFiles);
|
||
const md = PackageFiles.getDashboardMarkdown(Infinity);
|
||
const len = md.length + note.length;
|
||
PackageFiles.add('dev', packageFilesWithDigest);
|
||
const truncated = PackageFiles.getDashboardMarkdown(len);
|
||
expect(truncated.includes('dev')).toBeFalse();
|
||
expect(truncated.length === len).toBeTrue();
|
||
});
|
||
|
||
it('ensures original data is unchanged', () => {
|
||
PackageFiles.add('main', packageFiles);
|
||
PackageFiles.add('dev', packageFilesWithDigest);
|
||
const pre = PackageFiles.getDashboardMarkdown(Infinity);
|
||
const truncated = PackageFiles.getDashboardMarkdown(-1, false);
|
||
const post = PackageFiles.getDashboardMarkdown(Infinity);
|
||
expect(truncated).toBe('');
|
||
expect(pre === post).toBeTrue();
|
||
expect(post.includes('main')).toBeTrue();
|
||
expect(post.includes('dev')).toBeTrue();
|
||
});
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('getDashboardMarkdownVulnerabilities()', () => {
|
||
const packageFiles = Fixtures.getJson<Record<string, PackageFile[]>>(
|
||
'./package-files.json',
|
||
);
|
||
|
||
it('return empty string if summary is empty', async () => {
|
||
const result = await getDashboardMarkdownVulnerabilities(
|
||
config,
|
||
packageFiles,
|
||
);
|
||
expect(result).toBeEmpty();
|
||
});
|
||
|
||
it('return empty string if summary is set to none', async () => {
|
||
const result = await getDashboardMarkdownVulnerabilities(
|
||
{
|
||
...config,
|
||
dependencyDashboardOSVVulnerabilitySummary: 'none',
|
||
},
|
||
packageFiles,
|
||
);
|
||
expect(result).toBeEmpty();
|
||
});
|
||
|
||
it('return no data section if summary is set to all and no vulnerabilities', async () => {
|
||
const fetchVulnerabilitiesMock = vi.fn();
|
||
createVulnerabilitiesMock.mockResolvedValueOnce({
|
||
fetchVulnerabilities: fetchVulnerabilitiesMock,
|
||
});
|
||
|
||
fetchVulnerabilitiesMock.mockResolvedValueOnce([]);
|
||
const result = await getDashboardMarkdownVulnerabilities(
|
||
{
|
||
...config,
|
||
dependencyDashboardOSVVulnerabilitySummary: 'all',
|
||
},
|
||
{},
|
||
);
|
||
expect(result).toBe(
|
||
`## Vulnerabilities\n\nRenovate has not found any CVEs on [osv.dev](https://osv.dev).\n\n`,
|
||
);
|
||
});
|
||
|
||
it('return all vulnerabilities if set to all and disabled osvVulnerabilities', async () => {
|
||
const fetchVulnerabilitiesMock = vi.fn();
|
||
createVulnerabilitiesMock.mockResolvedValueOnce({
|
||
fetchVulnerabilities: fetchVulnerabilitiesMock,
|
||
});
|
||
|
||
fetchVulnerabilitiesMock.mockResolvedValueOnce([
|
||
{
|
||
packageName: 'express',
|
||
depVersion: '4.17.3',
|
||
fixedVersion: '4.18.1',
|
||
packageFileConfig: {
|
||
manager: 'npm',
|
||
},
|
||
vulnerability: {
|
||
id: 'GHSA-29mw-wpgm-hmr9',
|
||
},
|
||
},
|
||
{
|
||
packageName: 'cookie-parser',
|
||
depVersion: '1.4.6',
|
||
packageFileConfig: {
|
||
manager: 'npm',
|
||
},
|
||
vulnerability: {
|
||
id: 'GHSA-35jh-r3h4-6jhm',
|
||
},
|
||
},
|
||
]);
|
||
const result = await getDashboardMarkdownVulnerabilities(
|
||
{
|
||
...config,
|
||
dependencyDashboardOSVVulnerabilitySummary: 'all',
|
||
osvVulnerabilityAlerts: true,
|
||
},
|
||
packageFiles,
|
||
);
|
||
|
||
const heading =
|
||
'## Vulnerabilities\n\n> ❗ **Important**\n> \n' +
|
||
'> `1`/`2` CVEs have Renovate fixes.\n\n';
|
||
|
||
expect(result).toStartWith(heading);
|
||
expect(result).toContain('<details><summary>npm</summary>');
|
||
expect(result).toContain('<details><summary>express</summary>');
|
||
expect(result).toContain(
|
||
'- [GHSA-29mw-wpgm-hmr9](https://osv.dev/vulnerability/GHSA-29mw-wpgm-hmr9) (fixed in 4.18.1)',
|
||
);
|
||
expect(result).toContain('<details><summary>cookie-parser</summary>');
|
||
expect(result).toContain(
|
||
'- [GHSA-35jh-r3h4-6jhm](https://osv.dev/vulnerability/GHSA-35jh-r3h4-6jhm)',
|
||
);
|
||
});
|
||
|
||
it('return unresolved vulnerabilities if set to "unresolved"', async () => {
|
||
const fetchVulnerabilitiesMock = vi.fn();
|
||
createVulnerabilitiesMock.mockResolvedValueOnce({
|
||
fetchVulnerabilities: fetchVulnerabilitiesMock,
|
||
});
|
||
|
||
fetchVulnerabilitiesMock.mockResolvedValueOnce([
|
||
{
|
||
packageName: 'express',
|
||
depVersion: '4.17.3',
|
||
fixedVersion: '4.18.1',
|
||
packageFileConfig: {
|
||
manager: 'npm',
|
||
},
|
||
vulnerability: {
|
||
id: 'GHSA-29mw-wpgm-hmr9',
|
||
},
|
||
},
|
||
{
|
||
packageName: 'cookie-parser',
|
||
depVersion: '1.4.6',
|
||
packageFileConfig: {
|
||
manager: 'npm',
|
||
},
|
||
vulnerability: {
|
||
id: 'GHSA-35jh-r3h4-6jhm',
|
||
},
|
||
},
|
||
]);
|
||
|
||
const heading =
|
||
'## Vulnerabilities\n\n> ❗ **Important**\n> \n' +
|
||
'> `1`/`2` CVEs have possible Renovate fixes.\n' +
|
||
'> See [`osvVulnerabilityAlerts`](https://docs.renovatebot.com/configuration-options/#osvvulnerabilityalerts) to allow Renovate to supply fixes.\n\n';
|
||
|
||
const result = await getDashboardMarkdownVulnerabilities(
|
||
{
|
||
...config,
|
||
dependencyDashboardOSVVulnerabilitySummary: 'unresolved',
|
||
},
|
||
packageFiles,
|
||
);
|
||
expect(result).toStartWith(heading);
|
||
expect(result).toContain('<details><summary>npm</summary>');
|
||
expect(result).toContain('<details><summary>cookie-parser</summary>');
|
||
expect(result).toContain(
|
||
'- [GHSA-35jh-r3h4-6jhm](https://osv.dev/vulnerability/GHSA-35jh-r3h4-6jhm)',
|
||
);
|
||
// the vulnerability with a fixed version is left out
|
||
expect(result).not.toContain('express');
|
||
expect(result).not.toContain('GHSA-29mw-wpgm-hmr9');
|
||
});
|
||
});
|
||
|
||
describe('getAbandonedPackagesMd()', () => {
|
||
it('returns empty string when no abandoned packages exist', () => {
|
||
const packageFiles: Record<string, PackageFile[]> = {
|
||
npm: [
|
||
{
|
||
packageFile: 'package.json',
|
||
deps: [
|
||
{ depName: 'lodash', isAbandoned: false },
|
||
{ depName: 'express', isAbandoned: false },
|
||
],
|
||
},
|
||
],
|
||
};
|
||
|
||
const result = dependencyDashboard.getAbandonedPackagesMd(
|
||
config,
|
||
packageFiles,
|
||
);
|
||
expect(result).toEqual('');
|
||
});
|
||
|
||
it('returns formatted markdown when abandoned packages exist', () => {
|
||
const packageFiles: Record<string, PackageFile[]> = {
|
||
npm: [
|
||
{
|
||
packageFile: 'package.json',
|
||
deps: [
|
||
{
|
||
depName: 'abandoned-pkg',
|
||
isAbandoned: true,
|
||
mostRecentTimestamp: asTimestamp('2020-05-15T12:00:00.000Z')!,
|
||
},
|
||
],
|
||
},
|
||
],
|
||
};
|
||
|
||
const result = dependencyDashboard.getAbandonedPackagesMd(
|
||
config,
|
||
packageFiles,
|
||
);
|
||
|
||
expect(result).toContain('## Abandoned Dependencies');
|
||
expect(result).toContain(
|
||
'The following dependencies have not received updates for an extended period and may be unmaintained.',
|
||
);
|
||
expect(result).toContain(
|
||
'<summary>View abandoned dependencies (1)</summary>',
|
||
);
|
||
expect(result).toContain('> ℹ️ **Note**');
|
||
expect(result).toContain('| Datasource | Package | Last Updated |');
|
||
expect(result).toContain('| npm | `abandoned-pkg` | `2020-05-15` |');
|
||
expect(result).toContain('abandonmentThreshold');
|
||
});
|
||
|
||
it('handles multiple abandoned packages across different managers', () => {
|
||
const packageFiles: Record<string, PackageFile[]> = {
|
||
npm: [
|
||
{
|
||
packageFile: 'package.json',
|
||
deps: [
|
||
{
|
||
depName: 'pkg1',
|
||
isAbandoned: true,
|
||
mostRecentTimestamp: asTimestamp('2021-01-10T10:00:00.000Z')!,
|
||
},
|
||
{ depName: 'pkg2', isAbandoned: false },
|
||
{
|
||
depName: 'pkg3',
|
||
isAbandoned: true,
|
||
mostRecentTimestamp: asTimestamp('2020-11-05T15:30:00.000Z')!,
|
||
},
|
||
],
|
||
},
|
||
],
|
||
gradle: [
|
||
{
|
||
packageFile: 'build.gradle',
|
||
deps: [
|
||
{
|
||
depName: 'org.example:lib',
|
||
isAbandoned: true,
|
||
mostRecentTimestamp: asTimestamp('2019-07-22T08:15:00.000Z')!,
|
||
},
|
||
],
|
||
},
|
||
],
|
||
};
|
||
|
||
const result = dependencyDashboard.getAbandonedPackagesMd(
|
||
config,
|
||
packageFiles,
|
||
);
|
||
|
||
expect(result).toContain('## Abandoned Dependencies');
|
||
expect(result).toContain(
|
||
'The following dependencies have not received updates for an extended period and may be unmaintained.',
|
||
);
|
||
expect(result).toContain(
|
||
'<summary>View abandoned dependencies (3)</summary>',
|
||
);
|
||
expect(result).toContain('> ℹ️ **Note**');
|
||
expect(result).toContain('| Datasource | Package | Last Updated |');
|
||
expect(result).toContain('| gradle | `org.example:lib` | `2019-07-22` |');
|
||
expect(result).toContain('| npm | `pkg1` | `2021-01-10` |');
|
||
expect(result).toContain('| npm | `pkg3` | `2020-11-05` |');
|
||
expect(result).not.toContain('pkg2');
|
||
});
|
||
|
||
it('displays "unknown" when mostRecentTimestamp is missing', () => {
|
||
const packageFiles: Record<string, PackageFile[]> = {
|
||
npm: [
|
||
{
|
||
packageFile: 'package.json',
|
||
deps: [
|
||
{
|
||
depName: 'pkg-with-date',
|
||
isAbandoned: true,
|
||
mostRecentTimestamp: asTimestamp('2021-03-17T14:30:00.000Z')!,
|
||
},
|
||
{ depName: 'pkg-no-date', isAbandoned: true },
|
||
],
|
||
},
|
||
],
|
||
};
|
||
|
||
const result = dependencyDashboard.getAbandonedPackagesMd(
|
||
config,
|
||
packageFiles,
|
||
);
|
||
|
||
expect(result).toContain('## Abandoned Dependencies');
|
||
expect(result).toContain(
|
||
'The following dependencies have not received updates for an extended period and may be unmaintained.',
|
||
);
|
||
expect(result).toContain(
|
||
'<summary>View abandoned dependencies (2)</summary>',
|
||
);
|
||
expect(result).toContain('> ℹ️ **Note**');
|
||
expect(result).toContain('| Datasource | Package | Last Updated |');
|
||
expect(result).toContain('| npm | `pkg-with-date` | `2021-03-17` |');
|
||
expect(result).toContain('| npm | `pkg-no-date` | `unknown` |');
|
||
});
|
||
|
||
it('handles empty deps array', () => {
|
||
const packageFiles: Record<string, PackageFile[]> = {
|
||
npm: [
|
||
{
|
||
packageFile: 'package.json',
|
||
deps: [],
|
||
},
|
||
],
|
||
};
|
||
|
||
const result = dependencyDashboard.getAbandonedPackagesMd(
|
||
config,
|
||
packageFiles,
|
||
);
|
||
expect(result).toEqual('');
|
||
});
|
||
});
|
||
});
|