This commit is contained in:
I584999 2025-01-01 14:21:30 +00:00 committed by GitHub
commit 3f4695bdbd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 90 additions and 35 deletions

View file

@ -1998,6 +1998,13 @@ const options: RenovateOptions[] = [
type: 'string', type: 'string',
default: 'automergeComment', default: 'automergeComment',
}, },
{
name: 'automergeFailureComment',
description:
'If an error occurs while automerging, a comment will be created to signal the user. Only used if `automergeFailureComment=on-error`.',
type: 'string',
default: 'never',
},
{ {
name: 'ignoreTests', name: 'ignoreTests',
description: 'Set to `true` to enable automerging without tests.', description: 'Set to `true` to enable automerging without tests.',

View file

@ -34,6 +34,7 @@ export interface RenovateSharedConfig {
addLabels?: string[]; addLabels?: string[];
autoReplaceGlobalMatch?: boolean; autoReplaceGlobalMatch?: boolean;
automerge?: boolean; automerge?: boolean;
autoMergeFailureComment?: string;
automergeSchedule?: string[]; automergeSchedule?: string[];
automergeStrategy?: MergeStrategy; automergeStrategy?: MergeStrategy;
branchName?: string; branchName?: string;

View file

@ -467,6 +467,7 @@ describe('modules/platform/github/index', () => {
isArchived: false, isArchived: false,
nameWithOwner: repository, nameWithOwner: repository,
autoMergeAllowed: true, autoMergeAllowed: true,
automergeFailureComment: 'never',
hasIssuesEnabled: true, hasIssuesEnabled: true,
mergeCommitAllowed: true, mergeCommitAllowed: true,
rebaseMergeAllowed: true, rebaseMergeAllowed: true,
@ -2798,6 +2799,18 @@ describe('modules/platform/github/index', () => {
platformPrOptions: { usePlatformAutomerge: true }, platformPrOptions: { usePlatformAutomerge: true },
}; };
const prConfigComment: CreatePRConfig = {
sourceBranch: 'some-branch',
targetBranch: 'dev',
prTitle: 'The Title',
prBody: 'Hello world',
labels: ['deps', 'renovate'],
platformPrOptions: {
usePlatformAutomerge: true,
automergeFailureComment: 'on-error',
},
};
const mockScope = async (repoOpts: any = {}): Promise<httpMock.Scope> => { const mockScope = async (repoOpts: any = {}): Promise<httpMock.Scope> => {
const scope = httpMock.scope(githubApiHost); const scope = httpMock.scope(githubApiHost);
initRepoMock(scope, 'some/repo', repoOpts); initRepoMock(scope, 'some/repo', repoOpts);
@ -2975,6 +2988,16 @@ describe('modules/platform/github/index', () => {
]); ]);
}); });
it('should handle GraphQL errors with automergeFailureComment', async () => {
const scope = await mockScope({ automergeFailureComment: 'on-error' });
scope
.post('/repos/some/repo/issues/123/comments')
.reply(200)
.post('/graphql')
.reply(200, graphqlAutomergeErrorResp);
await expect(github.createPr(prConfigComment)).toResolve();
});
it('should handle REST API errors', async () => { it('should handle REST API errors', async () => {
const scope = await mockScope(); const scope = await mockScope();
scope.post('/graphql').reply(500); scope.post('/graphql').reply(500);

View file

@ -1697,13 +1697,21 @@ async function tryPrAutomerge(
{ prNumber, errors: res.errors }, { prNumber, errors: res.errors },
'GitHub-native automerge: fail', 'GitHub-native automerge: fail',
); );
if (platformPrOptions.automergeFailureComment === 'on-error') {
logger.warn('This automerge request failed');
await addComment(
prNumber,
'The Automerge Request for this PR has failed. Please attend.',
);
return;
}
return; return;
} }
logger.debug(`GitHub-native automerge: success...PrNo: ${prNumber}`); logger.debug(`GitHub-native automerge: success...PrNo: ${prNumber}`);
} catch (err) /* istanbul ignore next: missing test #22198 */ { } catch (err) /* istanbul ignore next: missing test #22198 */ {
logger.warn({ prNumber, err }, 'GitHub-native automerge: REST API error'); logger.warn({ prNumber, err }, 'GitHub-native automerge: REST API error');
} }
return;
} }
// Creates PR and returns PR number // Creates PR and returns PR number

