diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index a0b519f6113340..ba378faa185545 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -894,6 +894,22 @@ Renovate follows tags _strictly_, this can cause problems when a tagged stream i For example: you're following the `next` tag, but later the stream you actually want is called `stable` instead. If `next` is no longer getting updates, you must switch your `followTag` to `stable` to get updates again. +## forkModeDisallowMaintainerEdits + +Use `forkModeDisallowMaintainerEdits` to disallow maintainers from editing Renovate's pull requests when in fork mode. + +If GitHub pull requests are created from a [fork repository](https://docs.github.com/en/get-started/quickstart/fork-a-repo), the PR author can decide to allow upstream repository to modify the PR directly. + +Allowing maintainers to edit pull requests directly is helpful when Renovate pull requests require additional changes. +The reviewer can simply push to the pull request without having to create a new PR. [More details here](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork). + +You may decide to disallow edits to Renovate pull requests in order to workaround issues in Renovate where modified fork branches are not deleted properly: [See this issue](https://github.com/renovatebot/renovate/issues/16657). +If this option is enabled, reviewers will need to create a new PR if additional changes are needed. + + +!!! note + This option is only relevant if you set `forkToken`. + ## gitAuthor You can customize the Git author that's used whenever Renovate creates a commit. diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index d9c6f9b20e509b..a401297e2dafee 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -1929,6 +1929,14 @@ const options: RenovateOptions[] = [ default: false, supportedPlatforms: ['gitlab'], }, + { + name: 'forkModeDisallowMaintainerEdits', + description: + 'Disallow maintainers to push to Renovate pull requests when running in fork mode.', + type: 'boolean', + supportedPlatforms: ['github'], + default: false, + }, { name: 'confidential', description: diff --git a/lib/config/types.ts b/lib/config/types.ts index 74110ea2c3282a..d4572fafb49e74 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -230,6 +230,7 @@ export interface RenovateConfig postUpdateOptions?: string[]; prConcurrentLimit?: number; prHourlyLimit?: number; + forkModeDisallowMaintainerEdits?: boolean; defaultRegistryUrls?: string[]; registryUrls?: string[] | null; diff --git a/lib/modules/platform/github/index.spec.ts b/lib/modules/platform/github/index.spec.ts index 1983ce3d2c1829..acdf7859662641 100644 --- a/lib/modules/platform/github/index.spec.ts +++ b/lib/modules/platform/github/index.spec.ts @@ -274,7 +274,7 @@ describe('modules/platform/github/index', () => { repository: string, forkExisted: boolean, forkResult = 200, - forkDefaulBranch = 'master' + forkDefaultBranch = 'master' ): void { scope // repo info @@ -307,7 +307,7 @@ describe('modules/platform/github/index', () => { { full_name: 'forked/repo', owner: { login: 'forked' }, - default_branch: forkDefaulBranch, + default_branch: forkDefaultBranch, }, ] : [] @@ -2180,6 +2180,96 @@ describe('modules/platform/github/index', () => { expect(pr).toMatchObject({ number: 123 }); }); + describe('with forkToken', () => { + let scope: httpMock.Scope; + + beforeEach(async () => { + scope = httpMock.scope(githubApiHost); + forkInitRepoMock(scope, 'some/repo', false); + scope.get('/user').reply(200, { + login: 'forked', + }); + scope.post('/repos/some/repo/forks').reply(200, { + full_name: 'forked/repo', + default_branch: 'master', + }); + + await github.initRepo({ + repository: 'some/repo', + forkToken: 'true', + }); + }); + + it('should allow maintainer edits if explicitly enabled via options', async () => { + scope + .post( + '/repos/some/repo/pulls', + // Ensure the `maintainer_can_modify` option is set in the REST API request. + (body) => body.maintainer_can_modify === true + ) + .reply(200, { + number: 123, + head: { repo: { full_name: 'some/repo' }, ref: 'some-branch' }, + }); + const pr = await github.createPr({ + sourceBranch: 'some-branch', + targetBranch: 'main', + prTitle: 'PR title', + prBody: 'PR can be edited by maintainers.', + labels: null, + platformOptions: { + forkModeDisallowMaintainerEdits: false, + }, + }); + expect(pr).toMatchObject({ number: 123 }); + }); + + it('should allow maintainer edits if not explicitly set', async () => { + scope + .post( + '/repos/some/repo/pulls', + // Ensure the `maintainer_can_modify` option is `false` in the REST API request. + (body) => body.maintainer_can_modify === true + ) + .reply(200, { + number: 123, + head: { repo: { full_name: 'some/repo' }, ref: 'some-branch' }, + }); + const pr = await github.createPr({ + sourceBranch: 'some-branch', + targetBranch: 'main', + prTitle: 'PR title', + prBody: 'PR *cannot* be edited by maintainers.', + labels: null, + }); + expect(pr).toMatchObject({ number: 123 }); + }); + + it('should disallow maintainer edits if explicitly disabled', async () => { + scope + .post( + '/repos/some/repo/pulls', + // Ensure the `maintainer_can_modify` option is `false` in the REST API request. + (body) => body.maintainer_can_modify === false + ) + .reply(200, { + number: 123, + head: { repo: { full_name: 'some/repo' }, ref: 'some-branch' }, + }); + const pr = await github.createPr({ + sourceBranch: 'some-branch', + targetBranch: 'main', + prTitle: 'PR title', + prBody: 'PR *cannot* be edited by maintainers.', + labels: null, + platformOptions: { + forkModeDisallowMaintainerEdits: true, + }, + }); + expect(pr).toMatchObject({ number: 123 }); + }); + }); + describe('automerge', () => { const createdPrResp = { number: 123, diff --git a/lib/modules/platform/github/index.ts b/lib/modules/platform/github/index.ts index 1ce38129842978..7422d4a6ff93d0 100644 --- a/lib/modules/platform/github/index.ts +++ b/lib/modules/platform/github/index.ts @@ -1499,7 +1499,8 @@ export async function createPr({ // istanbul ignore if if (config.forkToken) { options.token = config.forkToken; - options.body.maintainer_can_modify = true; + options.body.maintainer_can_modify = + platformOptions?.forkModeDisallowMaintainerEdits !== true; } logger.debug({ title, head, base, draft: draftPR }, 'Creating PR'); const ghPr = ( diff --git a/lib/modules/platform/types.ts b/lib/modules/platform/types.ts index 0b5219e68ad946..07272f44987eba 100644 --- a/lib/modules/platform/types.ts +++ b/lib/modules/platform/types.ts @@ -92,6 +92,7 @@ export type PlatformPrOptions = { bbUseDefaultReviewers?: boolean; gitLabIgnoreApprovals?: boolean; usePlatformAutomerge?: boolean; + forkModeDisallowMaintainerEdits?: boolean; }; export interface CreatePRConfig { sourceBranch: string; diff --git a/lib/workers/repository/update/pr/index.ts b/lib/workers/repository/update/pr/index.ts index 34b49cf15cf02e..08e29f3aef8b34 100644 --- a/lib/workers/repository/update/pr/index.ts +++ b/lib/workers/repository/update/pr/index.ts @@ -51,6 +51,7 @@ export function getPlatformPrOptions( azureWorkItemId: config.azureWorkItemId, bbUseDefaultReviewers: config.bbUseDefaultReviewers, gitLabIgnoreApprovals: config.gitLabIgnoreApprovals, + forkModeDisallowMaintainerEdits: config.forkModeDisallowMaintainerEdits, usePlatformAutomerge, }; }