From a133bb96afe193679e487552519f27589dfd8496 Mon Sep 17 00:00:00 2001 From: Oleg Krivtsov <olegkrivtsov@gmail.com> Date: Fri, 21 Jan 2022 15:33:22 +0700 Subject: [PATCH] feat(workers/branch): allow to define a blocked label (#12164) Co-authored-by: Rhys Arkins <rhys@arkins.net> --- docs/usage/configuration-options.md | 5 ++ lib/config/options/index.ts | 7 ++ lib/config/types.ts | 1 + lib/workers/branch/index.spec.ts | 70 +++++++++++++++++++ lib/workers/branch/index.ts | 20 ++++++ .../pr/body/config-description.spec.ts | 23 ++++++ lib/workers/pr/body/config-description.ts | 2 +- lib/workers/types.ts | 1 + 8 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 lib/workers/pr/body/config-description.spec.ts diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index d87ec95912..8df0079799 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 fb738ad170..34cceaece7 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 258b29e3d5..e102ae643c 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 c7615c3898..cfaed31930 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 11d79a3449..d0d3017188 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 0000000000..90d5d94cb4 --- /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 17ac4d82d1..3dc8034974 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 b007f84e1c..d311d0c18a 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; } -- GitLab