diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 84b388f31ffe9639eabe23ea7b3e362b6000a8e5..903ca5354af5b421dafefa28465f0b5e0ed22553 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -441,6 +441,11 @@ You can configure this to `true` if you prefer Renovate to close an existing Dep ## dependencyDashboardHeader +## dependencyDashboardLabels + +The labels only get updated when the Dependency Dashboard issue updates its content and/or title. +It is pointless to edit the labels, as Renovate bot restores the labels on each run. + ## dependencyDashboardTitle Configure this option if you prefer a different title for the Dependency Dashboard. diff --git a/lib/config/definitions.ts b/lib/config/definitions.ts index b59c9ae23f22fe5a69cc1699db4f0f0f5e837aac..5fd76556985d31f12ebc14f09dbf153d8bc1f498 100644 --- a/lib/config/definitions.ts +++ b/lib/config/definitions.ts @@ -419,6 +419,13 @@ const options: RenovateOptions[] = [ 'Any text added here will be placed last in the Dependency Dashboard issue body, with a divider separator before it.', type: 'string', }, + { + name: 'dependencyDashboardLabels', + description: + 'These labels will always be applied on the Dependency Dashboard issue, even when they have been removed manually.', + type: 'array', + subType: 'string', + }, { name: 'configWarningReuseIssue', description: diff --git a/lib/config/types.ts b/lib/config/types.ts index 1e99efe1ba410842d6dd0f2759afb6476e5fdaca..3fa1a645c2a7c7125b0ace70110022546993cc67 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -182,6 +182,7 @@ export interface RenovateConfig dependencyDashboardTitle?: string; dependencyDashboardHeader?: string; dependencyDashboardFooter?: string; + dependencyDashboardLabels?: string[]; packageFile?: string; packageRules?: PackageRule[]; postUpdateOptions?: string[]; diff --git a/lib/platform/gitea/__snapshots__/gitea-helper.spec.ts.snap b/lib/platform/gitea/__snapshots__/gitea-helper.spec.ts.snap index 5be3dfd1533198c8042f0bbb074bbf63b45f5173..2d4ed35815b745add455edd6175810156878b928 100644 --- a/lib/platform/gitea/__snapshots__/gitea-helper.spec.ts.snap +++ b/lib/platform/gitea/__snapshots__/gitea-helper.spec.ts.snap @@ -765,6 +765,24 @@ Array [ ] `; +exports[`platform/gitea/gitea-helper updateIssueLabels should call /api/v1/repos/[repo]/issues/[issue]/labels endpoint 1`] = ` +Array [ + Object { + "body": "{\\"labels\\":[1,3]}", + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "content-length": "16", + "content-type": "application/json", + "host": "gitea.renovatebot.com", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "PUT", + "url": "https://gitea.renovatebot.com/api/v1/repos/some/repo/issues/7/labels", + }, +] +`; + exports[`platform/gitea/gitea-helper updatePR should call /api/v1/repos/[repo]/pulls/[pull] endpoint 1`] = ` Array [ Object { diff --git a/lib/platform/gitea/gitea-helper.spec.ts b/lib/platform/gitea/gitea-helper.spec.ts index 4095b539dc8004931ecf8018008351ce0ca66195..4d1ea8a3e905101ffb887d36755c5e3ea988095e 100644 --- a/lib/platform/gitea/gitea-helper.spec.ts +++ b/lib/platform/gitea/gitea-helper.spec.ts @@ -85,6 +85,7 @@ describe(getName(), () => { title: 'Some Issue', body: 'just some issue', assignees: [mockUser], + labels: [], }; const mockComment: ght.Comment = { @@ -469,6 +470,30 @@ describe(getName(), () => { }); }); + describe('updateIssueLabels', () => { + it('should call /api/v1/repos/[repo]/issues/[issue]/labels endpoint', async () => { + const updatedMockLabels: Partial<ght.Label>[] = [ + { id: 1, name: 'Renovate' }, + { id: 3, name: 'Maintenance' }, + ]; + + httpMock + .scope(baseUrl) + .put(`/repos/${mockRepo.full_name}/issues/${mockIssue.number}/labels`) + .reply(200, updatedMockLabels); + + const res = await ght.updateIssueLabels( + mockRepo.full_name, + mockIssue.number, + { + labels: [1, 3], + } + ); + expect(res).toEqual(updatedMockLabels); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + }); + describe('closeIssue', () => { it('should call /api/v1/repos/[repo]/issues/[issue] endpoint', async () => { httpMock diff --git a/lib/platform/gitea/gitea-helper.ts b/lib/platform/gitea/gitea-helper.ts index 88e2bec406546311add2b229f6c36a3e2cbd9215..2e9a3af680f742800c1deec6edc8ceafa8865bfd 100644 --- a/lib/platform/gitea/gitea-helper.ts +++ b/lib/platform/gitea/gitea-helper.ts @@ -46,6 +46,7 @@ export interface Issue { title: string; body: string; assignees: User[]; + labels: Label[]; } export interface User { @@ -135,7 +136,8 @@ export type RepoSearchParams = { archived?: boolean; }; -export type IssueCreateParams = IssueUpdateParams; +export type IssueCreateParams = Partial<IssueUpdateLabelsParams> & + IssueUpdateParams; export type IssueUpdateParams = { title?: string; @@ -144,6 +146,10 @@ export type IssueUpdateParams = { assignees?: string[]; }; +export type IssueUpdateLabelsParams = { + labels: number[]; +}; + export type IssueSearchParams = { state?: IssueState; }; @@ -374,6 +380,21 @@ export async function updateIssue( return res.body; } +export async function updateIssueLabels( + repoPath: string, + idx: number, + params: IssueUpdateLabelsParams, + options?: GiteaHttpOptions +): Promise<Label[]> { + const url = `repos/${repoPath}/issues/${idx}/labels`; + const res = await giteaHttp.putJson<Label[]>(url, { + ...options, + body: params, + }); + + return res.body; +} + export async function closeIssue( repoPath: string, idx: number, diff --git a/lib/platform/gitea/index.spec.ts b/lib/platform/gitea/index.spec.ts index d5ab8c01d8dcd1ca2cfb1af2ffe2a00650250f6f..6a0ade67ca10f40b3d994ef1b05bf4ea60ac58cf 100644 --- a/lib/platform/gitea/index.spec.ts +++ b/lib/platform/gitea/index.spec.ts @@ -1,4 +1,10 @@ -import { BranchStatusConfig, Platform, RepoParams, RepoResult } from '..'; +import { + BranchStatusConfig, + EnsureIssueConfig, + Platform, + RepoParams, + RepoResult, +} from '..'; import { getName, partial } from '../../../test/util'; import { REPOSITORY_ACCESS_FORBIDDEN, @@ -94,6 +100,7 @@ describe(getName(), () => { state: 'open', body: 'some-content', assignees: [], + labels: [], }, { number: 2, @@ -101,6 +108,7 @@ describe(getName(), () => { state: 'closed', body: 'other-content', assignees: [], + labels: [], }, { number: 3, @@ -108,6 +116,7 @@ describe(getName(), () => { state: 'open', body: 'duplicate-content', assignees: [], + labels: [], }, { number: 4, @@ -115,6 +124,7 @@ describe(getName(), () => { state: 'open', body: 'duplicate-content', assignees: [], + labels: [], }, { number: 5, @@ -122,6 +132,7 @@ describe(getName(), () => { state: 'open', body: 'duplicate-content', assignees: [], + labels: [], }, ]; @@ -972,9 +983,43 @@ describe(getName(), () => { }); }); + it('should create issue with the correct labels', async () => { + const mockIssue: EnsureIssueConfig = { + title: 'new-title', + body: 'new-body', + shouldReOpen: false, + once: false, + labels: ['Renovate', 'Maintenance'], + }; + const mockLabels: ght.Label[] = [ + partial<ght.Label>({ id: 1, name: 'Renovate' }), + partial<ght.Label>({ id: 3, name: 'Maintenance' }), + ]; + + helper.getRepoLabels.mockResolvedValueOnce(partial(mockLabels)); + helper.getOrgLabels.mockResolvedValueOnce([]); + + helper.searchIssues.mockResolvedValueOnce(mockIssues); + helper.createIssue.mockResolvedValueOnce( + partial<ght.Issue>({ number: 42 }) + ); + + await initFakeRepo(); + const res = await gitea.ensureIssue(mockIssue); + + expect(res).toEqual('created'); + expect(helper.createIssue).toHaveBeenCalledTimes(1); + expect(helper.createIssue).toHaveBeenCalledWith(mockRepo.full_name, { + body: mockIssue.body, + title: mockIssue.title, + labels: [1, 3], + }); + }); + it('should not reopen closed issue by default', async () => { const closedIssue = mockIssues.find((i) => i.title === 'closed-issue'); helper.searchIssues.mockResolvedValueOnce(mockIssues); + helper.updateIssue.mockResolvedValueOnce(closedIssue); await initFakeRepo(); const res = await gitea.ensureIssue({ @@ -997,9 +1042,118 @@ describe(getName(), () => { ); }); + it('should not update labels when not necessary', async () => { + const mockLabels: ght.Label[] = [ + partial<ght.Label>({ id: 1, name: 'Renovate' }), + partial<ght.Label>({ id: 3, name: 'Maintenance' }), + ]; + const mockIssue: ght.Issue = { + number: 10, + title: 'label-issue', + body: 'label-body', + assignees: [], + labels: mockLabels, + state: 'open', + }; + + helper.getRepoLabels.mockResolvedValueOnce(partial(mockLabels)); + helper.getOrgLabels.mockResolvedValueOnce([]); + helper.searchIssues.mockResolvedValueOnce([mockIssue]); + helper.updateIssue.mockResolvedValueOnce(mockIssue); + + await initFakeRepo(); + const res = await gitea.ensureIssue({ + title: mockIssue.title, + body: 'new-body', + labels: ['Renovate', 'Maintenance'], + }); + + expect(res).toEqual('updated'); + expect(helper.updateIssue).toHaveBeenCalledTimes(1); + expect(helper.updateIssueLabels).toHaveBeenCalledTimes(0); + }); + + it('should update labels when missing', async () => { + const mockLabels: ght.Label[] = [ + partial<ght.Label>({ id: 1, name: 'Renovate' }), + partial<ght.Label>({ id: 3, name: 'Maintenance' }), + ]; + const mockIssue: ght.Issue = { + number: 10, + title: 'label-issue', + body: 'label-body', + assignees: [], + labels: [mockLabels[0]], + state: 'open', + }; + + helper.getRepoLabels.mockResolvedValueOnce(partial(mockLabels)); + helper.getOrgLabels.mockResolvedValueOnce([]); + helper.searchIssues.mockResolvedValueOnce([mockIssue]); + helper.updateIssue.mockResolvedValueOnce(mockIssue); + + await initFakeRepo(); + const res = await gitea.ensureIssue({ + title: mockIssue.title, + body: 'new-body', + labels: ['Renovate', 'Maintenance'], + }); + + expect(res).toEqual('updated'); + expect(helper.updateIssue).toHaveBeenCalledTimes(1); + expect(helper.updateIssueLabels).toHaveBeenCalledTimes(1); + expect(helper.updateIssueLabels).toHaveBeenCalledWith( + mockRepo.full_name, + mockIssue.number, + { + labels: [1, 3], + } + ); + }); + + it('should reset labels when others have been set', async () => { + const mockLabels: ght.Label[] = [ + partial<ght.Label>({ id: 1, name: 'Renovate' }), + partial<ght.Label>({ id: 2, name: 'Other label' }), + partial<ght.Label>({ id: 3, name: 'Maintenance' }), + ]; + const mockIssue: ght.Issue = { + number: 10, + title: 'label-issue', + body: 'label-body', + assignees: [], + labels: mockLabels, + state: 'open', + }; + + helper.getRepoLabels.mockResolvedValueOnce(partial(mockLabels)); + helper.getOrgLabels.mockResolvedValueOnce([]); + helper.searchIssues.mockResolvedValueOnce([mockIssue]); + helper.updateIssue.mockResolvedValueOnce(mockIssue); + + await initFakeRepo(); + const res = await gitea.ensureIssue({ + title: mockIssue.title, + body: 'new-body', + labels: ['Renovate', 'Maintenance'], + }); + + expect(res).toEqual('updated'); + expect(helper.updateIssue).toHaveBeenCalledTimes(1); + expect(helper.updateIssueLabels).toHaveBeenCalledTimes(1); + expect(helper.updateIssueLabels).toHaveBeenCalledWith( + mockRepo.full_name, + mockIssue.number, + { + labels: [1, 3], + } + ); + }); + it('should reopen closed issue if desired', async () => { const closedIssue = mockIssues.find((i) => i.title === 'closed-issue'); helper.searchIssues.mockResolvedValueOnce(mockIssues); + helper.updateIssue.mockResolvedValueOnce(closedIssue); await initFakeRepo(); const res = await gitea.ensureIssue({ diff --git a/lib/platform/gitea/index.ts b/lib/platform/gitea/index.ts index 0458ff5758f7fe17a94c68a52e0eca3b2ddac9ee..61aca917500fd9da4631bbbd32b569201e24191d 100644 --- a/lib/platform/gitea/index.ts +++ b/lib/platform/gitea/index.ts @@ -621,6 +621,7 @@ const platform: Platform = { title, reuseTitle, body: content, + labels: labelNames, shouldReOpen, once, }: EnsureIssueConfig): Promise<'updated' | 'created' | null> { @@ -633,6 +634,11 @@ const platform: Platform = { if (!issues.length) { issues = issueList.filter((i) => i.title === reuseTitle); } + + const labels = Array.isArray(labelNames) + ? await Promise.all(labelNames.map(lookupLabelByName)) + : undefined; + // Update any matching issues which currently exist if (issues.length) { let activeIssue = issues.find((i) => i.state === 'open'); @@ -673,13 +679,37 @@ const platform: Platform = { // Update issue body and re-open if enabled logger.debug(`Updating Issue #${activeIssue.number}`); - await helper.updateIssue(config.repository, activeIssue.number, { - body, - title, - state: shouldReOpen - ? 'open' - : (activeIssue.state as helper.IssueState), - }); + const existingIssue = await helper.updateIssue( + config.repository, + activeIssue.number, + { + body, + title, + state: shouldReOpen + ? 'open' + : (activeIssue.state as helper.IssueState), + } + ); + + // Test whether the issues need to be updated + const existingLabelIds = (existingIssue.labels ?? []).map( + (label) => label.id + ); + if ( + labels !== undefined && + labels.length !== 0 && + (labels.length !== existingLabelIds.length || + labels.filter((labelId) => !existingLabelIds.includes(labelId)) + .length !== 0) + ) { + await helper.updateIssueLabels( + config.repository, + activeIssue.number, + { + labels, + } + ); + } return 'updated'; } @@ -688,6 +718,7 @@ const platform: Platform = { const issue = await helper.createIssue(config.repository, { body, title, + labels, }); logger.debug(`Created new Issue #${issue.number}`); config.issueList = null; diff --git a/lib/platform/github/__snapshots__/index.spec.ts.snap b/lib/platform/github/__snapshots__/index.spec.ts.snap index f1ea6d0f2b77a6c54481468ed627fec09c08a598..b1709b06a7d800a7fbd671bbf239c113359b05dd 100644 --- a/lib/platform/github/__snapshots__/index.spec.ts.snap +++ b/lib/platform/github/__snapshots__/index.spec.ts.snap @@ -1349,6 +1349,63 @@ Array [ ] `; +exports[`platform/github/index ensureIssue() creates issue with labels 1`] = ` +Array [ + Object { + "graphql": Object { + "query": Object { + "repository": Object { + "__args": Object { + "name": "undefined", + "owner": "undefined", + }, + "issues": Object { + "__args": Object { + "first": "100", + }, + "nodes": Object { + "body": null, + "number": null, + "state": null, + "title": null, + }, + "pageInfo": Object { + "endCursor": null, + "hasNextPage": null, + }, + }, + }, + }, + }, + "headers": Object { + "accept": "application/vnd.github.v3+json", + "accept-encoding": "gzip, deflate, br", + "authorization": "token abc123", + "content-length": "425", + "content-type": "application/json", + "host": "api.github.com", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "POST", + "url": "https://api.github.com/graphql", + }, + Object { + "body": "{\\"title\\":\\"new-title\\",\\"body\\":\\"new-content\\",\\"labels\\":[\\"Renovate\\",\\"Maintenance\\"]}", + "headers": Object { + "accept": "application/vnd.github.v3+json", + "accept-encoding": "gzip, deflate, br", + "authorization": "token abc123", + "content-length": "78", + "content-type": "application/json", + "host": "api.github.com", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "POST", + "url": "https://api.github.com/repos/undefined/issues", + }, +] +`; + exports[`platform/github/index ensureIssue() deletes if duplicate 1`] = ` Array [ Object { @@ -1636,6 +1693,74 @@ Array [ ] `; +exports[`platform/github/index ensureIssue() updates issue with labels 1`] = ` +Array [ + Object { + "graphql": Object { + "query": Object { + "repository": Object { + "__args": Object { + "name": "undefined", + "owner": "undefined", + }, + "issues": Object { + "__args": Object { + "first": "100", + }, + "nodes": Object { + "body": null, + "number": null, + "state": null, + "title": null, + }, + "pageInfo": Object { + "endCursor": null, + "hasNextPage": null, + }, + }, + }, + }, + }, + "headers": Object { + "accept": "application/vnd.github.v3+json", + "accept-encoding": "gzip, deflate, br", + "authorization": "token abc123", + "content-length": "425", + "content-type": "application/json", + "host": "api.github.com", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "POST", + "url": "https://api.github.com/graphql", + }, + Object { + "headers": Object { + "accept": "application/vnd.github.v3+json", + "accept-encoding": "gzip, deflate, br", + "authorization": "token abc123", + "host": "api.github.com", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://api.github.com/repos/undefined/issues/2", + }, + Object { + "body": "{\\"body\\":\\"newer-content\\",\\"state\\":\\"open\\",\\"title\\":\\"title-3\\",\\"labels\\":[\\"Renovate\\",\\"Maintenance\\"]}", + "headers": Object { + "accept": "application/vnd.github.v3+json", + "accept-encoding": "gzip, deflate, br", + "authorization": "token abc123", + "content-length": "93", + "content-type": "application/json", + "host": "api.github.com", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "PATCH", + "url": "https://api.github.com/repos/undefined/issues/2", + }, +] +`; + exports[`platform/github/index ensureIssueClosing() closes issue 1`] = ` Array [ Object { diff --git a/lib/platform/github/index.spec.ts b/lib/platform/github/index.spec.ts index ed7d87995ea995f042e1347f68639c33d04849bb..2cbf5fd73912c703223399c595c3cdb24cb8e51b 100644 --- a/lib/platform/github/index.spec.ts +++ b/lib/platform/github/index.spec.ts @@ -1180,6 +1180,36 @@ describe(getName(), () => { expect(res).toBeNull(); expect(httpMock.getTrace()).toMatchSnapshot(); }); + + it('creates issue with labels', async () => { + httpMock + .scope(githubApiHost) + .post('/graphql') + .reply(200, { + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [], + }, + }, + }, + }) + .post('/repos/undefined/issues') + .reply(200); + const res = await github.ensureIssue({ + title: 'new-title', + body: 'new-content', + labels: ['Renovate', 'Maintenance'], + }); + expect(res).toEqual('created'); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + it('closes others if ensuring only once', async () => { httpMock .scope(githubApiHost) @@ -1266,6 +1296,50 @@ describe(getName(), () => { expect(res).toEqual('updated'); expect(httpMock.getTrace()).toMatchSnapshot(); }); + + it('updates issue with labels', async () => { + httpMock + .scope(githubApiHost) + .post('/graphql') + .reply(200, { + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 2, + state: 'open', + title: 'title-2', + }, + { + number: 1, + state: 'open', + title: 'title-1', + }, + ], + }, + }, + }, + }) + .get('/repos/undefined/issues/2') + .reply(200, { body: 'new-content' }) + .patch('/repos/undefined/issues/2') + .reply(200); + const res = await github.ensureIssue({ + title: 'title-3', + reuseTitle: 'title-2', + body: 'newer-content', + labels: ['Renovate', 'Maintenance'], + }); + expect(res).toEqual('updated'); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + it('skips update if unchanged', async () => { httpMock .scope(githubApiHost) diff --git a/lib/platform/github/index.ts b/lib/platform/github/index.ts index 0363732eee9cc61fa7bc41f1e98b7c45bfbad532..61f32c3cf7c21d9331a4c28b378026ea796dae77 100644 --- a/lib/platform/github/index.ts +++ b/lib/platform/github/index.ts @@ -1152,6 +1152,7 @@ export async function ensureIssue({ title, reuseTitle, body: rawBody, + labels, once = false, shouldReOpen = true, }: EnsureIssueConfig): Promise<EnsureIssueResult | null> { @@ -1206,7 +1207,7 @@ export async function ensureIssue({ issue.number }`, { - body: { body, state: 'open', title }, + body: { body, state: 'open', title, labels }, } ); logger.debug('Issue updated'); @@ -1219,6 +1220,7 @@ export async function ensureIssue({ body: { title, body, + labels, }, } ); diff --git a/lib/platform/gitlab/__snapshots__/index.spec.ts.snap b/lib/platform/gitlab/__snapshots__/index.spec.ts.snap index 6be6e08090f43998a1fcd82f11b49fbb2d27b527..d1499c65bebe09c5c93e9f83c842cecdaf3dd44b 100644 --- a/lib/platform/gitlab/__snapshots__/index.spec.ts.snap +++ b/lib/platform/gitlab/__snapshots__/index.spec.ts.snap @@ -700,6 +700,36 @@ Array [ ] `; +exports[`platform/gitlab/index ensureIssue() sets issue labels 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Bearer abc123", + "host": "gitlab.com", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://gitlab.com/api/v4/projects/undefined/issues?per_page=100&author_id=undefined&state=opened", + }, + Object { + "body": "{\\"title\\":\\"new-title\\",\\"description\\":\\"new-content\\",\\"labels\\":[\\"Renovate\\",\\"Maintenance\\"]}", + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Bearer abc123", + "content-length": "85", + "content-type": "application/json", + "host": "gitlab.com", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "POST", + "url": "https://gitlab.com/api/v4/projects/undefined/issues", + }, +] +`; + exports[`platform/gitlab/index ensureIssue() skips update if unchanged 1`] = ` Array [ Object { @@ -768,6 +798,47 @@ Array [ ] `; +exports[`platform/gitlab/index ensureIssue() updates issue with labels 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Bearer abc123", + "host": "gitlab.com", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://gitlab.com/api/v4/projects/undefined/issues?per_page=100&author_id=undefined&state=opened", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Bearer abc123", + "host": "gitlab.com", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://gitlab.com/api/v4/projects/undefined/issues/2", + }, + Object { + "body": "{\\"title\\":\\"title-2\\",\\"description\\":\\"newer-content\\",\\"labels\\":[\\"Renovate\\",\\"Maintenance\\"]}", + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Bearer abc123", + "content-length": "85", + "content-type": "application/json", + "host": "gitlab.com", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "PUT", + "url": "https://gitlab.com/api/v4/projects/undefined/issues/2", + }, +] +`; + exports[`platform/gitlab/index ensureIssueClosing() closes issue 1`] = ` Array [ Object { diff --git a/lib/platform/gitlab/index.spec.ts b/lib/platform/gitlab/index.spec.ts index 76a0a00e125017624e2bd79dd85e5c4b44b6d832..1deb9edbbf1d01995c3fd9bd50e11349e56d3178 100644 --- a/lib/platform/gitlab/index.spec.ts +++ b/lib/platform/gitlab/index.spec.ts @@ -742,6 +742,25 @@ describe(getName(), () => { expect(res).toEqual('created'); expect(httpMock.getTrace()).toMatchSnapshot(); }); + + it('sets issue labels', async () => { + httpMock + .scope(gitlabApiHost) + .get( + '/api/v4/projects/undefined/issues?per_page=100&author_id=undefined&state=opened' + ) + .reply(200, []) + .post('/api/v4/projects/undefined/issues') + .reply(200); + const res = await gitlab.ensureIssue({ + title: 'new-title', + body: 'new-content', + labels: ['Renovate', 'Maintenance'], + }); + expect(res).toEqual('created'); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + it('updates issue', async () => { httpMock .scope(gitlabApiHost) @@ -769,6 +788,36 @@ describe(getName(), () => { expect(res).toEqual('updated'); expect(httpMock.getTrace()).toMatchSnapshot(); }); + + it('updates issue with labels', async () => { + httpMock + .scope(gitlabApiHost) + .get( + '/api/v4/projects/undefined/issues?per_page=100&author_id=undefined&state=opened' + ) + .reply(200, [ + { + iid: 1, + title: 'title-1', + }, + { + iid: 2, + title: 'title-2', + }, + ]) + .get('/api/v4/projects/undefined/issues/2') + .reply(200, { description: 'new-content' }) + .put('/api/v4/projects/undefined/issues/2') + .reply(200); + const res = await gitlab.ensureIssue({ + title: 'title-2', + body: 'newer-content', + labels: ['Renovate', 'Maintenance'], + }); + expect(res).toEqual('updated'); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + it('skips update if unchanged', async () => { httpMock .scope(gitlabApiHost) diff --git a/lib/platform/gitlab/index.ts b/lib/platform/gitlab/index.ts old mode 100755 new mode 100644 index da190b10893409da0906d8ade34020f8422e1d60..4a585f3a28250d5bd368fd6adc080d7e726e1fd7 --- a/lib/platform/gitlab/index.ts +++ b/lib/platform/gitlab/index.ts @@ -793,6 +793,7 @@ export async function ensureIssue({ title, reuseTitle, body, + labels, }: EnsureIssueConfig): Promise<'updated' | 'created' | null> { logger.debug(`ensureIssue()`); const description = massageMarkdown(sanitize(body)); @@ -813,7 +814,7 @@ export async function ensureIssue({ await gitlabApi.putJson( `projects/${config.repository}/issues/${issue.iid}`, { - body: { title, description }, + body: { title, description, labels }, } ); return 'updated'; @@ -823,6 +824,7 @@ export async function ensureIssue({ body: { title, description, + labels, }, }); logger.info('Issue created'); diff --git a/lib/platform/types.ts b/lib/platform/types.ts index dc73dda2298009adc455b927d33f4f2fee128cc8..150e6d1818e40636dfd06a23f5bd76ff9382037d 100644 --- a/lib/platform/types.ts +++ b/lib/platform/types.ts @@ -98,6 +98,7 @@ export interface EnsureIssueConfig { title: string; reuseTitle?: string; body: string; + labels?: string[]; once?: boolean; shouldReOpen?: boolean; } diff --git a/lib/workers/repository/dependency-dashboard.spec.ts b/lib/workers/repository/dependency-dashboard.spec.ts index 8968713df9882bf7ec49687f5c0ddfcdbb28bc32..9a2e323835e43f68b17ba9cf53c0a60543060ce7 100644 --- a/lib/workers/repository/dependency-dashboard.spec.ts +++ b/lib/workers/repository/dependency-dashboard.spec.ts @@ -517,5 +517,20 @@ describe(getName(), () => { await dependencyDashboard.ensureDependencyDashboard(config, branches); expect(platform.ensureIssue.mock.calls[0][0].body).toMatchSnapshot(); }); + + it('forwards configured labels to the ensure issue call', async () => { + const branches: BranchConfig[] = []; + config.dependencyDashboard = true; + config.dependencyDashboardLabels = ['RenovateBot', 'Maintenance']; + await dependencyDashboard.ensureDependencyDashboard(config, branches); + expect(platform.ensureIssue).toHaveBeenCalledTimes(1); + expect(platform.ensureIssue.mock.calls[0][0].labels).toStrictEqual([ + 'RenovateBot', + 'Maintenance', + ]); + + // same with dry run + await dryRun(branches, platform); + }); }); }); diff --git a/lib/workers/repository/dependency-dashboard.ts b/lib/workers/repository/dependency-dashboard.ts index 04c03feaa88018d7e7937ca862e1c9f9c68f7604..6e1072c81deb6a89192e27fced93b87ee26f9113 100644 --- a/lib/workers/repository/dependency-dashboard.ts +++ b/lib/workers/repository/dependency-dashboard.ts @@ -349,6 +349,7 @@ export async function ensureDependencyDashboard( title: config.dependencyDashboardTitle, reuseTitle, body: issueBody, + labels: config.dependencyDashboardLabels, }); } }