diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index d87ec95912f74cc3b2c52182d664f80168c69f93..8df007979992dad33b493498cc783176cac7da8e 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -2529,6 +2529,11 @@ This works because Renovate will add a "renovate/stability-days" pending status <!-- markdownlint-enable MD001 --> +## stopUpdatingLabel + +On supported platforms it is possible to add a label to a PR to request Renovate stop updating the PR. +By default this label is `"stop-updating"` however you can configure it to anything you want by changing this `stopUpdatingLabel` field. + ## suppressNotifications Use this field to suppress various types of warnings and other notifications from Renovate. diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index fb738ad170089c9075a37d1345792da27b7be951..34cceaece77687e617a59297b94a36f95231bb29 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -1313,6 +1313,13 @@ const options: RenovateOptions[] = [ type: 'string', default: 'rebase', }, + { + name: 'stopUpdatingLabel', + description: 'Label to use to request the bot to stop updating a PR.', + type: 'string', + default: 'stop-updating', + supportedPlatforms: ['azure', 'github', 'gitlab', 'gitea'], + }, { name: 'stabilityDays', description: diff --git a/lib/config/types.ts b/lib/config/types.ts index 258b29e3d5a883b5b723e33ede9e6327f37d3518..e102ae643c79161d3c8361675c6dec7987d29c7c 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -54,6 +54,7 @@ export interface RenovateSharedConfig { productLinks?: Record<string, string>; prPriority?: number; rebaseLabel?: string; + stopUpdatingLabel?: string; rebaseWhen?: string; recreateClosed?: boolean; repository?: string; diff --git a/lib/workers/branch/index.spec.ts b/lib/workers/branch/index.spec.ts index c7615c389850d900c6ff03da101af57c01801ba4..cfaed31930cce0ff97a219f782501610458fa70a 100644 --- a/lib/workers/branch/index.spec.ts +++ b/lib/workers/branch/index.spec.ts @@ -939,6 +939,76 @@ describe('workers/branch/index', () => { }); }); + it('skips branch update if stopUpdatingLabel presents', async () => { + getUpdated.getUpdatedPackageFiles.mockResolvedValueOnce({ + updatedPackageFiles: [{}], + artifactErrors: [], + } as PackageFilesResult); + npmPostExtract.getAdditionalFiles.mockResolvedValueOnce({ + artifactErrors: [], + updatedArtifacts: [{}], + } as WriteExistingFilesResult); + git.branchExists.mockReturnValue(true); + platform.getBranchPr.mockResolvedValueOnce({ + title: 'rebase!', + state: PrState.Open, + labels: ['stop-updating'], + body: `- [ ] <!-- rebase-check -->`, + } as Pr); + git.isBranchModified.mockResolvedValueOnce(true); + schedule.isScheduledNow.mockReturnValueOnce(false); + commit.commitFilesToBranch.mockResolvedValueOnce(null); + expect( + await branchWorker.processBranch({ + ...config, + dependencyDashboardChecks: { 'renovate/some-branch': 'true' }, + updatedArtifacts: [{ type: 'deletion', path: 'dummy' }], + }) + ).toMatchInlineSnapshot(` + Object { + "branchExists": true, + "prNo": undefined, + "result": "no-work", + } + `); + expect(commit.commitFilesToBranch).not.toHaveBeenCalled(); + }); + + it('updates branch if stopUpdatingLabel presents and PR rebase/retry box checked', async () => { + getUpdated.getUpdatedPackageFiles.mockResolvedValueOnce({ + updatedPackageFiles: [{}], + artifactErrors: [], + } as PackageFilesResult); + npmPostExtract.getAdditionalFiles.mockResolvedValueOnce({ + artifactErrors: [], + updatedArtifacts: [{}], + } as WriteExistingFilesResult); + git.branchExists.mockReturnValue(true); + platform.getBranchPr.mockResolvedValueOnce({ + title: 'Update dependency', + state: PrState.Open, + labels: ['stop-updating'], + body: `- [x] <!-- rebase-check -->`, + } as Pr); + git.isBranchModified.mockResolvedValueOnce(true); + schedule.isScheduledNow.mockReturnValueOnce(false); + commit.commitFilesToBranch.mockResolvedValueOnce(null); + expect( + await branchWorker.processBranch({ + ...config, + reuseExistingBranch: false, + updatedArtifacts: [{ type: 'deletion', path: 'dummy' }], + }) + ).toMatchInlineSnapshot(` + Object { + "branchExists": true, + "prNo": undefined, + "result": "done", + } + `); + expect(commit.commitFilesToBranch).toHaveBeenCalled(); + }); + it('executes post-upgrade tasks if trust is high', async () => { const updatedPackageFile: File = { type: 'addition', diff --git a/lib/workers/branch/index.ts b/lib/workers/branch/index.ts index 11d79a3449b4dbc0872c2177ba0e12d519f31f72..d0d301718824debba2e782d10a8a253b679bc929 100644 --- a/lib/workers/branch/index.ts +++ b/lib/workers/branch/index.ts @@ -440,6 +440,26 @@ export async function processBranch( } const forcedManually = userRebaseRequested || !branchExists; config.forceCommit = forcedManually || branchPr?.isConflicted; + + config.stopUpdating = branchPr?.labels?.includes(config.stopUpdatingLabel); + + const prRebaseChecked = branchPr?.body?.includes( + `- [x] <!-- rebase-check -->` + ); + + if (branchExists && dependencyDashboardCheck && config.stopUpdating) { + if (!prRebaseChecked) { + logger.info( + 'Branch updating is skipped because stopUpdatingLabel is present in config' + ); + return { + branchExists: true, + prNo: branchPr?.number, + result: BranchResult.NoWork, + }; + } + } + const commitSha = await commitFilesToBranch(config); // istanbul ignore if if (branchPr && platform.refreshPr) { diff --git a/lib/workers/pr/body/config-description.spec.ts b/lib/workers/pr/body/config-description.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..90d5d94cb4e2c54255575a9335828ab200157194 --- /dev/null +++ b/lib/workers/pr/body/config-description.spec.ts @@ -0,0 +1,23 @@ +import { mock } from 'jest-mock-extended'; +import { BranchConfig } from '../../types'; +import { getPrConfigDescription } from './config-description'; + +jest.mock('../../../util/git'); + +describe('workers/pr/body/config-description', () => { + describe('getPrConfigDescription', () => { + let branchConfig: BranchConfig; + beforeEach(() => { + jest.resetAllMocks(); + branchConfig = mock<BranchConfig>(); + branchConfig.branchName = 'branchName'; + }); + + it('handles stopUpdatingLabel correctly', async () => { + branchConfig.stopUpdating = true; + expect(await getPrConfigDescription(branchConfig)).toContain( + `**Rebasing**: Never, or you tick the rebase/retry checkbox.` + ); + }); + }); +}); diff --git a/lib/workers/pr/body/config-description.ts b/lib/workers/pr/body/config-description.ts index 17ac4d82d1202a580c4386516e7fb303ca3cefcb..3dc80349749af9dcc573782280f1fba17e49955e 100644 --- a/lib/workers/pr/body/config-description.ts +++ b/lib/workers/pr/body/config-description.ts @@ -44,7 +44,7 @@ export async function getPrConfigDescription( prBody += emojify(':recycle: **Rebasing**: '); if (config.rebaseWhen === 'behind-base-branch') { prBody += 'Whenever PR is behind base branch'; - } else if (config.rebaseWhen === 'never') { + } else if (config.rebaseWhen === 'never' || config.stopUpdating) { prBody += 'Never'; } else { prBody += 'Whenever PR becomes conflicted'; diff --git a/lib/workers/types.ts b/lib/workers/types.ts index b007f84e1c46861da4ea6c56d42b39e1b2f805af..d311d0c18a785279f1c791c56dce080225cd9041 100644 --- a/lib/workers/types.ts +++ b/lib/workers/types.ts @@ -115,4 +115,5 @@ export interface BranchConfig packageFiles?: Record<string, PackageFile[]>; prBlockedBy?: PrBlockedBy; prNo?: number; + stopUpdating?: boolean; }