mirror of
https://github.com/renovatebot/renovate.git
synced 2026-11-01 18:19:51 +00:00
feat(onboarding): add a summary for detected package files (#44258)
* refactor(onboarding): extract a function for calculating packageFiles As part of a future change, we'll introduce a "summary" view for larger repositories, and having a function extracted makes intent clearer. Co-authored-by: Claude Sonnet 4.6 <jamie.tanna+claude-code@mend.io> * refactor(onboarding): add a summary for detected package files As part of future changes in #41502, we're introducing a Platform-limit-aware onboarding/reconfigure flow that will provide a summary where appropriate. Before we do so, we can introduce the new functionality - but not integrate it right now - to incrementally add support. As a slightly better interface when we have a large list of detected package files, we can provide a summary per Manager. Co-authored-by: Claude Sonnet 4.6 <jamie.tanna+claude-code@mend.io> * refactor(onboarding): group 3+ files under a directory as a list As it's a more readable summary, especially with i.e. GitHub Actions, where all package files commonly share the same directory. The use of `3` as the lower bound was chosen without any research or too much thought put into it. Co-authored-by: Claude Sonnet 4.6 <jamie.tanna+claude-code@mend.io> --------- Co-authored-by: Claude Sonnet 4.6 <jamie.tanna+claude-code@mend.io>
This commit is contained in:
@@ -32,6 +32,7 @@ import {
|
||||
} from '../common.ts';
|
||||
import { getBaseBranchDesc } from './base-branch.ts';
|
||||
import { getConfigDesc } from './config-description.ts';
|
||||
import { getPackageFilesDesc } from './package-files.ts';
|
||||
import { getExpectedPrList } from './pr-list.ts';
|
||||
|
||||
/**
|
||||
@@ -187,17 +188,9 @@ If you need any further assistance then you can also [request help here](${
|
||||
);
|
||||
prTemplate += rebaseCheckBox;
|
||||
let prBody = prTemplate;
|
||||
if (packageFiles && Object.entries(packageFiles).length) {
|
||||
let files: string[] = [];
|
||||
for (const [manager, managerFiles] of Object.entries(packageFiles)) {
|
||||
files = files.concat(
|
||||
managerFiles.map((file) => ` * \`${file.packageFile}\` (${manager})`),
|
||||
);
|
||||
}
|
||||
prBody = `${prBody.replace(
|
||||
'{{PACKAGE FILES}}',
|
||||
`### Detected Package Files\n\n${files.join('\n')}`,
|
||||
)}\n`;
|
||||
const packageFilesDesc = getPackageFilesDesc(packageFiles);
|
||||
if (packageFilesDesc) {
|
||||
prBody = `${prBody.replace('{{PACKAGE FILES}}', packageFilesDesc)}\n`;
|
||||
} else {
|
||||
prBody = prBody.replace('{{PACKAGE FILES}}\n', '');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { partial } from '~test/util.ts';
|
||||
import type { PackageFile } from '../../../../modules/manager/types.ts';
|
||||
import {
|
||||
getPackageFilesDesc,
|
||||
getPackageFilesSummary,
|
||||
} from './package-files.ts';
|
||||
|
||||
describe('workers/repository/onboarding/pr/package-files', () => {
|
||||
describe('getPackageFilesDesc()', () => {
|
||||
it('returns empty string when packageFiles is null', () => {
|
||||
expect(getPackageFilesDesc(null)).toBeEmptyString();
|
||||
});
|
||||
|
||||
it('returns empty string when packageFiles is empty', () => {
|
||||
expect(getPackageFilesDesc({})).toBeEmptyString();
|
||||
});
|
||||
|
||||
it('returns formatted markdown for a single manager', () => {
|
||||
const res = getPackageFilesDesc({
|
||||
npm: [partial<PackageFile>({ packageFile: 'package.json' })],
|
||||
});
|
||||
expect(res).toBe('### Detected Package Files\n\n * `package.json` (npm)');
|
||||
});
|
||||
|
||||
it('returns formatted markdown for multiple managers', () => {
|
||||
const res = getPackageFilesDesc({
|
||||
npm: [
|
||||
partial<PackageFile>({ packageFile: 'package.json' }),
|
||||
partial<PackageFile>({ packageFile: 'a/package.json' }),
|
||||
],
|
||||
dockerfile: [partial<PackageFile>({ packageFile: 'Dockerfile' })],
|
||||
});
|
||||
expect(res).toBe(
|
||||
'### Detected Package Files\n\n * `package.json` (npm)\n * `a/package.json` (npm)\n * `Dockerfile` (dockerfile)',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns formatted markdown for many files across managers, including a file handled by multiple managers', () => {
|
||||
const res = getPackageFilesDesc({
|
||||
npm: [
|
||||
partial<PackageFile>({ packageFile: 'package.json' }),
|
||||
partial<PackageFile>({ packageFile: 'frontend/package.json' }),
|
||||
partial<PackageFile>({ packageFile: 'backend/package.json' }),
|
||||
],
|
||||
dockerfile: [
|
||||
partial<PackageFile>({ packageFile: 'Dockerfile' }),
|
||||
partial<PackageFile>({ packageFile: 'services/api/Dockerfile' }),
|
||||
],
|
||||
'docker-compose': [
|
||||
partial<PackageFile>({ packageFile: 'docker-compose.yml' }),
|
||||
],
|
||||
'github-actions': [
|
||||
partial<PackageFile>({ packageFile: '.github/workflows/ci.yml' }),
|
||||
partial<PackageFile>({
|
||||
packageFile: '.github/workflows/release.yml',
|
||||
}),
|
||||
],
|
||||
regex: [partial<PackageFile>({ packageFile: 'Dockerfile' })],
|
||||
});
|
||||
expect(res).toBe(
|
||||
'### Detected Package Files\n\n' +
|
||||
' * `package.json` (npm)\n' +
|
||||
' * `frontend/package.json` (npm)\n' +
|
||||
' * `backend/package.json` (npm)\n' +
|
||||
' * `Dockerfile` (dockerfile)\n' +
|
||||
' * `services/api/Dockerfile` (dockerfile)\n' +
|
||||
' * `docker-compose.yml` (docker-compose)\n' +
|
||||
' * `.github/workflows/ci.yml` (github-actions)\n' +
|
||||
' * `.github/workflows/release.yml` (github-actions)\n' +
|
||||
' * `Dockerfile` (regex)',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPackageFilesSummary()', () => {
|
||||
it('returns empty string when packageFiles is null', () => {
|
||||
expect(getPackageFilesSummary(null)).toBeEmptyString();
|
||||
});
|
||||
|
||||
it('returns empty string when packageFiles is empty', () => {
|
||||
expect(getPackageFilesSummary({})).toBeEmptyString();
|
||||
});
|
||||
|
||||
it('returns grouped markdown for a single manager', () => {
|
||||
const res = getPackageFilesSummary({
|
||||
npm: [partial<PackageFile>({ packageFile: 'package.json' })],
|
||||
});
|
||||
expect(res).toBe('#### npm\n\n * `package.json`');
|
||||
});
|
||||
|
||||
it('groups files under a nested list when a directory has 3 or more files', () => {
|
||||
const res = getPackageFilesSummary({
|
||||
'github-actions': [
|
||||
partial<PackageFile>({
|
||||
packageFile: '.github/workflows/ci.yml',
|
||||
}),
|
||||
partial<PackageFile>({
|
||||
packageFile: '.github/workflows/release.yml',
|
||||
}),
|
||||
partial<PackageFile>({
|
||||
packageFile: '.github/workflows/lint.yml',
|
||||
}),
|
||||
],
|
||||
});
|
||||
expect(res).toBe(
|
||||
'#### github-actions\n\n' +
|
||||
' * `.github/workflows/`\n' +
|
||||
' * `ci.yml`\n' +
|
||||
' * `release.yml`\n' +
|
||||
' * `lint.yml`',
|
||||
);
|
||||
});
|
||||
|
||||
it('lists files individually when a directory has fewer than 3 files, and groups when it meets the threshold', () => {
|
||||
const res = getPackageFilesSummary({
|
||||
'github-actions': [
|
||||
partial<PackageFile>({ packageFile: '.github/workflows/ci.yml' }),
|
||||
partial<PackageFile>({
|
||||
packageFile: '.github/workflows/release.yml',
|
||||
}),
|
||||
partial<PackageFile>({
|
||||
packageFile: '.github/workflows/lint.yml',
|
||||
}),
|
||||
partial<PackageFile>({ packageFile: '.github/dependabot.yml' }),
|
||||
],
|
||||
});
|
||||
expect(res).toBe(
|
||||
'#### github-actions\n\n' +
|
||||
' * `.github/workflows/`\n' +
|
||||
' * `ci.yml`\n' +
|
||||
' * `release.yml`\n' +
|
||||
' * `lint.yml`\n' +
|
||||
' * `.github/dependabot.yml`',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns grouped markdown for many files across managers, including a file handled by multiple managers', () => {
|
||||
const res = getPackageFilesSummary({
|
||||
npm: [
|
||||
partial<PackageFile>({ packageFile: 'package.json' }),
|
||||
partial<PackageFile>({ packageFile: 'frontend/package.json' }),
|
||||
partial<PackageFile>({ packageFile: 'backend/package.json' }),
|
||||
],
|
||||
dockerfile: [
|
||||
partial<PackageFile>({ packageFile: 'Dockerfile' }),
|
||||
partial<PackageFile>({ packageFile: 'services/api/Dockerfile' }),
|
||||
],
|
||||
'docker-compose': [
|
||||
partial<PackageFile>({ packageFile: 'docker-compose.yml' }),
|
||||
],
|
||||
'github-actions': [
|
||||
partial<PackageFile>({ packageFile: '.github/workflows/ci.yml' }),
|
||||
partial<PackageFile>({
|
||||
packageFile: '.github/workflows/release.yml',
|
||||
}),
|
||||
],
|
||||
regex: [partial<PackageFile>({ packageFile: 'Dockerfile' })],
|
||||
});
|
||||
expect(res).toBe(
|
||||
'#### npm\n\n' +
|
||||
' * `package.json`\n' +
|
||||
' * `frontend/package.json`\n' +
|
||||
' * `backend/package.json`\n\n' +
|
||||
'#### dockerfile\n\n' +
|
||||
' * `Dockerfile`\n' +
|
||||
' * `services/api/Dockerfile`\n\n' +
|
||||
'#### docker-compose\n\n' +
|
||||
' * `docker-compose.yml`\n\n' +
|
||||
'#### github-actions\n\n' +
|
||||
' * `.github/workflows/ci.yml`\n' +
|
||||
' * `.github/workflows/release.yml`\n\n' +
|
||||
'#### regex\n\n' +
|
||||
' * `Dockerfile`',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { PackageFile } from '../../../../modules/manager/types.ts';
|
||||
|
||||
const GROUP_THRESHOLD = 3;
|
||||
|
||||
function formatManagerFiles(files: PackageFile[]): string {
|
||||
const byDir = new Map<string, string[]>();
|
||||
for (const { packageFile } of files) {
|
||||
const lastSlash = packageFile.lastIndexOf('/');
|
||||
const dir = lastSlash >= 0 ? packageFile.slice(0, lastSlash + 1) : '';
|
||||
const name =
|
||||
lastSlash >= 0 ? packageFile.slice(lastSlash + 1) : packageFile;
|
||||
const existing = byDir.get(dir);
|
||||
if (existing) {
|
||||
existing.push(name);
|
||||
} else {
|
||||
byDir.set(dir, [name]);
|
||||
}
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
for (const [dir, names] of byDir) {
|
||||
if (dir && names.length >= GROUP_THRESHOLD) {
|
||||
lines.push(` * \`${dir}\``);
|
||||
for (const name of names) {
|
||||
lines.push(` * \`${name}\``);
|
||||
}
|
||||
} else {
|
||||
for (const name of names) {
|
||||
lines.push(` * \`${dir}${name}\``);
|
||||
}
|
||||
}
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function getPackageFilesSummary(
|
||||
packageFiles: Record<string, PackageFile[]> | null,
|
||||
): string {
|
||||
if (!packageFiles || !Object.entries(packageFiles).length) {
|
||||
return '';
|
||||
}
|
||||
const sections: string[] = [];
|
||||
for (const [manager, managerFiles] of Object.entries(packageFiles)) {
|
||||
sections.push(`#### ${manager}\n\n${formatManagerFiles(managerFiles)}`);
|
||||
}
|
||||
return sections.join('\n\n');
|
||||
}
|
||||
|
||||
export function getPackageFilesDesc(
|
||||
packageFiles: Record<string, PackageFile[]> | null,
|
||||
): string {
|
||||
if (!packageFiles || !Object.entries(packageFiles).length) {
|
||||
return '';
|
||||
}
|
||||
let files: string[] = [];
|
||||
for (const [manager, managerFiles] of Object.entries(packageFiles)) {
|
||||
files = files.concat(
|
||||
managerFiles.map((file) => ` * \`${file.packageFile}\` (${manager})`),
|
||||
);
|
||||
}
|
||||
return `### Detected Package Files\n\n${files.join('\n')}`;
|
||||
}
|
||||
Reference in New Issue
Block a user