mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-12 06:56:24 +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.
|
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
|
## minimumReleaseAge
|
||||||
|
|
||||||
This feature used to be called `stabilityDays`.
|
This feature used to be called `stabilityDays`.
|
||||||
|
|
|
@ -2844,6 +2844,13 @@ const options: RenovateOptions[] = [
|
||||||
cli: false,
|
cli: false,
|
||||||
env: 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[] {
|
export function getOptions(): RenovateOptions[] {
|
||||||
|
|
|
@ -90,6 +90,7 @@ export interface RenovateSharedConfig {
|
||||||
unicodeEmoji?: boolean;
|
unicodeEmoji?: boolean;
|
||||||
gitIgnoredAuthors?: string[];
|
gitIgnoredAuthors?: string[];
|
||||||
platformCommit?: boolean;
|
platformCommit?: boolean;
|
||||||
|
milestone?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config options used only within the global worker
|
// 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)', () => {
|
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(
|
export async function addAssignees(
|
||||||
issueNo: number,
|
issueNo: number,
|
||||||
assignees: string[],
|
assignees: string[],
|
||||||
|
@ -1658,6 +1693,7 @@ export async function createPr({
|
||||||
labels,
|
labels,
|
||||||
draftPR = false,
|
draftPR = false,
|
||||||
platformOptions,
|
platformOptions,
|
||||||
|
milestone,
|
||||||
}: CreatePRConfig): Promise<GhPr | null> {
|
}: CreatePRConfig): Promise<GhPr | null> {
|
||||||
const body = sanitize(rawBody);
|
const body = sanitize(rawBody);
|
||||||
const base = targetBranch;
|
const base = targetBranch;
|
||||||
|
@ -1697,6 +1733,7 @@ export async function createPr({
|
||||||
const { number, node_id } = result;
|
const { number, node_id } = result;
|
||||||
|
|
||||||
await addLabels(number, labels);
|
await addLabels(number, labels);
|
||||||
|
await tryAddMilestone(number, milestone);
|
||||||
await tryPrAutomerge(number, node_id, platformOptions);
|
await tryPrAutomerge(number, node_id, platformOptions);
|
||||||
|
|
||||||
cachePr(result);
|
cachePr(result);
|
||||||
|
|
|
@ -112,6 +112,7 @@ export interface CreatePRConfig {
|
||||||
labels?: string[] | null;
|
labels?: string[] | null;
|
||||||
platformOptions?: PlatformPrOptions;
|
platformOptions?: PlatformPrOptions;
|
||||||
draftPR?: boolean;
|
draftPR?: boolean;
|
||||||
|
milestone?: number;
|
||||||
}
|
}
|
||||||
export interface UpdatePrConfig {
|
export interface UpdatePrConfig {
|
||||||
number: number;
|
number: number;
|
||||||
|
|
|
@ -461,6 +461,26 @@ describe('util/http/github', () => {
|
||||||
}),
|
}),
|
||||||
).rejects.toThrow('Sorry, this is a teapot');
|
).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')
|
message.includes('Review cannot be requested from pull request author')
|
||||||
) {
|
) {
|
||||||
return err;
|
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')) {
|
} else if (err.body?.errors?.find((e: any) => e.code === 'invalid')) {
|
||||||
logger.debug({ err }, 'Received invalid response - aborting');
|
logger.debug({ err }, 'Received invalid response - aborting');
|
||||||
return new Error(REPOSITORY_CHANGED);
|
return new Error(REPOSITORY_CHANGED);
|
||||||
|
|
|
@ -437,6 +437,7 @@ export async function ensurePr(
|
||||||
labels: prepareLabels(config),
|
labels: prepareLabels(config),
|
||||||
platformOptions: getPlatformPrOptions(config),
|
platformOptions: getPlatformPrOptions(config),
|
||||||
draftPR: !!config.draftPR,
|
draftPR: !!config.draftPR,
|
||||||
|
milestone: config.milestone,
|
||||||
});
|
});
|
||||||
|
|
||||||
incLimitedValue('PullRequests');
|
incLimitedValue('PullRequests');
|
||||||
|
|
Loading…
Reference in a new issue