View file

@ -100,6 +100,7 @@ export interface Issue {
export type PlatformPrOptions = { export type PlatformPrOptions = {
autoApprove?: boolean; autoApprove?: boolean;
automergeStrategy?: MergeStrategy; automergeStrategy?: MergeStrategy;
automergeFailureComment?: string;
azureWorkItemId?: number; azureWorkItemId?: number;
bbUseDefaultReviewers?: boolean; bbUseDefaultReviewers?: boolean;
bbAutoResolvePrTasks?: boolean; bbAutoResolvePrTasks?: boolean;

View file

@ -57,6 +57,7 @@ export function getPlatformPrOptions(
return { return {
autoApprove: !!config.autoApprove, autoApprove: !!config.autoApprove,
automergeStrategy: config.automergeStrategy, automergeStrategy: config.automergeStrategy,
automergeFailureComment: config.automergeFailureComment,
azureWorkItemId: config.azureWorkItemId ?? 0, azureWorkItemId: config.azureWorkItemId ?? 0,
bbAutoResolvePrTasks: !!config.bbAutoResolvePrTasks, bbAutoResolvePrTasks: !!config.bbAutoResolvePrTasks,
bbUseDefaultReviewers: !!config.bbUseDefaultReviewers, bbUseDefaultReviewers: !!config.bbUseDefaultReviewers,

View file

@ -116,6 +116,7 @@ export interface BranchConfig
LegacyAdminConfig, LegacyAdminConfig,
PlatformPrOptions { PlatformPrOptions {
automergeComment?: string; automergeComment?: string;
automergeFailureComment?: string;
automergeType?: string; automergeType?: string;
automergedPreviously?: boolean; automergedPreviously?: boolean;
baseBranch: string; baseBranch: string;

View file

@ -19,12 +19,12 @@ describe('documentation', () => {
}); });
describe('website-documentation', () => { describe('website-documentation', () => {
function getConfigOptionSubHeaders( async function getConfigOptionSubHeaders(
file: string, file: string,
configOption: string, configOption: string,
): string[] { ): Promise<string[]> {
const subHeadings = []; const subHeadings = [];
const content = fs.readFileSync(`docs/usage/${file}`, 'utf8'); const content = await fs.readFile(`docs/usage/${file}`, 'utf8');
const reg = regEx(`##\\s${configOption}[\\s\\S]+?\n##\\s`); const reg = regEx(`##\\s${configOption}[\\s\\S]+?\n##\\s`);
const match = reg.exec(content); const match = reg.exec(content);
const subHeadersMatch = match?.[0]?.matchAll(/\n###\s(?<child>\w+)\n/g); const subHeadersMatch = match?.[0]?.matchAll(/\n###\s(?<child>\w+)\n/g);
@ -39,8 +39,8 @@ describe('documentation', () => {
} }
describe('docs/usage/configuration-options.md', () => { describe('docs/usage/configuration-options.md', () => {
function getConfigHeaders(file: string): string[] { async function getConfigHeaders(file: string): Promise<string[]> {
const content = fs.readFileSync(`docs/usage/${file}`, 'utf8'); const content = await fs.readFile(`docs/usage/${file}`, 'utf8');
const matches = content.match(/\n## (.*?)\n/g) ?? []; const matches = content.match(/\n## (.*?)\n/g) ?? [];
return matches.map((match) => match.substring(4, match.length - 1)); return matches.map((match) => match.substring(4, match.length - 1));
} }
@ -54,20 +54,20 @@ describe('documentation', () => {
.sort(); .sort();
} }
it('has doc headers sorted alphabetically', () => { it('has doc headers sorted alphabetically', async () => {
expect(getConfigHeaders('configuration-options.md')).toEqual( expect(await getConfigHeaders('configuration-options.md')).toEqual(
getConfigHeaders('configuration-options.md').sort(), (await getConfigHeaders('configuration-options.md')).sort(),
); );
}); });
it('has headers for every required option', () => { it('has headers for every required option', async () => {
expect(getConfigHeaders('configuration-options.md')).toEqual( expect(await getConfigHeaders('configuration-options.md')).toEqual(
getRequiredConfigOptions(), getRequiredConfigOptions(),
); );
}); });
function getConfigSubHeaders(file: string): string[] { async function getConfigSubHeaders(file: string): Promise<string[]> {
const content = fs.readFileSync(`docs/usage/${file}`, 'utf8'); const content = await fs.readFile(`docs/usage/${file}`, 'utf8');
const matches = content.match(/\n### (.*?)\n/g) ?? []; const matches = content.match(/\n### (.*?)\n/g) ?? [];
return matches return matches
.map((match) => match.substring(5, match.length - 1)) .map((match) => match.substring(5, match.length - 1))
@ -100,21 +100,26 @@ describe('documentation', () => {
return parentNames; return parentNames;
} }
it('has headers for every required sub-option', () => { it('has headers for every required sub-option', async () => {
expect(getConfigSubHeaders('configuration-options.md')).toEqual( expect(await getConfigSubHeaders('configuration-options.md')).toEqual(
getRequiredConfigSubOptions(), getRequiredConfigSubOptions(),
); );
}); });
it.each([...getParentNames()])( test.each([...getParentNames()])(
'%s has sub-headers sorted alphabetically', '%s has sub-headers sorted alphabetically',
(parentName: string) => { async (parentName: string) => {
expect( expect(
getConfigOptionSubHeaders('configuration-options.md', parentName), await getConfigOptionSubHeaders(
).toEqual(
getConfigOptionSubHeaders(
'configuration-options.md', 'configuration-options.md',
parentName, parentName,
),
).toEqual(
(
await getConfigOptionSubHeaders(
'configuration-options.md',
parentName,
)
).sort(), ).sort(),
); );
}, },
@ -122,8 +127,8 @@ describe('documentation', () => {
}); });
describe('docs/usage/self-hosted-configuration.md', () => { describe('docs/usage/self-hosted-configuration.md', () => {
function getSelfHostedHeaders(file: string): string[] { async function getSelfHostedHeaders(file: string): Promise<string[]> {
const content = fs.readFileSync(`docs/usage/${file}`, 'utf8'); const content = await fs.readFile(`docs/usage/${file}`, 'utf8');
const matches = content.match(/\n## (.*?)\n/g) ?? []; const matches = content.match(/\n## (.*?)\n/g) ?? [];
return matches.map((match) => match.substring(4, match.length - 1)); return matches.map((match) => match.substring(4, match.length - 1));
} }
@ -135,32 +140,40 @@ describe('documentation', () => {
.sort(); .sort();
} }
it('has headers sorted alphabetically', () => { it('has headers sorted alphabetically', async () => {
expect(getSelfHostedHeaders('self-hosted-configuration.md')).toEqual( expect(
getSelfHostedHeaders('self-hosted-configuration.md').sort(), await getSelfHostedHeaders('self-hosted-configuration.md'),
).toEqual(
(await getSelfHostedHeaders('self-hosted-configuration.md')).sort(),
); );
}); });
it('has headers for every required option', () => { it('has headers for every required option', async () => {
expect(getSelfHostedHeaders('self-hosted-configuration.md')).toEqual( expect(
getRequiredSelfHostedOptions(), await getSelfHostedHeaders('self-hosted-configuration.md'),
); ).toEqual(getRequiredSelfHostedOptions());
}); });
}); });
describe('docs/usage/self-hosted-experimental.md', () => { describe('docs/usage/self-hosted-experimental.md', () => {
function getSelfHostedExperimentalConfigHeaders(file: string): string[] { async function getSelfHostedExperimentalConfigHeaders(
const content = fs.readFileSync(`docs/usage/${file}`, 'utf8'); file: string,
): Promise<string[]> {
const content = await fs.readFile(`docs/usage/${file}`, 'utf8');
const matches = content.match(/\n## (.*?)\n/g) ?? []; const matches = content.match(/\n## (.*?)\n/g) ?? [];
return matches.map((match) => match.substring(4, match.length - 1)); return matches.map((match) => match.substring(4, match.length - 1));
} }
it('has headers sorted alphabetically', () => { it('has headers sorted alphabetically', async () => {
expect( expect(
getSelfHostedExperimentalConfigHeaders('self-hosted-experimental.md'), await getSelfHostedExperimentalConfigHeaders(
).toEqual(
getSelfHostedExperimentalConfigHeaders(
'self-hosted-experimental.md', 'self-hosted-experimental.md',
),
).toEqual(
(
await getSelfHostedExperimentalConfigHeaders(
'self-hosted-experimental.md',
)
).sort(), ).sort(),
); );
}); });