Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(config): validate reconfigure branch #24699

Merged
merged 38 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
d87eedd
feat: implement reconfigureLogic
RahulGautamSingh Sep 28, 2023
48b4ecf
refactor
RahulGautamSingh Sep 28, 2023
18e95ca
Apply suggestions from code review
RahulGautamSingh Sep 28, 2023
45bc7c8
rename function name
RahulGautamSingh Sep 29, 2023
044c755
ignore reconfigure branch while pruning
RahulGautamSingh Sep 29, 2023
2e94c72
add tests
RahulGautamSingh Oct 3, 2023
d28455f
more tests
RahulGautamSingh Oct 3, 2023
67e18f1
fix coverage
RahulGautamSingh Oct 3, 2023
b7c1dc1
Apply Suggestions
RahulGautamSingh Oct 4, 2023
0d466ab
use JSON5.parse
RahulGautamSingh Oct 4, 2023
6c6153e
Apply suggestions from code review
RahulGautamSingh Oct 4, 2023
bec3c67
Merge branch 'rename-second-parsePreset' of https://github.com/RahulG…
RahulGautamSingh Oct 4, 2023
803bbdc
fix formatting
RahulGautamSingh Oct 4, 2023
16d7cc3
don't cache configFileName
RahulGautamSingh Oct 4, 2023
c712e27
add docs
RahulGautamSingh Oct 4, 2023
e3fa7a2
Update docs/usage/config-validation.md
RahulGautamSingh Oct 4, 2023
98026f6
Apply suggestions from code review
RahulGautamSingh Oct 8, 2023
47fc0f4
refactor
RahulGautamSingh Oct 8, 2023
c46e3ff
fix lint issues
RahulGautamSingh Oct 8, 2023
88dafb9
Update docs/usage/config-validation.md
RahulGautamSingh Oct 8, 2023
ef3bb71
fix coverage
RahulGautamSingh Oct 8, 2023
a535bc5
Merge branch 'rename-second-parsePreset' of https://github.com/RahulG…
RahulGautamSingh Oct 8, 2023
abcf4cb
Apply suggestions from code review
RahulGautamSingh Oct 14, 2023
ca14c35
add tests
RahulGautamSingh Oct 15, 2023
250ba9e
merge
RahulGautamSingh Oct 15, 2023
3296472
fix lint issues
RahulGautamSingh Oct 15, 2023
bb71f60
Merge branch 'main' into rename-second-parsePreset
RahulGautamSingh Oct 15, 2023
eae1621
Merge branch 'main' into rename-second-parsePreset
rarkins Oct 23, 2023
aa59a7d
Update docs/usage/config-validation.md
RahulGautamSingh Oct 23, 2023
b2b3b7a
Apply suggestions from code review
RahulGautamSingh Oct 23, 2023
caae616
Merge branch 'main' into rename-second-parsePreset
RahulGautamSingh Oct 23, 2023
06969d8
skips validation if status check found
RahulGautamSingh Oct 24, 2023
e0ea724
Apply suggestions from code review
RahulGautamSingh Oct 26, 2023
6e051f6
apply suggestions
RahulGautamSingh Oct 26, 2023
1a203e4
fix logging message
RahulGautamSingh Oct 27, 2023
f551b9d
fix test name
RahulGautamSingh Oct 27, 2023
3db751a
Merge branch 'main' into rename-second-parsePreset
RahulGautamSingh Oct 30, 2023
ce96698
fix: set cache and branch status, if file is unparsable
RahulGautamSingh Oct 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/usage/config-validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,20 @@ $ renovate-config-validator first_config.jsonn

