diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 6b85afa5b1131f..6c79553825d028 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -337,6 +337,10 @@ You can also use the special `"$default"` string to denote the repository's defa Do _not_ use the `baseBranches` config option when you've set a `forkToken`. You may need a `forkToken` when you're using the Forking Renovate app. +## bbAutoResolvePrTasks + +Configuring this to `true` means that Renovate will mark all PR Tasks as complete. + ## bbUseDefaultReviewers Configuring this to `true` means that Renovate will detect and apply the default reviewers rules to PRs (Bitbucket only). diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index fa6ccce4b974f4..da33196557f253 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -1893,6 +1893,14 @@ const options: RenovateOptions[] = [ cli: false, env: false, }, + { + name: 'bbAutoResolvePrTasks', + description: + 'The PR tasks will be automatically completed after the PR is raised.', + type: 'boolean', + default: false, + supportedPlatforms: ['bitbucket'], + }, { name: 'bbUseDefaultReviewers', description: 'Use the default reviewers (Bitbucket only).', diff --git a/lib/modules/platform/bitbucket/index.spec.ts b/lib/modules/platform/bitbucket/index.spec.ts index 9de64419b236dc..92ca09c7817f29 100644 --- a/lib/modules/platform/bitbucket/index.spec.ts +++ b/lib/modules/platform/bitbucket/index.spec.ts @@ -4,6 +4,7 @@ import { reset as memCacheReset } from '../../../util/cache/memory'; import type * as _git from '../../../util/git'; import { setBaseUrl } from '../../../util/http/bitbucket'; import type { Platform, PlatformResult, RepoParams } from '../types'; +import type { PrTask } from './schema'; jest.mock('../../../util/git'); jest.mock('../../../util/host-rules'); @@ -1436,6 +1437,157 @@ describe('modules/platform/bitbucket/index', () => { }), ).rejects.toThrow(new Error('Response code 400 (Bad Request)')); }); + + it('lists PR tasks and resolves the unresolved tasks', async () => { + const prTask1: PrTask = { + id: 1, + state: 'UNRESOLVED', + content: { + raw: 'task 1', + }, + }; + const resolvedPrTask1: Partial = { + state: 'RESOLVED', + content: { + raw: 'task 1', + }, + }; + const prTask2: PrTask = { + id: 2, + state: 'UNRESOLVED', + content: { + raw: 'task 2', + }, + }; + const resolvedPrTask2: Partial = { + state: 'RESOLVED', + content: { + raw: 'task 2', + }, + }; + const prTask3: PrTask = { + id: 3, + state: 'RESOLVED', + content: { + raw: 'task 3', + }, + }; + const scope = await initRepoMock(); + scope + .post('/2.0/repositories/some/repo/pullrequests') + .reply(200, { id: 5 }) + .get(`/2.0/repositories/some/repo/pullrequests`) + .query(true) + .reply(200, { + values: [{ id: 5 }], + }); + scope + .get('/2.0/repositories/some/repo/pullrequests/5/tasks') + .query({ + pagelen: 100, + }) + .reply(200, { values: [prTask1, prTask2, prTask3] }); + scope + .put( + '/2.0/repositories/some/repo/pullrequests/5/tasks/1', + resolvedPrTask1, + ) + .reply(200); + scope + .put( + '/2.0/repositories/some/repo/pullrequests/5/tasks/2', + resolvedPrTask2, + ) + .reply(200); + const pr = await bitbucket.createPr({ + sourceBranch: 'branch', + targetBranch: 'master', + prTitle: 'title', + prBody: 'body', + platformPrOptions: { + bbUseDefaultReviewers: false, + bbAutoResolvePrTasks: true, + }, + }); + expect(pr?.number).toBe(5); + }); + + it('swallows list PR error and PR creation succeeds', async () => { + const scope = await initRepoMock(); + scope + .post('/2.0/repositories/some/repo/pullrequests') + .reply(200, { id: 5 }) + .get(`/2.0/repositories/some/repo/pullrequests`) + .query(true) + .reply(200, { + values: [{ id: 5 }], + }); + scope + .get('/2.0/repositories/some/repo/pullrequests/5/tasks') + .query({ + pagelen: 100, + }) + .reply(500); + const pr = await bitbucket.createPr({ + sourceBranch: 'branch', + targetBranch: 'master', + prTitle: 'title', + prBody: 'body', + platformPrOptions: { + bbUseDefaultReviewers: false, + bbAutoResolvePrTasks: true, + }, + }); + expect(pr?.number).toBe(5); + }); + + it('swallows resolve PR task error and PR creation succeeds', async () => { + const prTask1: PrTask = { + id: 1, + state: 'UNRESOLVED', + content: { + raw: 'task 1', + }, + }; + const resolvedPrTask1: Partial = { + state: 'RESOLVED', + content: { + raw: 'task 1', + }, + }; + const scope = await initRepoMock(); + scope + .post('/2.0/repositories/some/repo/pullrequests') + .reply(200, { id: 5 }) + .get(`/2.0/repositories/some/repo/pullrequests`) + .query(true) + .reply(200, { + values: [{ id: 5 }], + }); + scope + .get('/2.0/repositories/some/repo/pullrequests/5/tasks') + .query({ + pagelen: 100, + }) + .reply(200, { values: [prTask1] }); + scope + .put( + '/2.0/repositories/some/repo/pullrequests/5/tasks/1', + resolvedPrTask1, + ) + .reply(500); + const pr = await bitbucket.createPr({ + sourceBranch: 'branch', + targetBranch: 'master', + prTitle: 'title', + prBody: 'body', + platformPrOptions: { + bbUseDefaultReviewers: false, + bbAutoResolvePrTasks: true, + }, + }); + expect(pr?.number).toBe(5); + }); }); describe('getPr()', () => { diff --git a/lib/modules/platform/bitbucket/index.ts b/lib/modules/platform/bitbucket/index.ts index 4ebd23684d6031..195f4a65b4a2c1 100644 --- a/lib/modules/platform/bitbucket/index.ts +++ b/lib/modules/platform/bitbucket/index.ts @@ -35,7 +35,7 @@ import { smartTruncate } from '../utils/pr-body'; import { readOnlyIssueBody } from '../utils/read-only-issue-body'; import * as comments from './comments'; import { BitbucketPrCache } from './pr-cache'; -import { RepoInfo, Repositories } from './schema'; +import { RepoInfo, Repositories, UnresolvedPrTasks } from './schema'; import type { Account, BitbucketStatus, @@ -925,6 +925,9 @@ export async function createPr({ renovateUserUuid, pr, ); + if (platformPrOptions?.bbAutoResolvePrTasks) { + await autoResolvePrTasks(pr); + } return pr; } catch (err) /* istanbul ignore next */ { // Try sanitizing reviewers @@ -952,11 +955,58 @@ export async function createPr({ renovateUserUuid, pr, ); + if (platformPrOptions?.bbAutoResolvePrTasks) { + await autoResolvePrTasks(pr); + } return pr; } } } +async function autoResolvePrTasks(pr: Pr): Promise { + logger.debug(`Auto resolve PR tasks in #${pr.number}`); + try { + const unResolvedTasks = ( + await bitbucketHttp.getJson( + `/2.0/repositories/${config.repository}/pullrequests/${pr.number}/tasks`, + { paginate: true, pagelen: 100 }, + UnresolvedPrTasks, + ) + ).body; + + logger.trace( + { + prId: pr.number, + listTaskRes: unResolvedTasks, + }, + 'List PR tasks', + ); + + for (const task of unResolvedTasks) { + const res = await bitbucketHttp.putJson( + `/2.0/repositories/${config.repository}/pullrequests/${pr.number}/tasks/${task.id}`, + { + body: { + state: 'RESOLVED', + content: { + raw: task.content.raw, + }, + }, + }, + ); + logger.trace( + { + prId: pr.number, + updateTaskResponse: res, + }, + 'Put PR tasks - mark resolved', + ); + } + } catch (err) { + logger.warn({ prId: pr.number, err }, 'Error resolving PR tasks'); + } +} + export async function updatePr({ number: prNo, prTitle: title, diff --git a/lib/modules/platform/bitbucket/schema.ts b/lib/modules/platform/bitbucket/schema.ts index 679073591b6f25..a4f7e336d7d969 100644 --- a/lib/modules/platform/bitbucket/schema.ts +++ b/lib/modules/platform/bitbucket/schema.ts @@ -74,3 +74,23 @@ export const Repositories = z values: LooseArray(RepoInfo), }) .transform((body) => body.values); + +const TaskState = z.union([z.literal('RESOLVED'), z.literal('UNRESOLVED')]); + +const PrTask = z.object({ + id: z.number(), + state: TaskState, + content: z.object({ + raw: z.string(), + }), +}); + +export type PrTask = z.infer; + +export const UnresolvedPrTasks = z + .object({ + values: z.array(PrTask), + }) + .transform((data) => + data.values.filter((task) => task.state === 'UNRESOLVED'), + ); diff --git a/lib/modules/platform/types.ts b/lib/modules/platform/types.ts index 8a98fe9b4b2cc0..752d98d779b274 100644 --- a/lib/modules/platform/types.ts +++ b/lib/modules/platform/types.ts @@ -101,6 +101,7 @@ export type PlatformPrOptions = { automergeStrategy?: MergeStrategy; azureWorkItemId?: number; bbUseDefaultReviewers?: boolean; + bbAutoResolvePrTasks?: boolean; gitLabIgnoreApprovals?: boolean; usePlatformAutomerge?: boolean; forkModeDisallowMaintainerEdits?: boolean; diff --git a/lib/workers/repository/update/pr/index.ts b/lib/workers/repository/update/pr/index.ts index f57b619c02acb1..94e341790e080c 100644 --- a/lib/workers/repository/update/pr/index.ts +++ b/lib/workers/repository/update/pr/index.ts @@ -57,6 +57,7 @@ export function getPlatformPrOptions( autoApprove: !!config.autoApprove, automergeStrategy: config.automergeStrategy, azureWorkItemId: config.azureWorkItemId ?? 0, + bbAutoResolvePrTasks: !!config.bbAutoResolvePrTasks, bbUseDefaultReviewers: !!config.bbUseDefaultReviewers, gitLabIgnoreApprovals: !!config.gitLabIgnoreApprovals, forkModeDisallowMaintainerEdits: !!config.forkModeDisallowMaintainerEdits,