Skip to content

Commit

Permalink
feat: update labels when config changes (#25340)
Browse files Browse the repository at this point in the history
Co-authored-by: Rhys Arkins <rhys@arkins.net>
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com>
  • Loading branch information
4 people committed Mar 30, 2024
1 parent f9ba846 commit f889079
Show file tree
Hide file tree
Showing 17 changed files with 568 additions and 23 deletions.
6 changes: 3 additions & 3 deletions docs/usage/configuration-options.md
Expand Up @@ -2165,10 +2165,10 @@ Consider this example:

With the above config, every PR raised by Renovate will have the label `dependencies` while PRs containing `eslint`-related packages will instead have the label `linting`.

Renovate only adds labels when it creates the PR, which means:
Behaviour details:

- If you remove labels which Renovate added, it won't re-apply them
- If you change your config, the new/changed labels are not applied to any open PRs
- On GitHub, GitLab and Gitea: Renovate will keep PR labels in sync with configured labels, provided that no other user or bot has made changes to the labels after PR creation. If labels are changed by any other account, Renovate will stop making further changes.
- For other platforms, Renovate will add labels only at time of PR creation and not update them after that.

The `labels` array is non-mergeable, meaning if multiple `packageRules` match then Renovate uses the last value for `labels`.
If you want to add/combine labels, use the `addLabels` config option, which is mergeable.
Expand Down
21 changes: 9 additions & 12 deletions lib/modules/platform/gitea/gitea-helper.spec.ts
Expand Up @@ -387,8 +387,7 @@ describe('modules/platform/gitea/gitea-helper', () => {
.patch(`/repos/${mockRepo.full_name}/pulls/${mockPR.number}`)
.reply(200);

const res = await closePR(mockRepo.full_name, mockPR.number);
expect(res).toBeUndefined();
await expect(closePR(mockRepo.full_name, mockPR.number)).toResolve();
});
});

Expand All @@ -399,10 +398,11 @@ describe('modules/platform/gitea/gitea-helper', () => {
.post(`/repos/${mockRepo.full_name}/pulls/${mockPR.number}/merge`)
.reply(200);

const res = await mergePR(mockRepo.full_name, mockPR.number, {
Do: 'rebase',
});
expect(res).toBeUndefined();
await expect(
mergePR(mockRepo.full_name, mockPR.number, {
Do: 'rebase',
}),
).toResolve();
});
});

Expand Down Expand Up @@ -579,12 +579,9 @@ describe('modules/platform/gitea/gitea-helper', () => {
)
.reply(200);

const res = await unassignLabel(
mockRepo.full_name,
mockIssue.number,
mockLabel.id,
);
expect(res).toBeUndefined();
await expect(
unassignLabel(mockRepo.full_name, mockIssue.number, mockLabel.id),
).toResolve();
});
});

Expand Down
87 changes: 87 additions & 0 deletions lib/modules/platform/gitea/index.spec.ts
Expand Up @@ -112,6 +112,12 @@ describe('modules/platform/gitea/index', () => {
sha: 'other-head-sha' as LongCommitSha,
repo: partial<Repo>({ full_name: mockRepo.full_name }),
},
labels: [
{
id: 1,
name: 'bug',
},
],
}),
partial<MockPr>({
number: 3,
Expand Down Expand Up @@ -1850,6 +1856,87 @@ describe('modules/platform/gitea/index', () => {
}),
).toResolve();
});

it('should update labels', async () => {
const updatedMockPR = partial<PR>({
...mockPRs[0],
number: 1,
title: 'New Title',
body: 'New Body',
state: 'open',
labels: [
{
id: 1,
name: 'some-label',
},
],
});
const scope = httpMock
.scope('https://gitea.com/api/v1')
.get('/repos/some/repo/pulls')
.query({ state: 'all', sort: 'recentupdate' })
.reply(200, mockPRs)
.get('/repos/some/repo/labels')
.reply(200, mockRepoLabels)
.get('/orgs/some/labels')
.reply(200, mockOrgLabels)
.patch('/repos/some/repo/pulls/1')
.reply(200, updatedMockPR);

await initFakePlatform(scope);
await initFakeRepo(scope);
await expect(
gitea.updatePr({
number: 1,
prTitle: 'New Title',
prBody: 'New Body',
state: 'open',
labels: ['some-label'],
}),
).toResolve();
});

