Skip to content

Commit

Permalink
feat(github): Add the possibility to link a Milestone (#27343)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
4 people committed Feb 21, 2024
1 parent 2c2608f commit 16589bf
Show file tree
Hide file tree
Showing 9 changed files with 166 additions and 0 deletions.
11 changes: 11 additions & 0 deletions docs/usage/configuration-options.md
Expand Up @@ -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`.
Expand Down
7 changes: 7 additions & 0 deletions lib/config/options/index.ts
Expand Up @@ -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[] {
Expand Down
1 change: 1 addition & 0 deletions lib/config/types.ts
Expand Up @@ -90,6 +90,7 @@ export interface RenovateSharedConfig {
unicodeEmoji?: boolean;
gitIgnoredAuthors?: string[];
platformCommit?: boolean;
milestone?: number;
}

// Config options used only within the global worker
Expand Down
86 changes: 86 additions & 0 deletions lib/modules/platform/github/index.spec.ts
Expand Up @@ -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)', () => {
Expand Down
37 changes: 37 additions & 0 deletions lib/modules/platform/github/index.ts
Expand Up @@ -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[],
Expand Down Expand Up @@ -1658,6 +1693,7 @@ export async function createPr({
labels,
draftPR = false,
platformOptions,
milestone,
}: CreatePRConfig): Promise<GhPr | null> {
const body = sanitize(rawBody);
const base = targetBranch;
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions lib/modules/platform/types.ts
Expand Up @@ -112,6 +112,7 @@ export interface CreatePRConfig {
labels?: string[] | null;
platformOptions?: PlatformPrOptions;
draftPR?: boolean;
milestone?: number;
}
export interface UpdatePrConfig {
number: number;
Expand Down
20 changes: 20 additions & 0 deletions lib/util/http/github.spec.ts
Expand Up @@ -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',
);
});
});
});

Expand Down
2 changes: 2 additions & 0 deletions lib/util/http/github.ts
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions lib/workers/repository/update/pr/index.ts
Expand Up @@ -437,6 +437,7 @@ export async function ensurePr(
labels: prepareLabels(config),
platformOptions: getPlatformPrOptions(config),
draftPR: !!config.draftPR,
milestone: config.milestone,
});

incLimitedValue('PullRequests');
Expand Down

0 comments on commit 16589bf

Please sign in to comment.