Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(bitbucket): support task autocomplete #30901

Merged
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c2358bf
refactor - return Pr at the end of the function
dandandy Aug 19, 2024
f92f192
add new platform pr option bbAutoCompletePrTasks
dandandy Aug 19, 2024
807acde
list PR tasks and update task state to resolved
dandandy Aug 19, 2024
525d0af
clean up logging statements
dandandy Aug 25, 2024
2b350c1
put auto resolve tasks in try catch block
dandandy Aug 25, 2024
92832f3
check pr is not null/undefined
dandandy Aug 25, 2024
722a2b7
don't log repository, it's already in metadata
dandandy Aug 25, 2024
b0b414d
make bbAutoResolvePrTasks optional incase it's not specified. Fixes b…
dandandy Aug 25, 2024
880447f
add tests for resolve bb pr tasks
dandandy Aug 25, 2024
fad7a16
Merge branch 'main' into feat/27485-bitbucket-task-autocomplete
dandandy Aug 25, 2024
83e5702
refactor - move auotResolvePrTask logic to private function, switch p…
dandandy Aug 25, 2024
1b00595
fix eslint error
dandandy Aug 25, 2024
bc882f6
trace instead of debug
dandandy Aug 26, 2024
285be40
just log pr number
dandandy Aug 26, 2024
6903e32
use zod validation
dandandy Aug 26, 2024
2bd439d
remove underscore from variable
dandandy Aug 26, 2024
13c5974
move to schema file
dandandy Aug 26, 2024
bb5ee9e
fix types in test file
dandandy Aug 26, 2024
3ec5fa3
Merge branch 'main' into feat/27485-bitbucket-task-autocomplete
dandandy Aug 26, 2024
1d93199
debug string instead of object
dandandy Aug 27, 2024
be8be4d
add prId to warn log
dandandy Aug 27, 2024
3a15251
use an starting uppercase letter for schemas
dandandy Sep 6, 2024
95d33c2
use an starting uppercase letter for schemas
dandandy Sep 6, 2024
95da535
use an starting uppercase letter for schemas
dandandy Sep 6, 2024
06969d1
use an starting uppercase letter for schemas
dandandy Sep 6, 2024
5b08e03
use an starting uppercase letter for schemas
dandandy Sep 6, 2024
b7c99eb
typescript knows the types
dandandy Sep 6, 2024
890c0e7
schema validation done in http layer
dandandy Sep 6, 2024
9d255c2
fix prettier error
dandandy Sep 6, 2024
9ed86bd
Merge branch 'main' into feat/27485-bitbucket-task-autocomplete
dandandy Sep 6, 2024
85aa188
Merge branch 'main' into feat/27485-bitbucket-task-autocomplete
dandandy Sep 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/usage/configuration-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
8 changes: 8 additions & 0 deletions lib/config/options/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).',
Expand Down
152 changes: 152 additions & 0 deletions lib/modules/platform/bitbucket/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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<PrTask> = {
state: 'RESOLVED',
content: {
raw: 'task 1',
},
};
const prTask2: PrTask = {
id: 2,
state: 'UNRESOLVED',
content: {
raw: 'task 2',
},
};
const resolvedPrTask2: Partial<PrTask> = {
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<PrTask> = {
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()', () => {
Expand Down
54 changes: 53 additions & 1 deletion lib/modules/platform/bitbucket/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ 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 type { PrTask } from './schema';
import { RepoInfo, Repositories, UnresolvedPrTasks } from './schema';
import type {
Account,
BitbucketStatus,
Expand Down Expand Up @@ -925,6 +926,9 @@ export async function createPr({
renovateUserUuid,
pr,
);
if (platformPrOptions?.bbAutoResolvePrTasks) {
await autoResolvePrTasks(pr);
}
return pr;
} catch (err) /* istanbul ignore next */ {
// Try sanitizing reviewers
Expand Down Expand Up @@ -952,11 +956,59 @@ export async function createPr({
renovateUserUuid,
pr,
);
if (platformPrOptions?.bbAutoResolvePrTasks) {
await autoResolvePrTasks(pr);
}
return pr;
}
}
}

async function autoResolvePrTasks(pr: Pr): Promise<void> {
logger.debug(`Auto resolve PR tasks in #${pr.number}`);
try {
const response = (
await bitbucketHttp.getJson(
`/2.0/repositories/${config.repository}/pullrequests/${pr.number}/tasks`,
{ paginate: true, pagelen: 100 },
)
).body;

logger.trace(
{
prId: pr.number,
listTaskRes: response,
},
'List PR tasks',
);

const unResolvedTasks: Array<PrTask> = UnresolvedPrTasks.parse(response);
dandandy marked this conversation as resolved.
Show resolved Hide resolved

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,
},
dandandy marked this conversation as resolved.
Show resolved Hide resolved
},
},
);
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,
Expand Down
21 changes: 21 additions & 0 deletions lib/modules/platform/bitbucket/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,24 @@ export const Repositories = z
values: LooseArray(RepoInfo),
})
.transform((body) => body.values);

const taskState = z.union([z.literal('RESOLVED'), z.literal('UNRESOLVED')]);
dandandy marked this conversation as resolved.
Show resolved Hide resolved

const prTask = z.object({
dandandy marked this conversation as resolved.
Show resolved Hide resolved
id: z.number(),
state: taskState,
dandandy marked this conversation as resolved.
Show resolved Hide resolved
content: z.object({
raw: z.string(),
}),
});

export type PrTask = z.infer<typeof prTask>;
dandandy marked this conversation as resolved.
Show resolved Hide resolved

export const UnresolvedPrTasks = z
.object({
values: z.array(prTask),
dandandy marked this conversation as resolved.
Show resolved Hide resolved
})
.transform(
(data): Array<PrTask> =>
data.values.filter((task: PrTask) => task.state === 'UNRESOLVED'),
dandandy marked this conversation as resolved.
Show resolved Hide resolved
);
1 change: 1 addition & 0 deletions lib/modules/platform/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export type PlatformPrOptions = {
automergeStrategy?: MergeStrategy;
azureWorkItemId?: number;
bbUseDefaultReviewers?: boolean;
bbAutoResolvePrTasks?: boolean;
gitLabIgnoreApprovals?: boolean;
usePlatformAutomerge?: boolean;
forkModeDisallowMaintainerEdits?: boolean;
Expand Down
1 change: 1 addition & 0 deletions lib/workers/repository/update/pr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down