diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index e7516bd06463d3..750e96b8d98c0b 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -173,6 +173,25 @@ Example use: } ``` +## automergeStrategy + +This setting is only applicable if you opt-in by configuring `automerge` to `true` and `automergeType` to `pr` for any of your dependencies. + +The automerge strategy defaults to `auto`, in which Renovate will make its best guess as to how to merge pull requests. +This generally results in Renovate respecting the strategy configured in the platform itself for the repository if possible. +Acceptable values are: + +- `auto`, in which the choice is left to Renovate +- `fast-forward`, which generally involves no new commits in the resultant tree, but "fast-forwarding" the main branch reference +- `merge-commit`, which generally involves synthesizing a new merge commit +- `rebase`, which generally involves rewriting history as part of the merge — but usually retaining the individual commits +- `squash`, which generally involves flattening the commits that are being merged into a single new commit + +Not all platforms support all pull request merge strategies. +In cases where a merge strategy is not supported by the platform, Renovate will hold off on merging instead of silently merging in a way you didn't wish for. + +The only platform that supports `automergeStrategy` is Bitbucket Cloud. + ## automergeType This setting is only applicable if you opt in to configure `automerge` to `true` for any of your dependencies. diff --git a/lib/config/definitions.ts b/lib/config/definitions.ts index 2c232094fd336c..26290fa07ebea7 100644 --- a/lib/config/definitions.ts +++ b/lib/config/definitions.ts @@ -1285,6 +1285,14 @@ const options: RenovateOptions[] = [ allowedValues: ['branch', 'pr', 'pr-comment'], default: 'pr', }, + { + name: 'automergeStrategy', + description: + 'The merge strategy to use when automerging PRs. Used only if `automergeType=pr`.', + type: 'string', + allowedValues: ['auto', 'fast-forward', 'merge-commit', 'rebase', 'squash'], + default: 'auto', + }, { name: 'automergeComment', description: diff --git a/lib/config/types.ts b/lib/config/types.ts index 3fa1a645c2a7c7..fdd56479c4d8e2 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -21,6 +21,7 @@ export interface GroupConfig extends Record { export interface RenovateSharedConfig { $schema?: string; automerge?: boolean; + automergeStrategy?: MergeStrategy; branchPrefix?: string; branchName?: string; manager?: string; @@ -229,6 +230,13 @@ export type UpdateType = export type MatchStringsStrategy = 'any' | 'recursive' | 'combination'; +export type MergeStrategy = + | 'auto' + | 'fast-forward' + | 'merge-commit' + | 'rebase' + | 'squash'; + // TODO: Proper typings export interface PackageRule extends RenovateSharedConfig, diff --git a/lib/platform/azure/index.md b/lib/platform/azure/index.md new file mode 100644 index 00000000000000..d8fadac49eb1b2 --- /dev/null +++ b/lib/platform/azure/index.md @@ -0,0 +1,5 @@ +# Azure DevOps and Azure DevOps Server + +## Features awaiting implementation + +- The `automergeStrategy` configuration option has not been implemented for this platform, and all values behave as if the value `auto` was used. Renovate will use the merge strategy configured in the Azure Repos repository itself, and this cannot be overridden yet diff --git a/lib/platform/bitbucket-server/index.md b/lib/platform/bitbucket-server/index.md index d5ef66a96a7c30..6937275b2f24ec 100644 --- a/lib/platform/bitbucket-server/index.md +++ b/lib/platform/bitbucket-server/index.md @@ -8,6 +8,7 @@ ## Features awaiting implementation - Creating issues not implemented yet, used to alert users when there is a config error +- The `automergeStrategy` configuration option has not been implemented for this platform, and all values behave as if the value `auto` was used. Renovate will implicitly use the merge strategy configured as 'default' in the Bitbucket Server repository itself, and this cannot be overridden yet ## Testing diff --git a/lib/platform/bitbucket/__snapshots__/index.spec.ts.snap b/lib/platform/bitbucket/__snapshots__/index.spec.ts.snap index 1bcedd7cb3fc3e..041ab86b614530 100644 --- a/lib/platform/bitbucket/__snapshots__/index.spec.ts.snap +++ b/lib/platform/bitbucket/__snapshots__/index.spec.ts.snap @@ -1254,7 +1254,7 @@ Array [ exports[`platform/bitbucket/index massageMarkdown() returns diff files 1`] = `"**foo**bartext"`; -exports[`platform/bitbucket/index mergePr() posts Merge 1`] = ` +exports[`platform/bitbucket/index mergePr() posts Merge with auto 1`] = ` Array [ Object { "headers": Object { @@ -1268,7 +1268,67 @@ Array [ "url": "https://api.bitbucket.org/2.0/repositories/some/repo", }, Object { - "body": "{\\"close_source_branch\\":true,\\"merge_strategy\\":\\"merge_commit\\",\\"message\\":\\"auto merged\\"}", + "body": "{\\"close_source_branch\\":true,\\"message\\":\\"auto merged\\"}", + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Basic YWJjOjEyMw==", + "content-length": "52", + "content-type": "application/json", + "host": "api.bitbucket.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "POST", + "url": "https://api.bitbucket.org/2.0/repositories/some/repo/pullrequests/5/merge", + }, +] +`; + +exports[`platform/bitbucket/index mergePr() posts Merge with fast-forward 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Basic YWJjOjEyMw==", + "host": "api.bitbucket.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://api.bitbucket.org/2.0/repositories/some/repo", + }, + Object { + "body": "{\\"close_source_branch\\":true,\\"message\\":\\"auto merged\\",\\"merge_strategy\\":\\"fast_forward\\"}", + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Basic YWJjOjEyMw==", + "content-length": "84", + "content-type": "application/json", + "host": "api.bitbucket.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "POST", + "url": "https://api.bitbucket.org/2.0/repositories/some/repo/pullrequests/5/merge", + }, +] +`; + +exports[`platform/bitbucket/index mergePr() posts Merge with merge-commit 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Basic YWJjOjEyMw==", + "host": "api.bitbucket.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://api.bitbucket.org/2.0/repositories/some/repo", + }, + Object { + "body": "{\\"close_source_branch\\":true,\\"message\\":\\"auto merged\\",\\"merge_strategy\\":\\"merge_commit\\"}", "headers": Object { "accept": "application/json", "accept-encoding": "gzip, deflate, br", @@ -1284,6 +1344,66 @@ Array [ ] `; +exports[`platform/bitbucket/index mergePr() posts Merge with optional merge strategy 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Basic YWJjOjEyMw==", + "host": "api.bitbucket.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://api.bitbucket.org/2.0/repositories/some/repo", + }, + Object { + "body": "{\\"close_source_branch\\":true,\\"message\\":\\"auto merged\\"}", + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Basic YWJjOjEyMw==", + "content-length": "52", + "content-type": "application/json", + "host": "api.bitbucket.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "POST", + "url": "https://api.bitbucket.org/2.0/repositories/some/repo/pullrequests/5/merge", + }, +] +`; + +exports[`platform/bitbucket/index mergePr() posts Merge with squash 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Basic YWJjOjEyMw==", + "host": "api.bitbucket.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://api.bitbucket.org/2.0/repositories/some/repo", + }, + Object { + "body": "{\\"close_source_branch\\":true,\\"message\\":\\"auto merged\\",\\"merge_strategy\\":\\"squash\\"}", + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Basic YWJjOjEyMw==", + "content-length": "78", + "content-type": "application/json", + "host": "api.bitbucket.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "POST", + "url": "https://api.bitbucket.org/2.0/repositories/some/repo/pullrequests/5/merge", + }, +] +`; + exports[`platform/bitbucket/index setBranchStatus() posts status 1`] = ` Array [ Object { diff --git a/lib/platform/bitbucket/index.md b/lib/platform/bitbucket/index.md index 1f77fc5d65730f..5655abc9c04c8a 100644 --- a/lib/platform/bitbucket/index.md +++ b/lib/platform/bitbucket/index.md @@ -3,6 +3,7 @@ ## Unsupported platform features/concepts - Adding assignees to PRs not supported (does not seem to be a Bitbucket concept) +- `automergeStrategy=rebase` not supported by BitBucket Cloud, see [Jira issue BCLOUD-16610](https://jira.atlassian.com/browse/BCLOUD-16610) ## Features requiring implementation diff --git a/lib/platform/bitbucket/index.spec.ts b/lib/platform/bitbucket/index.spec.ts index 435bb242782ea9..1959e1f1fe3281 100644 --- a/lib/platform/bitbucket/index.spec.ts +++ b/lib/platform/bitbucket/index.spec.ts @@ -812,12 +812,45 @@ describe(getName(), () => { }); describe('mergePr()', () => { - it('posts Merge', async () => { + it('posts Merge with optional merge strategy', async () => { const scope = await initRepoMock(); scope.post('/2.0/repositories/some/repo/pullrequests/5/merge').reply(200); await bitbucket.mergePr(5, 'branch'); expect(httpMock.getTrace()).toMatchSnapshot(); }); + + it('posts Merge with auto', async () => { + const scope = await initRepoMock(); + scope.post('/2.0/repositories/some/repo/pullrequests/5/merge').reply(200); + await bitbucket.mergePr(5, 'branch', 'auto'); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + + it('posts Merge with merge-commit', async () => { + const scope = await initRepoMock(); + scope.post('/2.0/repositories/some/repo/pullrequests/5/merge').reply(200); + await bitbucket.mergePr(5, 'branch', 'merge-commit'); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + + it('posts Merge with squash', async () => { + const scope = await initRepoMock(); + scope.post('/2.0/repositories/some/repo/pullrequests/5/merge').reply(200); + await bitbucket.mergePr(5, 'branch', 'squash'); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + + it('does not post Merge with rebase', async () => { + await bitbucket.mergePr(5, 'branch', 'rebase'); + expect(httpMock.getTrace()).toEqual([]); + }); + + it('posts Merge with fast-forward', async () => { + const scope = await initRepoMock(); + scope.post('/2.0/repositories/some/repo/pullrequests/5/merge').reply(200); + await bitbucket.mergePr(5, 'branch', 'fast-forward'); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); }); describe('getVulnerabilityAlerts()', () => { diff --git a/lib/platform/bitbucket/index.ts b/lib/platform/bitbucket/index.ts index 972da032e2de1d..5699528b541d31 100644 --- a/lib/platform/bitbucket/index.ts +++ b/lib/platform/bitbucket/index.ts @@ -1,6 +1,7 @@ import URL from 'url'; import is from '@sindresorhus/is'; import parseDiff from 'parse-diff'; +import type { MergeStrategy } from '../../config/types'; import { REPOSITORY_NOT_FOUND } from '../../constants/error-messages'; import { PLATFORM_TYPE_BITBUCKET } from '../../constants/platforms'; import { logger } from '../../logger'; @@ -29,7 +30,7 @@ import { smartTruncate } from '../utils/pr-body'; import { readOnlyIssueBody } from '../utils/read-only-issue-body'; import * as comments from './comments'; import * as utils from './utils'; -import { PrResponse, RepoInfoBody } from './utils'; +import { PrResponse, RepoInfoBody, mergeBodyTransformer } from './utils'; const bitbucketHttp = new BitbucketHttp(); @@ -391,7 +392,7 @@ export async function setBranchStatus({ const sha = await getBranchCommit(branchName); // TargetUrl can not be empty so default to bitbucket - const url = targetUrl || /* istanbul ignore next */ 'http://bitbucket.org'; + const url = targetUrl || /* istanbul ignore next */ 'https://bitbucket.org'; const body = { name: context, @@ -748,19 +749,22 @@ export async function updatePr({ export async function mergePr( prNo: number, - branchName: string + branchName: string, + mergeStrategy: MergeStrategy ): Promise { - logger.debug(`mergePr(${prNo}, ${branchName})`); + logger.debug(`mergePr(${prNo}, ${branchName}, ${mergeStrategy})`); + + // Bitbucket Cloud does not support a rebase-alike; https://jira.atlassian.com/browse/BCLOUD-16610 + if (mergeStrategy === 'rebase') { + logger.warn('Bitbucket Cloud does not support a "rebase" strategy.'); + return false; + } try { await bitbucketHttp.postJson( `/2.0/repositories/${config.repository}/pullrequests/${prNo}/merge`, { - body: { - close_source_branch: true, - merge_strategy: 'merge_commit', - message: 'auto merged', - }, + body: mergeBodyTransformer(mergeStrategy), } ); logger.debug('Automerging succeeded'); diff --git a/lib/platform/bitbucket/types.ts b/lib/platform/bitbucket/types.ts new file mode 100644 index 00000000000000..9e3bb0c16788ee --- /dev/null +++ b/lib/platform/bitbucket/types.ts @@ -0,0 +1,7 @@ +export type BitbucketMergeStrategy = 'fast_forward' | 'merge_commit' | 'squash'; + +export interface MergeRequestBody { + close_source_branch?: boolean; + message: string; + merge_strategy?: BitbucketMergeStrategy; +} diff --git a/lib/platform/bitbucket/utils.ts b/lib/platform/bitbucket/utils.ts index 1f7c07f65d2d66..c20b2ec28c9239 100644 --- a/lib/platform/bitbucket/utils.ts +++ b/lib/platform/bitbucket/utils.ts @@ -1,8 +1,10 @@ import url from 'url'; +import type { MergeStrategy } from '../../config/types'; import { BranchStatus, PrState } from '../../types'; import { HttpOptions, HttpPostOptions, HttpResponse } from '../../util/http'; import { BitbucketHttp } from '../../util/http/bitbucket'; import type { Pr } from '../types'; +import type { BitbucketMergeStrategy, MergeRequestBody } from './types'; const bitbucketHttp = new BitbucketHttp(); @@ -56,6 +58,29 @@ export function repoInfoTransformer(repoInfoBody: RepoInfoBody): RepoInfo { }; } +const bitbucketMergeStrategies: Map = + new Map([ + ['squash', 'squash'], + ['merge-commit', 'merge_commit'], + ['fast-forward', 'fast_forward'], + ]); + +export function mergeBodyTransformer( + mergeStrategy: MergeStrategy +): MergeRequestBody { + const body: MergeRequestBody = { + close_source_branch: true, + message: 'auto merged', + }; + + // The `auto` strategy will use the strategy configured inside Bitbucket. + if (mergeStrategy !== 'auto') { + body.merge_strategy = bitbucketMergeStrategies.get(mergeStrategy); + } + + return body; +} + export const prStates = { open: ['OPEN'], notOpen: ['MERGED', 'DECLINED', 'SUPERSEDED'], diff --git a/lib/platform/gitea/index.md b/lib/platform/gitea/index.md index e9cd63ea62fa71..3a45af107fe621 100644 --- a/lib/platform/gitea/index.md +++ b/lib/platform/gitea/index.md @@ -3,3 +3,7 @@ ## Unsupported platform features/concepts - **Adding reviewers to PRs not supported**: Gitea versions older than v1.14.0 do not have the required API. + +## Features awaiting implementation + +- The `automergeStrategy` configuration option has not been implemented for this platform, and all values behave as if the value `auto` was used. Renovate will use the merge strategy configured in the Gitea repository itself, and this cannot be overridden yet diff --git a/lib/platform/github/index.md b/lib/platform/github/index.md new file mode 100644 index 00000000000000..b0f378dba227aa --- /dev/null +++ b/lib/platform/github/index.md @@ -0,0 +1,5 @@ +# GitHub and GitHub Enterprise + +## Features awaiting implementation + +- The `automergeStrategy` configuration option has not been implemented for this platform, and all values behave as if the value `auto` was used. Renovate will use the merge strategy configured in the GitHub repository itself, and this cannot be overridden yet diff --git a/lib/platform/gitlab/index.md b/lib/platform/gitlab/index.md new file mode 100644 index 00000000000000..cd05a459cd99dc --- /dev/null +++ b/lib/platform/gitlab/index.md @@ -0,0 +1,5 @@ +# GitLab + +## Features awaiting implementation + +- The `automergeStrategy` configuration option has not been implemented for this platform, and all values behave as if the value `auto` was used. Renovate will accept the Merge Request without further configuration, and respect the strategy defined in the Merge Request, and this cannot be overridden yet diff --git a/lib/platform/types.ts b/lib/platform/types.ts index 150e6d1818e406..121245f42f8b73 100644 --- a/lib/platform/types.ts +++ b/lib/platform/types.ts @@ -1,3 +1,4 @@ +import type { MergeStrategy } from '../config/types'; import type { BranchStatus, PrState, VulnerabilityAlert } from '../types'; type VulnerabilityKey = string; @@ -152,7 +153,11 @@ export interface Platform { ): Promise; massageMarkdown(prBody: string): string; updatePr(prConfig: UpdatePrConfig): Promise; - mergePr(number: number, branchName: string): Promise; + mergePr( + number: number, + branchName: string, + mergeStrategy?: MergeStrategy + ): Promise; addReviewers(number: number, reviewers: string[]): Promise; addAssignees(number: number, assignees: string[]): Promise; createPr(prConfig: CreatePRConfig): Promise; diff --git a/lib/workers/pr/automerge.ts b/lib/workers/pr/automerge.ts index d3d86a4ef0f3cc..b1c5af8fc13295 100644 --- a/lib/workers/pr/automerge.ts +++ b/lib/workers/pr/automerge.ts @@ -28,6 +28,7 @@ export async function checkAutoMerge( const { branchName, automergeType, + automergeStrategy, automergeComment, requiredStatusChecks, rebaseRequested, @@ -99,14 +100,16 @@ export async function checkAutoMerge( // Let's merge this // istanbul ignore if if (getAdminConfig().dryRun) { - logger.info(`DRY-RUN: Would merge PR #${pr.number}`); + logger.info( + `DRY-RUN: Would merge PR #${pr.number} with strategy "${automergeStrategy}"` + ); return { automerged: false, prAutomergeBlockReason: PrAutomergeBlockReason.DryRun, }; } - logger.debug(`Automerging #${pr.number}`); - const res = await platform.mergePr(pr.number, branchName); + logger.debug(`Automerging #${pr.number} with strategy ${automergeStrategy}`); + const res = await platform.mergePr(pr.number, branchName, automergeStrategy); if (res) { logger.info({ pr: pr.number, prTitle: pr.title }, 'PR automerged'); let branchRemoved = false;