You can create a [pre-commit](https://pre-commit.com) hook to validate your configuration automatically.
Go to the [`renovatebot/pre-commit-hooks` repository](https://github.com/renovatebot/pre-commit-hooks) for more information.

### Validation of Renovate config change PRs

Renovate can validate configuration changes in Pull Requests when you use a special branch name.

Follow these steps to validate your configuration:

1. Create a new Git branch that matches the `{{branchPrefix}}reconfigure` pattern. For example, if you're using the default prefix `renovate/`, your branch name must be `renovate/reconfigure`.
1. Commit your updated Renovate config file to this branch, and push it to your Git hosting platform.

The next time Renovate runs on that repo it will:

1. Search for a branch that matches the special reconfigure pattern.
1. Check for a config file in the reconfigure branch. Renovate can even find a renamed configuration file (compared to the config file in the default branch).
1. Add a passing or failing status to the branch, depending on the outcome of the config validation run.
1. If there's an _open_ pull request with validation errors from the _reconfigure_ branch then Renovate comments in the PR with details.
1. Validate each commit the next time Renovate runs on the repository, until the PR is merged.
6 changes: 6 additions & 0 deletions lib/util/cache/repository/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ export interface OnboardingBranchCache {
configFileParsed?: string;
}

export interface ReconfigureBranchCache {
reconfigureBranchSha: string;
isConfigValid: boolean;
}

export interface PrCache {
/**
* Fingerprint of the PR body
Expand Down Expand Up @@ -129,6 +134,7 @@ export interface RepoCacheData {
};
prComments?: Record<number, Record<string, string>>;
onboardingBranchCache?: OnboardingBranchCache;
reconfigureBranchCache?: ReconfigureBranchCache;
}

export interface RepoCache {
Expand Down
2 changes: 2 additions & 0 deletions lib/workers/repository/finalize/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as repositoryCache from '../../../util/cache/repository';
import { clearRenovateRefs } from '../../../util/git';
import { configMigration } from '../config-migration';
import { PackageFiles } from '../package-files';
import { validateReconfigureBranch } from '../reconfigure';
import { pruneStaleBranches } from './prune';
import {
runBranchSummary,
Expand All @@ -16,6 +17,7 @@ export async function finalizeRepo(
config: RenovateConfig,
branchList: string[]
): Promise<void> {
await validateReconfigureBranch(config);
await configMigration(config, branchList);
await repositoryCache.saveCache();
await pruneStaleBranches(config, branchList);
Expand Down
6 changes: 6 additions & 0 deletions lib/workers/repository/finalize/prune.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ describe('workers/repository/finalize/prune', () => {
expect(git.getBranchList).toHaveBeenCalledTimes(0);
});

it('ignores reconfigure branch', async () => {
delete config.branchList;
await cleanup.pruneStaleBranches(config, config.branchList);
expect(git.getBranchList).toHaveBeenCalledTimes(0);
});

it('returns if no renovate branches', async () => {
config.branchList = [];
git.getBranchList.mockReturnValueOnce([]);
Expand Down
7 changes: 5 additions & 2 deletions lib/workers/repository/finalize/prune.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { platform } from '../../../modules/platform';
import { ensureComment } from '../../../modules/platform/comment';
import { scm } from '../../../modules/platform/scm';
import { getBranchList, setUserRepoConfig } from '../../../util/git';
import { getReconfigureBranchName } from '../reconfigure';

async function cleanUpBranches(
config: RenovateConfig,
Expand Down Expand Up @@ -109,8 +110,10 @@ export async function pruneStaleBranches(
return;
}
// TODO: types (#22198)
let renovateBranches = getBranchList().filter((branchName) =>
branchName.startsWith(config.branchPrefix!)
let renovateBranches = getBranchList().filter(
(branchName) =>
branchName.startsWith(config.branchPrefix!) &&
branchName !== getReconfigureBranchName(config.branchPrefix!)
);
if (!renovateBranches?.length) {
logger.debug('No renovate branches found');
Expand Down
2 changes: 1 addition & 1 deletion lib/workers/repository/init/merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
import { OnboardingState } from '../onboarding/common';
import type { RepoFileConfig } from './types';

async function detectConfigFile(): Promise<string | null> {
export async function detectConfigFile(): Promise<string | null> {
const fileList = await scm.getFileList();
for (const fileName of configFileNames) {
if (fileName === 'package.json') {
Expand Down
196 changes: 196 additions & 0 deletions lib/workers/repository/reconfigure/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { mock } from 'jest-mock-extended';
import {
RenovateConfig,
fs,
git,
mocked,
platform,
scm,
} from '../../../../test/util';
import { logger } from '../../../logger';
import type { Pr } from '../../../modules/platform/types';
import * as _cache from '../../../util/cache/repository';
import * as _merge from '../init/merge';
import { validateReconfigureBranch } from '.';

jest.mock('../../../util/cache/repository');
jest.mock('../../../util/fs');
jest.mock('../../../util/git');
jest.mock('../init/merge');

const cache = mocked(_cache);
const merge = mocked(_merge);

describe('workers/repository/reconfigure/index', () => {
const config: RenovateConfig = {
branchPrefix: 'prefix/',
baseBranch: 'base',
};

beforeEach(() => {
config.repository = 'some/repo';
merge.detectConfigFile.mockResolvedValue('renovate.json');
scm.branchExists.mockResolvedValue(true);
cache.getCache.mockReturnValue({});
git.getBranchCommit.mockReturnValue('sha');
fs.readLocalFile.mockResolvedValue(null);
platform.getBranchPr.mockResolvedValue(null);
platform.getBranchStatusCheck.mockResolvedValue(null);
});

it('no effect on repo with no reconfigure branch', async () => {
scm.branchExists.mockResolvedValueOnce(false);
await validateReconfigureBranch(config);
expect(logger.debug).toHaveBeenCalledWith('No reconfigure branch found');
});

it('logs error if config file search fails', async () => {
const err = new Error();
merge.detectConfigFile.mockRejectedValueOnce(err as never);
await validateReconfigureBranch(config);
expect(logger.error).toHaveBeenCalledWith(
{ err },
'Error while searching for config file in reconfigure branch'
);
});

it('throws error if config file not found in reconfigure branch', async () => {
merge.detectConfigFile.mockResolvedValue(null);
await validateReconfigureBranch(config);
expect(logger.warn).toHaveBeenCalledWith(
'No config file found in reconfigure branch'
);
});

it('logs error if config file is unreadable', async () => {
const err = new Error();
fs.readLocalFile.mockRejectedValueOnce(err as never);
await validateReconfigureBranch(config);
expect(logger.error).toHaveBeenCalledWith(
{ err },
'Error while reading config file'
);
});

it('throws error if config file is empty', async () => {
await validateReconfigureBranch(config);
expect(logger.warn).toHaveBeenCalledWith('Empty or invalid config file');
});

it('throws error if config file content is invalid', async () => {
fs.readLocalFile.mockResolvedValueOnce(`
{
"name":
}
`);
await validateReconfigureBranch(config);
expect(logger.error).toHaveBeenCalledWith(
{ err: expect.any(Object) },
'Error while parsing config file'
);
expect(platform.setBranchStatus).toHaveBeenCalledWith({
branchName: 'prefix/reconfigure',
context: 'renovate/config-validation',
description: 'Validation Failed - Unparsable config file',
state: 'red',
});
});

it('handles failed validation', async () => {
fs.readLocalFile.mockResolvedValueOnce(`
{
"enabledManagers": ["docker"]
}
`);
await validateReconfigureBranch(config);
expect(logger.debug).toHaveBeenCalledWith(
{ errors: expect.any(String) },
'Validation Errors'
);
expect(platform.setBranchStatus).toHaveBeenCalledWith({
branchName: 'prefix/reconfigure',
context: 'renovate/config-validation',
description: 'Validation Failed',
state: 'red',
});
});

it('adds comment if reconfigure PR exists', async () => {
fs.readLocalFile.mockResolvedValueOnce(`
{
"enabledManagers": ["docker"]
}
`);
platform.getBranchPr.mockResolvedValueOnce(mock<Pr>({ number: 1 }));
await validateReconfigureBranch(config);
expect(logger.debug).toHaveBeenCalledWith(
{ errors: expect.any(String) },
'Validation Errors'
);
expect(platform.setBranchStatus).toHaveBeenCalled();
expect(platform.ensureComment).toHaveBeenCalled();
});

it('handles successful validation', async () => {
const pJson = `
{
"renovate": {
"enabledManagers": ["npm"]
}
}
`;
merge.detectConfigFile.mockResolvedValue('package.json');
fs.readLocalFile.mockResolvedValueOnce(pJson).mockResolvedValueOnce(pJson);
await validateReconfigureBranch(config);
expect(platform.setBranchStatus).toHaveBeenCalledWith({
branchName: 'prefix/reconfigure',
context: 'renovate/config-validation',
description: 'Validation Successful',
state: 'green',
});
});

it('skips validation if cache is valid', async () => {
cache.getCache.mockReturnValueOnce({
reconfigureBranchCache: {
reconfigureBranchSha: 'sha',
isConfigValid: false,
},
});
await validateReconfigureBranch(config);
expect(logger.debug).toHaveBeenCalledWith(
'Skipping validation check as branch sha is unchanged'
);
});

it('skips validation if status check present', async () => {
cache.getCache.mockReturnValueOnce({
reconfigureBranchCache: {
reconfigureBranchSha: 'new_sha',
isConfigValid: false,
},
});
platform.getBranchStatusCheck.mockResolvedValueOnce('green');
await validateReconfigureBranch(config);
expect(logger.debug).toHaveBeenCalledWith(
'Skipping validation check as status check already exists'
);
});

it('handles non-default config file', async () => {
merge.detectConfigFile.mockResolvedValue('.renovaterc');
fs.readLocalFile.mockResolvedValueOnce(`
{
"enabledManagers": ["npm",]
}
`);
platform.getBranchPr.mockResolvedValueOnce(mock<Pr>({ number: 1 }));
await validateReconfigureBranch(config);
expect(platform.setBranchStatus).toHaveBeenCalledWith({
branchName: 'prefix/reconfigure',
context: 'renovate/config-validation',
description: 'Validation Successful',
state: 'green',
});
});
});