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',
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',
description: 'Set to `true` to enable automerging without tests.',

View file

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

View file

@ -467,6 +467,7 @@ describe('modules/platform/github/index', () => {
isArchived: false,
nameWithOwner: repository,
autoMergeAllowed: true,
automergeFailureComment: 'never',
hasIssuesEnabled: true,
mergeCommitAllowed: true,
rebaseMergeAllowed: true,
@ -2798,6 +2799,18 @@ describe('modules/platform/github/index', () => {
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 scope = httpMock.scope(githubApiHost);
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 () => {
const scope = await mockScope();
scope.post('/graphql').reply(500);

View file

@ -1697,13 +1697,21 @@ async function tryPrAutomerge(
{ prNumber, errors: res.errors },
'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;
}
logger.debug(`GitHub-native automerge: success...PrNo: ${prNumber}`);
} catch (err) /* istanbul ignore next: missing test #22198 */ {
logger.warn({ prNumber, err }, 'GitHub-native automerge: REST API error');
}
return;
}
// Creates PR and returns PR number

View file

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

View file

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

View file

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

View file

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