it('should log a warning if labels could not be looked up', async () => {
const updatedMockPR = partial<PR>({
...mockPRs[0],
number: 1,
title: 'New Title',
body: 'New Body',
state: 'open',
labels: [
{
id: 1,
name: 'some-label',
},
],
});
const scope = httpMock
.scope('https://gitea.com/api/v1')
.get('/repos/some/repo/pulls')
.query({ state: 'all', sort: 'recentupdate' })
.reply(200, mockPRs)
.get('/repos/some/repo/labels')
.reply(200, mockRepoLabels)
.get('/orgs/some/labels')
.reply(200, mockOrgLabels)
.patch('/repos/some/repo/pulls/1')
.reply(200, updatedMockPR);

await initFakePlatform(scope);
await initFakeRepo(scope);
await expect(
gitea.updatePr({
number: 1,
prTitle: 'New Title',
prBody: 'New Body',
state: 'open',
labels: ['some-label', 'unavailable-label'],
}),
).toResolve();
expect(logger.warn).toHaveBeenCalledWith(
'Some labels could not be looked up. Renovate may halt label updates assuming changes by others.',
);
});
});

describe('mergePr', () => {
Expand Down
21 changes: 20 additions & 1 deletion lib/modules/platform/gitea/index.ts
Expand Up @@ -506,7 +506,7 @@ const platform: Platform = {
logger.debug(`Creating pull request: ${title} (${head} => ${base})`);
try {
const labels = Array.isArray(labelNames)
? await Promise.all(labelNames.map(lookupLabelByName))
? await map(labelNames, lookupLabelByName)
: [];
const gpr = await helper.createPR(config.repository, {
base,
Expand Down Expand Up @@ -599,6 +599,7 @@ const platform: Platform = {
number,
prTitle,
prBody: body,
labels,
state,
targetBranch,
}: UpdatePrConfig): Promise<void> {
Expand All @@ -616,6 +617,24 @@ const platform: Platform = {
prUpdateParams.base = targetBranch;
}

/**
* Update PR labels.
* In the Gitea API, labels are replaced on each update if the field is present.
* If the field is not present (i.e., undefined), labels aren't updated.
* However, the labels array must contain label IDs instead of names,
* so a lookup is performed to fetch the details (including the ID) of each label.
*/
if (Array.isArray(labels)) {
prUpdateParams.labels = (await map(labels, lookupLabelByName)).filter(
is.number,
);
if (labels.length !== prUpdateParams.labels.length) {
logger.warn(
'Some labels could not be looked up. Renovate may halt label updates assuming changes by others.',
);
}
}

const gpr = await helper.updatePR(
config.repository,
number,
Expand Down
8 changes: 8 additions & 0 deletions lib/modules/platform/gitea/types.ts
Expand Up @@ -17,6 +17,10 @@ export type CommitStatusType =
| 'unknown';
export type PRMergeMethod = 'merge' | 'rebase' | 'rebase-merge' | 'squash';

export interface GiteaLabel {
id: number;
name: string;
}
export interface PR {
number: number;
state: PRState;
Expand All @@ -40,6 +44,10 @@ export interface PR {
};
assignees?: any[];
user?: { username?: string };

// labels returned from the Gitea API are represented as an array of objects
// ref: https://docs.gitea.com/api/1.20/#tag/repository/operation/repoGetPullRequest
labels?: GiteaLabel[];
}

export interface Issue {
Expand Down
2 changes: 2 additions & 0 deletions lib/modules/platform/gitea/utils.ts
Expand Up @@ -119,8 +119,10 @@ export function toRenovatePR(data: PR, author: string | null): Pr | null {
title = title.substring(DRAFT_PREFIX.length);
isDraft = true;
}
const labels = (data?.labels ?? []).map((l) => l.name);

return {
labels,
number: data.number,
state: data.state,
title,
Expand Down
37 changes: 37 additions & 0 deletions lib/modules/platform/github/index.spec.ts
Expand Up @@ -3333,6 +3333,43 @@ describe('modules/platform/github/index', () => {

await expect(github.updatePr(pr)).toResolve();
});

it('should add and remove labels', async () => {
const pr: UpdatePrConfig = {
number: 1234,
prTitle: 'The New Title',
prBody: 'Hello world again',
state: 'closed',
targetBranch: 'new_base',
addLabels: ['new_label'],
removeLabels: ['old_label'],
};
const scope = httpMock.scope(githubApiHost);
initRepoMock(scope, 'some/repo');
await github.initRepo({ repository: 'some/repo' });
scope
.patch('/repos/some/repo/pulls/1234')
.reply(200, {
number: 91,
base: { sha: '1234' },
head: { ref: 'somebranch', repo: { full_name: 'some/repo' } },
state: 'open',
title: 'old title',
updated_at: '01-09-2022',
})
.post('/repos/some/repo/issues/1234/labels')
.reply(200, pr)
.delete('/repos/some/repo/issues/1234/labels/old_label')
.reply(200, pr);

await expect(github.updatePr(pr)).toResolve();
expect(logger.logger.debug).toHaveBeenCalledWith(
`Adding labels 'new_label' to #1234`,
);
expect(logger.logger.debug).toHaveBeenCalledWith(
`Deleting label old_label from #1234`,
);
});
});

describe('reattemptPlatformAutomerge(number, platformOptions)', () => {
Expand Down
14 changes: 14 additions & 0 deletions lib/modules/platform/github/index.ts
Expand Up @@ -1761,6 +1761,8 @@ export async function updatePr({
number: prNo,
prTitle: title,
prBody: rawBody,
addLabels: labelsToAdd,
removeLabels,
state,
targetBranch,
}: UpdatePrConfig): Promise<void> {
Expand All @@ -1783,7 +1785,19 @@ export async function updatePr({
if (config.forkToken) {
options.token = config.forkToken;
}

// Update PR labels
try {
if (labelsToAdd) {
await addLabels(prNo, labelsToAdd);
}

if (removeLabels) {
for (const label of removeLabels) {
await deleteLabel(prNo, label);
}
}

const { body: ghPr } = await githubApi.patchJson<GhRestPr>(
`repos/${config.parentRepo ?? config.repository}/pulls/${prNo}`,
options,
Expand Down
29 changes: 29 additions & 0 deletions lib/modules/platform/gitlab/index.spec.ts
Expand Up @@ -2874,6 +2874,35 @@ describe('modules/platform/gitlab/index', () => {
}),
).toResolve();
});

it('adds and removes labels', async () => {
await initPlatform('13.3.6-ee');
httpMock
.scope(gitlabApiHost)
.get(
'/api/v4/projects/undefined/merge_requests?per_page=100&scope=created_by_me',
)
.reply(200, [
{
iid: 1,
source_branch: 'branch-a',
title: 'branch a pr',
state: 'open',
},
])
.put('/api/v4/projects/undefined/merge_requests/1')
.reply(200);
await expect(
gitlab.updatePr({
number: 1,
prTitle: 'title',
prBody: 'body',
state: 'closed',
addLabels: ['new_label'],
removeLabels: ['old_label'],
}),
).toResolve();
});
});

describe('reattemptPlatformAutomerge(number, platformOptions)', () => {
Expand Down
10 changes: 10 additions & 0 deletions lib/modules/platform/gitlab/index.ts
Expand Up @@ -818,6 +818,8 @@ export async function updatePr({
number: iid,
prTitle,
prBody: description,
addLabels,
removeLabels,
state,
platformOptions,
targetBranch,
Expand All @@ -841,6 +843,14 @@ export async function updatePr({
body.target_branch = targetBranch;
}

if (addLabels) {
body.add_labels = addLabels;
}

if (removeLabels) {
body.remove_labels = removeLabels;
}

await gitlabApi.putJson(
`projects/${config.repository}/merge_requests/${iid}`,
{ body },
Expand Down

0 comments on commit f889079

Please sign in to comment.