mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-11 22:46:27 +00:00
feat(github): Add the possibility to link a Milestone (#27343)
Co-authored-by: Michael Kriese <michael.kriese@visualon.de> Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> Co-authored-by: Rhys Arkins <rhys@arkins.net>
This commit is contained in:
parent
2c2608f2a9
commit
16589bfb69
9 changed files with 166 additions and 0 deletions
|
@ -2195,6 +2195,17 @@ Be careful with remapping `warn` or `error` messages to lower log levels, as it
|
|||
|
||||
Add to this object if you wish to define rules that apply only to major updates.
|
||||
|
||||
## milestone
|
||||
|
||||
If set to the number of an existing [GitHub milestone](https://docs.github.com/en/issues/using-labels-and-milestones-to-track-work/about-milestones), Renovate will add that milestone to its PR.
|
||||
Renovate will only add a milestone when it _creates_ the PR.
|
||||
|
||||
```json title="Example Renovate config"
|
||||
{
|
||||
"milestone": 12
|
||||
}
|
||||
```
|
||||
|
||||
## minimumReleaseAge
|
||||
|
||||
This feature used to be called `stabilityDays`.
|
||||
|
|
|
@ -2844,6 +2844,13 @@ const options: RenovateOptions[] = [
|
|||
cli: false,
|
||||
env: false,
|
||||
},
|
||||
{
|
||||
name: 'milestone',
|
||||
description: `The number of a milestone. If set, the milestone will be set when Renovate creates the PR.`,
|
||||
type: 'integer',
|
||||
default: null,
|
||||
supportedPlatforms: ['github'],
|
||||
},
|
||||
];
|
||||
|
||||
export function getOptions(): RenovateOptions[] {
|
||||
|
|
|
@ -90,6 +90,7 @@ export interface RenovateSharedConfig {
|
|||
unicodeEmoji?: boolean;
|
||||
gitIgnoredAuthors?: string[];
|
||||
platformCommit?: boolean;
|
||||
milestone?: number;
|
||||
}
|
||||
|
||||
// Config options used only within the global worker
|
||||
|
|
|
@ -2852,6 +2852,92 @@ describe('modules/platform/github/index', () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('milestone', () => {
|
||||
it('should set the milestone on the PR', async () => {
|
||||
const scope = httpMock.scope(githubApiHost);
|
||||
initRepoMock(scope, 'some/repo');
|
||||
scope
|
||||
.post(
|
||||
'/repos/some/repo/pulls',
|
||||
(body) => body.title === 'bump someDep to v2',
|
||||
)
|
||||
.reply(200, {
|
||||
number: 123,
|
||||
head: { repo: { full_name: 'some/repo' }, ref: 'some-branch' },
|
||||
});
|
||||
scope
|
||||
.patch('/repos/some/repo/issues/123', (body) => body.milestone === 1)
|
||||
.reply(200, {});
|
||||
await github.initRepo({ repository: 'some/repo' });
|
||||
const pr = await github.createPr({
|
||||
targetBranch: 'main',
|
||||
sourceBranch: 'renovate/someDep-v2',
|
||||
prTitle: 'bump someDep to v2',
|
||||
prBody: 'many informations about someDep',
|
||||
milestone: 1,
|
||||
});
|
||||
expect(pr?.number).toBe(123);
|
||||
});
|
||||
|
||||
it('should log a warning but not throw on error', async () => {
|
||||
const scope = httpMock.scope(githubApiHost);
|
||||
initRepoMock(scope, 'some/repo');
|
||||
scope
|
||||
.post(
|
||||
'/repos/some/repo/pulls',
|
||||
(body) => body.title === 'bump someDep to v2',
|
||||
)
|
||||
.reply(200, {
|
||||
number: 123,
|
||||
head: { repo: { full_name: 'some/repo' }, ref: 'some-branch' },
|
||||
});
|
||||
scope
|
||||
.patch('/repos/some/repo/issues/123', (body) => body.milestone === 1)
|
||||
.reply(422, {
|
||||
message: 'Validation Failed',
|
||||
errors: [
|
||||
{
|
||||
value: 1,
|
||||
resource: 'Issue',
|
||||
field: 'milestone',
|
||||
code: 'invalid',
|
||||
},
|
||||
],
|
||||
documentation_url:
|
||||
'https://docs.github.com/rest/issues/issues#update-an-issue',
|
||||
});
|
||||
await github.initRepo({ repository: 'some/repo' });
|
||||
const pr = await github.createPr({
|
||||
targetBranch: 'main',
|
||||
sourceBranch: 'renovate/someDep-v2',
|
||||
prTitle: 'bump someDep to v2',
|
||||
prBody: 'many informations about someDep',
|
||||
milestone: 1,
|
||||
});
|
||||
expect(pr?.number).toBe(123);
|
||||
expect(logger.logger.warn).toHaveBeenCalledWith(
|
||||
{
|
||||
err: {
|
||||
message: 'Validation Failed',
|
||||
errors: [
|
||||
{
|
||||
value: 1,
|
||||
resource: 'Issue',
|
||||
field: 'milestone',
|
||||
code: 'invalid',
|
||||
},
|
||||
],
|
||||
documentation_url:
|
||||
'https://docs.github.com/rest/issues/issues#update-an-issue',
|
||||
},
|
||||
milestone: 1,
|
||||
pr: 123,
|
||||
},
|
||||
'Unable to add milestone to PR',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPr(prNo)', () => {
|
||||
|
|
|
@ -1378,6 +1378,41 @@ export async function ensureIssueClosing(title: string): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
async function tryAddMilestone(
|
||||
issueNo: number,
|
||||
milestoneNo: number | undefined,
|
||||
): Promise<void> {
|
||||
if (!milestoneNo) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
{
|
||||
milestone: milestoneNo,
|
||||
pr: issueNo,
|
||||
},
|
||||
'Adding milestone to PR',
|
||||
);
|
||||
const repository = config.parentRepo ?? config.repository;
|
||||
try {
|
||||
await githubApi.patchJson(`repos/${repository}/issues/${issueNo}`, {
|
||||
body: {
|
||||
milestone: milestoneNo,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
const actualError = err.response?.body || /* istanbul ignore next */ err;
|
||||
logger.warn(
|
||||
{
|
||||
milestone: milestoneNo,
|
||||
pr: issueNo,
|
||||
err: actualError,
|
||||
},
|
||||
'Unable to add milestone to PR',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function addAssignees(
|
||||
issueNo: number,
|
||||
assignees: string[],
|
||||
|
@ -1658,6 +1693,7 @@ export async function createPr({
|
|||
labels,
|
||||
draftPR = false,
|
||||
platformOptions,
|
||||
milestone,
|
||||
}: CreatePRConfig): Promise<GhPr | null> {
|
||||
const body = sanitize(rawBody);
|
||||
const base = targetBranch;
|
||||
|
@ -1697,6 +1733,7 @@ export async function createPr({
|
|||
const { number, node_id } = result;
|
||||
|
||||
await addLabels(number, labels);
|
||||
await tryAddMilestone(number, milestone);
|
||||
await tryPrAutomerge(number, node_id, platformOptions);
|
||||
|
||||
cachePr(result);
|
||||
|
|
|
@ -112,6 +112,7 @@ export interface CreatePRConfig {
|
|||
labels?: string[] | null;
|
||||
platformOptions?: PlatformPrOptions;
|
||||
draftPR?: boolean;
|
||||
milestone?: number;
|
||||
}
|
||||
export interface UpdatePrConfig {
|
||||
number: number;
|
||||
|
|
|
@ -461,6 +461,26 @@ describe('util/http/github', () => {
|
|||
}),
|
||||
).rejects.toThrow('Sorry, this is a teapot');
|
||||
});
|
||||
|
||||
it('should throw original error when milestone not found', async () => {
|
||||
const milestoneNotFoundError = {
|
||||
message: 'Validation Failed',
|
||||
errors: [
|
||||
{
|
||||
value: 1,
|
||||
resource: 'Issue',
|
||||
field: 'milestone',
|
||||
code: 'invalid',
|
||||
},
|
||||
],
|
||||
documentation_url:
|
||||
'https://docs.github.com/rest/issues/issues#update-an-issue',
|
||||
};
|
||||
|
||||
await expect(fail(422, milestoneNotFoundError)).rejects.toThrow(
|
||||
'Validation Failed',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -135,6 +135,8 @@ function handleGotError(
|
|||
message.includes('Review cannot be requested from pull request author')
|
||||
) {
|
||||
return err;
|
||||
} else if (err.body?.errors?.find((e: any) => e.field === 'milestone')) {
|
||||
return err;
|
||||
} else if (err.body?.errors?.find((e: any) => e.code === 'invalid')) {
|
||||
logger.debug({ err }, 'Received invalid response - aborting');
|
||||
return new Error(REPOSITORY_CHANGED);
|
||||
|
|
|
@ -437,6 +437,7 @@ export async function ensurePr(
|
|||
labels: prepareLabels(config),
|
||||
platformOptions: getPlatformPrOptions(config),
|
||||
draftPR: !!config.draftPR,
|
||||
milestone: config.milestone,
|
||||
});
|
||||
|
||||
incLimitedValue('PullRequests');
|
||||
|
|
Loading…
Reference in a new issue