From d5ede226d62b316e56fee1bea476e6ead4db6e39 Mon Sep 17 00:00:00 2001 From: Elaine De-Mattos-Silva-Bezerra Date: Sun, 3 Dec 2023 11:43:30 +0100 Subject: [PATCH 01/18] feat: add gitlab create issues custom action Signed-off-by: Elaine Mattos --- packages/backend/src/plugins/scaffolder.ts | 12 + .../README.md | 4 + .../package.json | 1 + .../src/actions/createGitlabIssueAction.ts | 218 ++++++++++++++++++ .../src/commonGitlabConfig.ts | 5 + .../src/index.ts | 1 + .../src/util.ts | 138 +++++++++++ yarn.lock | 46 +++- 8 files changed, 423 insertions(+), 2 deletions(-) create mode 100644 plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index 505a514344..fdddc26fd2 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -23,6 +23,13 @@ import { Router } from 'express'; import type { PluginEnvironment } from '../types'; import { ScmIntegrations } from '@backstage/integration'; import { createConfluenceToMarkdownAction } from '@backstage/plugin-scaffolder-backend-module-confluence-to-markdown'; +import { + createGitlabProjectAccessTokenAction, + createGitlabProjectDeployTokenAction, + createGitlabProjectVariableAction, + createGitlabGroupEnsureExistsAction, + createGitlabIssueAction, +} from '@backstage/plugin-scaffolder-backend-module-gitlab'; export default async function createPlugin( env: PluginEnvironment, @@ -47,6 +54,11 @@ export default async function createPlugin( config: env.config, reader: env.reader, }), + createGitlabProjectAccessTokenAction({ integrations: integrations }), + createGitlabProjectDeployTokenAction({ integrations: integrations }), + createGitlabProjectVariableAction({ integrations: integrations }), + createGitlabGroupEnsureExistsAction({ integrations: integrations }), + createGitlabIssueAction({ integrations: integrations }), ]; return await createRouter({ diff --git a/plugins/scaffolder-backend-module-gitlab/README.md b/plugins/scaffolder-backend-module-gitlab/README.md index 340b57bbf8..128b91092d 100644 --- a/plugins/scaffolder-backend-module-gitlab/README.md +++ b/plugins/scaffolder-backend-module-gitlab/README.md @@ -24,6 +24,7 @@ import { createGitlabProjectDeployTokenAction, createGitlabProjectVariableAction, createGitlabGroupEnsureExistsAction, + createGitlabIssueAction, } from '@backstage/plugin-scaffolder-backend-module-gitlab'; // Create BuiltIn Actions @@ -41,6 +42,7 @@ const actions = [ createGitlabProjectDeployTokenAction({ integrations: integrations }), createGitlabProjectVariableAction({ integrations: integrations }), createGitlabGroupEnsureExistsAction({ integrations: integrations }), + createGitlabIssueAction({ integrations: integrations }), ]; // Create Scaffolder Router @@ -53,6 +55,8 @@ return await createRouter({ database: env.database, reader: env.reader, }); + +// TODO: incorporate Issues creation in example ``` After that you can use the action in your template: diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json index 94641be81c..09efaf5af0 100644 --- a/plugins/scaffolder-backend-module-gitlab/package.json +++ b/plugins/scaffolder-backend-module-gitlab/package.json @@ -36,6 +36,7 @@ "@backstage/integration": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", "@gitbeaker/node": "^35.8.0", + "@gitbeaker/rest": "^39.25.0", "yaml": "^2.0.0", "zod": "^3.21.4" }, diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts new file mode 100644 index 0000000000..50f03e2575 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts @@ -0,0 +1,218 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputError } from '@backstage/errors'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import { + TemplateExample, + createTemplateAction, +} from '@backstage/plugin-scaffolder-node'; +import * as yaml from 'yaml'; +import { + commonGitlabConfig, + commonGitlabConfigExample, +} from '../commonGitlabConfig'; +import { z } from 'zod'; +import { checkEpicScope, convertDate, getClient, parseRepoUrl } from '../util'; +import { Gitlab, CreateIssueOptions, IssueSchema } from '@gitbeaker/rest'; + +/** + * Creates a `gitlab:issues:create` Scaffolder action. + * + * @param {} - Templating configuration. + * @public + */ + +export const createGitlabIssueAction = (options: { + integrations: ScmIntegrationRegistry; +}) => { + const { integrations } = options; + return createTemplateAction({ + id: 'gitlab:issues:create', + description: 'Creates a Gitlab issue.', + examples: getExamples(), + schema: { + input: commonGitlabConfig.merge( + z.object({ + projectId: z.number({ description: 'Project Id' }), + title: z.string({ description: 'Title of the issue' }), + assignees: z + .array(z.number(), { + description: 'IDs of the users to assign the issue to.', + }) + .optional(), + confidential: z + .boolean({ description: 'Issue Confidentiality' }) + .optional(), + description: z + .string({ description: 'Issue description' }) + .optional(), + createdAt: z.string({ description: 'Creation date/time' }).optional(), + dueDate: z.string({ description: 'Due date/time' }).optional(), + discussionToResolve: z + .string({ + description: 'Id of a discussion to resolve', + }) + .optional(), + epicId: z.number({ description: 'Id of the linked Epic' }).optional(), + labels: z.string({ description: 'Labels to apply' }).optional(), + }), + ), + output: z.object({ + issueUrl: z.string({ description: 'Issue Url' }), + issueId: z.number({ description: 'Issue Id' }), + }), + }, + async handler(ctx) { + try { + const { + repoUrl, + projectId, + title, + description = '', + confidential = false, + assignees = [], + createdAt = '', + dueDate, + discussionToResolve = '', + epicId, + labels = '', + token, + } = ctx.input; + + const { host } = parseRepoUrl(repoUrl, integrations); + + const api = getClient({ host, integrations, token }); + + let isEpicScoped = false; + + if (epicId) { + isEpicScoped = await checkEpicScope( + api as any as InstanceType, + projectId, + epicId, + ); + + if (isEpicScoped) { + ctx.logger.info('Epic is within Project Scope'); + } else { + ctx.logger.warn( + 'Chosen epic is not within the Project Scope. The issue will be created without an associated epic.', + ); + } + } + const mappedCreatedAt = convertDate( + String(createdAt), + new Date().toISOString(), + ); + const mappedDueDate = dueDate + ? convertDate(String(dueDate), new Date().toISOString()) + : undefined; + + const issueOptions: CreateIssueOptions = { + description, + assigneeIds: assignees, + confidential, + epicId: isEpicScoped ? epicId : undefined, + labels, + createdAt: mappedCreatedAt, + dueDate: mappedDueDate, + discussionToResolve, + }; + + const response = (await api.Issues.create( + projectId, + title, + issueOptions, + )) as IssueSchema; + + ctx.output('issueId', response.id); + ctx.output('issueUrl', response.web_url); + } catch (error: any) { + throw new InputError(`Failed to create GitLab issue: ${error.message}`); + } + }, + }); +}; + +function getExamples(): TemplateExample[] { + return [ + { + description: 'Create a GitLab issue with minimal options', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: '12', + title: 'Test Issue', + description: 'This is the description of the issue', + }, + }, + ], + }), + }, + { + description: 'Create a GitLab issue with assignees and date options', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: '12', + title: 'Test Issue', + assignees: '18', + description: 'This is the description of the issue', + createdAt: '2022-09-27 18:00:00.000', + dueDate: '2022-09-28 12:00:00.000', + }, + }, + ], + }), + }, + { + description: 'Create a GitLab Issue with several options', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'dxc:gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: '12', + title: 'Test Issue', + assignees: '18', + description: 'This is the description of the issue', + confidential: false, + createdAt: '2022-09-27 18:00:00.000', + dueDate: '2022-09-28 12:00:00.000', + discussionToResolve: '1', + epicId: '1', + labels: 'test-label1,test-label2', + }, + }, + ], + }), + }, + ]; +} diff --git a/plugins/scaffolder-backend-module-gitlab/src/commonGitlabConfig.ts b/plugins/scaffolder-backend-module-gitlab/src/commonGitlabConfig.ts index 02724a3674..3c83eacf19 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/commonGitlabConfig.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/commonGitlabConfig.ts @@ -24,3 +24,8 @@ const commonGitlabConfig = z.object({ }); export default commonGitlabConfig; + +export const commonGitlabConfigExample = { + repoUrl: 'gitlab.com?owner=namespace-or-owner&repo=project-name', + token: '${{ secrets.USER_OAUTH_TOKEN }}', +}; diff --git a/plugins/scaffolder-backend-module-gitlab/src/index.ts b/plugins/scaffolder-backend-module-gitlab/src/index.ts index 4fd4dee35b..eadb1ba78d 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/index.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/index.ts @@ -23,3 +23,4 @@ export * from './actions/createGitlabGroupEnsureExistsAction'; export * from './actions/createGitlabProjectDeployTokenAction'; export * from './actions/createGitlabProjectAccessTokenAction'; export * from './actions/createGitlabProjectVariableAction'; +export * from './actions/createGitlabIssueAction'; diff --git a/plugins/scaffolder-backend-module-gitlab/src/util.ts b/plugins/scaffolder-backend-module-gitlab/src/util.ts index 13c95e48e8..c967e1cee9 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/util.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/util.ts @@ -21,6 +21,7 @@ import { } from '@backstage/integration'; import { z } from 'zod'; import commonGitlabConfig from './commonGitlabConfig'; +import { Gitlab, GroupSchema } from '@gitbeaker/rest'; export const parseRepoHost = (repoUrl: string): string => { let parsed; @@ -56,3 +57,140 @@ export const getToken = ( return { token: token, integrationConfig: integrationConfig }; }; + +export type RepoSpec = { + repo: string; + host: string; + owner?: string; +}; + +export const parseRepoUrl = ( + repoUrl: string, + integrations: ScmIntegrationRegistry, +): RepoSpec => { + let parsed; + try { + parsed = new URL(`https://${repoUrl}`); + } catch (error) { + throw new InputError( + `Invalid repo URL passed to publisher, got ${repoUrl}, ${error}`, + ); + } + const host = parsed.host; + const owner = parsed.searchParams.get('owner') ?? undefined; + const repo: string = parsed.searchParams.get('repo')!; + + const type = integrations.byHost(host)?.type; + + if (!type) { + throw new InputError( + `No matching integration configuration for host ${host}, please check your integrations config`, + ); + } + + return { host, owner, repo }; +}; + +export function getClient(props: { + host: string; + token?: string; + integrations: ScmIntegrationRegistry; +}): InstanceType { + const { host, token, integrations } = props; + const integrationConfig = integrations.gitlab.byHost(host); + + if (!integrationConfig) { + throw new InputError( + `No matching integration configuration for host ${host}, please check your integrations config`, + ); + } + + const { config } = integrationConfig; + + if (!config.token && !token) { + throw new InputError(`No token available for host ${host}`); + } + + const requestToken = token || config.token!; + const tokenType = token ? 'oauthToken' : 'token'; + + const gitlabOptions: any = { + host: config.baseUrl, + }; + + gitlabOptions[tokenType] = requestToken; + + return new Gitlab(gitlabOptions); +} + +export function convertDate( + inputDate: string | undefined, + defaultDate: string, +) { + try { + return inputDate + ? new Date(inputDate).toISOString() + : new Date(defaultDate).toISOString(); + } catch (error) { + throw new InputError(`Error converting input date - ${error}`); + } +} + +export async function getTopLevelParentGroup( + client: InstanceType, + groupId: number, +): Promise { + try { + const topParentGroup = await client.Groups.show(groupId); + if (topParentGroup.parent_id) { + return getTopLevelParentGroup(client, topParentGroup.parent_id as number); + } + + return topParentGroup as GroupSchema; + } catch (error: any) { + throw new InputError( + `Error finding top-level parent group ID: ${error.message}`, + ); + } +} + +export async function checkEpicScope( + client: InstanceType, + projectId: number, + epicId: number, +) { + try { + // If project exists, get the top level group id + const project = await client.Projects.show(projectId); + if (!project) { + throw new InputError( + `Project with id ${projectId} not found. Check your GitLab instance.`, + ); + } + const topParentGroup = await getTopLevelParentGroup( + client, + project.namespace.id, + ); + if (!topParentGroup) { + throw new InputError(`Couldn't find a suitable top-level parent group.`); + } + + // Get the epic + const epic = (await client.Epics.all(topParentGroup.id)).find( + (x: any) => x.id === epicId, + ); + + if (!epic) { + throw new InputError( + `Epic with id ${epicId} not found in the top-level parent group ${topParentGroup.name}.`, + ); + } + + const epicGroup = await client.Groups.show(epic.group_id as number); + const projectNamespace: string = project.path_with_namespace as string; + + return projectNamespace.startsWith(epicGroup.full_path as string); + } catch (error: any) { + throw new InputError(`Could not find epic scope: ${error.message}`); + } +} diff --git a/yarn.lock b/yarn.lock index 2fd28cd48c..a17bae88f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8655,7 +8655,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-scaffolder-backend-module-gitlab@workspace:plugins/scaffolder-backend-module-gitlab": +"@backstage/plugin-scaffolder-backend-module-gitlab@workspace:^, @backstage/plugin-scaffolder-backend-module-gitlab@workspace:plugins/scaffolder-backend-module-gitlab": version: 0.0.0-use.local resolution: "@backstage/plugin-scaffolder-backend-module-gitlab@workspace:plugins/scaffolder-backend-module-gitlab" dependencies: @@ -8667,6 +8667,7 @@ __metadata: "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" "@gitbeaker/node": ^35.8.0 + "@gitbeaker/rest": ^39.25.0 yaml: ^2.0.0 zod: ^3.21.4 languageName: unknown @@ -11569,6 +11570,17 @@ __metadata: languageName: node linkType: hard +"@gitbeaker/core@npm:^39.25.0": + version: 39.25.0 + resolution: "@gitbeaker/core@npm:39.25.0" + dependencies: + "@gitbeaker/requester-utils": ^39.25.0 + qs: ^6.11.2 + xcase: ^2.0.1 + checksum: 56ebe3c6ba1078c47a6cae0abcd5ae4fd928dc73c9eb7dc462894d497d6fa5373c0d09a7a7a0583097cffa641eeb0b28b1764d33a085a7b95f0115399240bc7e + languageName: node + linkType: hard + "@gitbeaker/node@npm:^35.1.0, @gitbeaker/node@npm:^35.8.0": version: 35.8.1 resolution: "@gitbeaker/node@npm:35.8.1" @@ -11593,6 +11605,28 @@ __metadata: languageName: node linkType: hard +"@gitbeaker/requester-utils@npm:^39.25.0": + version: 39.25.0 + resolution: "@gitbeaker/requester-utils@npm:39.25.0" + dependencies: + async-sema: ^3.1.1 + micromatch: ^4.0.5 + qs: ^6.11.2 + xcase: ^2.0.1 + checksum: 1ca90f5ac705d5ef9df682b3051106326bc660427b9b2a88762992690981ce0b6bed4b22bc08af0ba97043766f9898605ba58ed6c1a08b6711af34032f013ed2 + languageName: node + linkType: hard + +"@gitbeaker/rest@npm:^39.25.0": + version: 39.25.0 + resolution: "@gitbeaker/rest@npm:39.25.0" + dependencies: + "@gitbeaker/core": ^39.25.0 + "@gitbeaker/requester-utils": ^39.25.0 + checksum: 5732a34fe3f2fe0a048963baf90b237fde7a1be61fa327e2c7aa69266649394b5b48cd99a6c6abaf1cf8b32b84a293800af33eef49c011b252d6e061f7fe1af9 + languageName: node + linkType: hard + "@google-cloud/container@npm:^5.0.0": version: 5.4.0 resolution: "@google-cloud/container@npm:5.4.0" @@ -21062,6 +21096,13 @@ __metadata: languageName: node linkType: hard +"async-sema@npm:^3.1.1": + version: 3.1.1 + resolution: "async-sema@npm:3.1.1" + checksum: 07b8c51f6cab107417ecdd8126b7a9fe5a75151b7f69fdd420dcc8ee08f9e37c473a217247e894b56e999b088b32e902dbe41637e4e9b594d3f8dfcdddfadc5e + languageName: node + linkType: hard + "async@npm:^2.6.2, async@npm:^2.6.4": version: 2.6.4 resolution: "async@npm:2.6.4" @@ -26880,6 +26921,7 @@ __metadata: "@backstage/plugin-rollbar-backend": "workspace:^" "@backstage/plugin-scaffolder-backend": "workspace:^" "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "workspace:^" + "@backstage/plugin-scaffolder-backend-module-gitlab": "workspace:^" "@backstage/plugin-scaffolder-backend-module-rails": "workspace:^" "@backstage/plugin-search-backend": "workspace:^" "@backstage/plugin-search-backend-module-catalog": "workspace:^" @@ -38184,7 +38226,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.10.1, qs@npm:^6.10.2, qs@npm:^6.11.0, qs@npm:^6.9.1, qs@npm:^6.9.4, qs@npm:^6.9.6": +"qs@npm:^6.10.1, qs@npm:^6.10.2, qs@npm:^6.11.0, qs@npm:^6.11.2, qs@npm:^6.9.1, qs@npm:^6.9.4, qs@npm:^6.9.6": version: 6.11.2 resolution: "qs@npm:6.11.2" dependencies: From 96b8bf417c60f1773a3b2c9642abdf92cbd3025b Mon Sep 17 00:00:00 2001 From: Elaine De-Mattos-Silva-Bezerra Date: Sun, 3 Dec 2023 21:07:31 +0100 Subject: [PATCH 02/18] fix: fixed some issues Signed-off-by: Elaine Mattos --- .../README.md | 18 ++++++ .../sample-templates/template.yaml | 56 +++++++++++++++++++ .../src/actions/createGitlabIssueAction.ts | 23 ++++---- 3 files changed, 86 insertions(+), 11 deletions(-) create mode 100644 plugins/scaffolder-backend-module-gitlab/sample-templates/template.yaml diff --git a/plugins/scaffolder-backend-module-gitlab/README.md b/plugins/scaffolder-backend-module-gitlab/README.md index 128b91092d..9ca74079b2 100644 --- a/plugins/scaffolder-backend-module-gitlab/README.md +++ b/plugins/scaffolder-backend-module-gitlab/README.md @@ -164,8 +164,26 @@ spec: repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }} catalogInfoPath: '/catalog-info.yaml' + - id: gitlabIssue + name: Issues + action: gitlab:issues:create + input: + repoUrl: ${{ parameters.repoUrl }} + token: ${{ secrets.USER_OAUTH_TOKEN }} + projectId: 1111111 + title: Test Issue + assignees: + - 2222222 + description: This is the description of the issue + confidential: true + createdAt: 2022-09-27 18:00:00.000 + dueDate: 2024-09-28 12:00:00.000 + epicId: 3333333 + labels: phase1:label1,phase2:label2 output: links: - title: Repository url: ${{ steps['publish'].output.remoteUrl }} + - title: Link to new issue + url: ${{ steps['gitlabIssue'].output.issueUrl }} ``` diff --git a/plugins/scaffolder-backend-module-gitlab/sample-templates/template.yaml b/plugins/scaffolder-backend-module-gitlab/sample-templates/template.yaml new file mode 100644 index 0000000000..9078c33483 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/sample-templates/template.yaml @@ -0,0 +1,56 @@ +apiVersion: scaffolder.backstage.io/v1beta3 +kind: Template +metadata: + name: gitlab-demo + title: Gitlab DEMO + description: Scaffolder Gitlab Demo +spec: + owner: backstage/techdocs-core + type: service + + parameters: + - title: Fill in some steps + required: + - name + properties: + name: + title: Name + type: string + description: Unique name of the component + ui:autofocus: true + ui:options: + rows: 5 + - title: Choose a location + required: + - repoUrl + properties: + repoUrl: + title: Repository Location + type: string + ui:field: RepoUrlPicker + ui:options: + allowedHosts: + - gitlab.com + + steps: + - id: gitlabIssue + name: Issues + action: gitlab:issues:create + input: + repoUrl: ${{ parameters.repoUrl }} # git.tech.rz.db.de?owner=devex-core&repo=test-repo + token: ${{ secrets.USER_OAUTH_TOKEN }} + projectId: 52723821 + title: Test Issue + assignees: + - 11013327 + description: This is the description of the issue + confidential: true + createdAt: 2022-09-27 18:00:00.000 + dueDate: 2024-09-28 12:00:00.000 + epicId: 1456 + labels: test-label1,test-label2 + output: + links: + - title: Link to new issue + url: ${{ steps['gitlabIssue'].output.issueUrl }} + diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts index 50f03e2575..5f463182b3 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts @@ -21,8 +21,7 @@ import { createTemplateAction, } from '@backstage/plugin-scaffolder-node'; import * as yaml from 'yaml'; -import { - commonGitlabConfig, +import commonGitlabConfig, { commonGitlabConfigExample, } from '../commonGitlabConfig'; import { z } from 'zod'; @@ -160,7 +159,7 @@ function getExamples(): TemplateExample[] { action: 'gitlab:issues:create', input: { ...commonGitlabConfigExample, - projectId: '12', + projectId: 12, title: 'Test Issue', description: 'This is the description of the issue', }, @@ -178,9 +177,9 @@ function getExamples(): TemplateExample[] { action: 'gitlab:issues:create', input: { ...commonGitlabConfigExample, - projectId: '12', + projectId: 12, title: 'Test Issue', - assignees: '18', + assignees: -18, description: 'This is the description of the issue', createdAt: '2022-09-27 18:00:00.000', dueDate: '2022-09-28 12:00:00.000', @@ -196,19 +195,21 @@ function getExamples(): TemplateExample[] { { id: 'gitlabIssue', name: 'Issues', - action: 'dxc:gitlab:issues:create', + action: 'gitlab:issues:create', input: { ...commonGitlabConfigExample, - projectId: '12', + projectId: 12, title: 'Test Issue', - assignees: '18', + assignees: ` + - 18 + - 15 `, description: 'This is the description of the issue', confidential: false, createdAt: '2022-09-27 18:00:00.000', dueDate: '2022-09-28 12:00:00.000', - discussionToResolve: '1', - epicId: '1', - labels: 'test-label1,test-label2', + discussionToResolve: 1, + epicId: 1, + labels: 'phase1:label1,phase2:label2', }, }, ], From 8d5867e781c1bb517bde2f6592141a4ac2791436 Mon Sep 17 00:00:00 2001 From: Elaine De-Mattos-Silva-Bezerra Date: Tue, 5 Dec 2023 08:20:46 +0100 Subject: [PATCH 03/18] wip: add features Signed-off-by: Elaine Mattos --- .../createGitlabGroupEnsureExistsAction.ts | 3 +- .../createGitlabIssueAction.examples.ts | 87 +++++++ .../src/actions/createGitlabIssueAction.ts | 227 +++++++++--------- .../src/util.test.ts | 92 +++++++ 4 files changed, 297 insertions(+), 112 deletions(-) create mode 100644 plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.examples.ts create mode 100644 plugins/scaffolder-backend-module-gitlab/src/util.test.ts diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.ts index 74d3805254..cdc25b85d8 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.ts @@ -60,7 +60,8 @@ export const createGitlabGroupEnsureExistsAction = (options: { const { path } = ctx.input; const { token, integrationConfig } = getToken(ctx.input, integrations); - + console.log(`THIS IS THE TOKEN ${token}`); + console.log(`THIS IS THE PATH ${path}`); const api = new Gitlab({ host: integrationConfig.config.baseUrl, token: token, diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.examples.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.examples.ts new file mode 100644 index 0000000000..1be944a7c8 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.examples.ts @@ -0,0 +1,87 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; +import { commonGitlabConfigExample } from '../commonGitlabConfig'; + +export const examples: TemplateExample[] = [ + { + description: 'Create a GitLab issue with minimal options', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Test Issue', + description: 'This is the description of the issue', + }, + }, + ], + }), + }, + { + description: 'Create a GitLab issue with assignees and date options', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Test Issue', + assignees: -18, + description: 'This is the description of the issue', + createdAt: '2022-09-27 18:00:00.000', + dueDate: '2022-09-28 12:00:00.000', + }, + }, + ], + }), + }, + { + description: 'Create a GitLab Issue with several options', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Test Issue', + assignees: ` + - 18 + - 15 `, + description: 'This is the description of the issue', + confidential: false, + createdAt: '2022-09-27 18:00:00.000', + dueDate: '2022-09-28 12:00:00.000', + discussionToResolve: 1, + epicId: 1, + labels: 'phase1:label1,phase2:label2', + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts index 5f463182b3..57f7d753e4 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts @@ -16,14 +16,9 @@ import { InputError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; -import { - TemplateExample, - createTemplateAction, -} from '@backstage/plugin-scaffolder-node'; -import * as yaml from 'yaml'; -import commonGitlabConfig, { - commonGitlabConfigExample, -} from '../commonGitlabConfig'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; +import commonGitlabConfig from '../commonGitlabConfig'; +import { examples } from './createGitlabIssueAction.examples'; import { z } from 'zod'; import { checkEpicScope, convertDate, getClient, parseRepoUrl } from '../util'; import { Gitlab, CreateIssueOptions, IssueSchema } from '@gitbeaker/rest'; @@ -35,6 +30,100 @@ import { Gitlab, CreateIssueOptions, IssueSchema } from '@gitbeaker/rest'; * @public */ +const enumIssueType = { + ISSUE: 'issue', + INCIDENT: 'incident', + TEST: 'test_case', +}; + +const issueInputProperties = z.object({ + projectId: z.number().describe('Project Id'), + title: z.string({ description: 'Title of the issue' }), + assignees: z + .array(z.number(), { + description: 'IDs of the users to assign the issue to.', + }) + .optional(), + confidential: z.boolean({ description: 'Issue Confidentiality' }).optional(), + description: z.string().describe('Issue description').max(1048576).optional(), + createdAt: z + .string() + .describe('Creation date/time') + .regex( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/, + 'Invalid date format. Use YYYY-MM-DDTHH:mm:ssZ or YYYY-MM-DDTHH:mm:ss.SSSZ', + ) + .optional(), + dueDate: z + .string() + .describe('Due date/time') + .regex( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/, + 'Invalid date format. Use YYYY-MM-DDTHH:mm:ssZ or YYYY-MM-DDTHH:mm:ss.SSSZ', + ) + .optional(), + discussionToResolve: z + .string({ + description: + 'Id of a discussion to resolve. Use in combination with "merge_request_to_resolve_discussions_of"', + }) + .optional(), + epicId: z + .number({ description: 'Id of the linked Epic' }) + .min(0, 'Valid values should be equal or greater than zero') + .optional(), + labels: z.string({ description: 'Labels to apply' }).optional(), + issueType: z + .string({ + description: 'Type of the issue', + }) + .refine(issueType => { + const isValid = Object.values(enumIssueType).includes(issueType); + if (!isValid) { + throw new z.ZodError([ + { + code: 'invalid_enum_value', + options: Object.values(enumIssueType), + path: ['issueType'], + message: `Invalid value for 'issueType'. Must be one of: ${Object.values( + enumIssueType, + ).join(', ')}`, + received: issueType, + }, + ]); + } + return isValid; + }) + .default(enumIssueType.ISSUE) + .optional(), + mergeRequestToResolveDiscussionsOf: z + .number({ + description: 'IID of a merge request in which to resolve all issues', + }) + .optional(), + milestoneId: z + .number({ description: 'Global ID of a milestone to assign the issue' }) + .optional(), + weight: z + .number({ description: 'The issue weight' }) + .min(0) + .refine(value => { + const isValid = value >= 0; + if (!isValid) { + return { + message: 'Valid values should be equal or greater than zero', + }; + } + return isValid; + }) + .optional(), +}); + +const issueOutputProperties = z.object({ + issueUrl: z.string({ description: 'Issue Url' }), + issueId: z.number({ description: 'Issue Id' }), +}); + export const createGitlabIssueAction = (options: { integrations: ScmIntegrationRegistry; }) => { @@ -42,38 +131,10 @@ export const createGitlabIssueAction = (options: { return createTemplateAction({ id: 'gitlab:issues:create', description: 'Creates a Gitlab issue.', - examples: getExamples(), + examples, schema: { - input: commonGitlabConfig.merge( - z.object({ - projectId: z.number({ description: 'Project Id' }), - title: z.string({ description: 'Title of the issue' }), - assignees: z - .array(z.number(), { - description: 'IDs of the users to assign the issue to.', - }) - .optional(), - confidential: z - .boolean({ description: 'Issue Confidentiality' }) - .optional(), - description: z - .string({ description: 'Issue description' }) - .optional(), - createdAt: z.string({ description: 'Creation date/time' }).optional(), - dueDate: z.string({ description: 'Due date/time' }).optional(), - discussionToResolve: z - .string({ - description: 'Id of a discussion to resolve', - }) - .optional(), - epicId: z.number({ description: 'Id of the linked Epic' }).optional(), - labels: z.string({ description: 'Labels to apply' }).optional(), - }), - ), - output: z.object({ - issueUrl: z.string({ description: 'Issue Url' }), - issueId: z.number({ description: 'Issue Id' }), - }), + input: commonGitlabConfig.merge(issueInputProperties), + output: issueOutputProperties, }, async handler(ctx) { try { @@ -89,8 +150,12 @@ export const createGitlabIssueAction = (options: { discussionToResolve = '', epicId, labels = '', + issueType, + mergeRequestToResolveDiscussionsOf, + milestoneId, + weight, token, - } = ctx.input; + } = commonGitlabConfig.merge(issueInputProperties).parse(ctx.input); const { host } = parseRepoUrl(repoUrl, integrations); @@ -113,6 +178,8 @@ export const createGitlabIssueAction = (options: { ); } } + + // TODO: do I really need the convertDate? const mappedCreatedAt = convertDate( String(createdAt), new Date().toISOString(), @@ -130,6 +197,10 @@ export const createGitlabIssueAction = (options: { createdAt: mappedCreatedAt, dueDate: mappedDueDate, discussionToResolve, + issueType, + mergeRequestToResolveDiscussionsOf, + milestoneId, + weight, }; const response = (await api.Issues.create( @@ -141,79 +212,13 @@ export const createGitlabIssueAction = (options: { ctx.output('issueId', response.id); ctx.output('issueUrl', response.web_url); } catch (error: any) { + if (error instanceof z.ZodError) { + // Handling Zod validation errors + throw new InputError(`Validation error: ${error.message}`); + } + // Handling other errors throw new InputError(`Failed to create GitLab issue: ${error.message}`); } }, }); }; - -function getExamples(): TemplateExample[] { - return [ - { - description: 'Create a GitLab issue with minimal options', - example: yaml.stringify({ - steps: [ - { - id: 'gitlabIssue', - name: 'Issues', - action: 'gitlab:issues:create', - input: { - ...commonGitlabConfigExample, - projectId: 12, - title: 'Test Issue', - description: 'This is the description of the issue', - }, - }, - ], - }), - }, - { - description: 'Create a GitLab issue with assignees and date options', - example: yaml.stringify({ - steps: [ - { - id: 'gitlabIssue', - name: 'Issues', - action: 'gitlab:issues:create', - input: { - ...commonGitlabConfigExample, - projectId: 12, - title: 'Test Issue', - assignees: -18, - description: 'This is the description of the issue', - createdAt: '2022-09-27 18:00:00.000', - dueDate: '2022-09-28 12:00:00.000', - }, - }, - ], - }), - }, - { - description: 'Create a GitLab Issue with several options', - example: yaml.stringify({ - steps: [ - { - id: 'gitlabIssue', - name: 'Issues', - action: 'gitlab:issues:create', - input: { - ...commonGitlabConfigExample, - projectId: 12, - title: 'Test Issue', - assignees: ` - - 18 - - 15 `, - description: 'This is the description of the issue', - confidential: false, - createdAt: '2022-09-27 18:00:00.000', - dueDate: '2022-09-28 12:00:00.000', - discussionToResolve: 1, - epicId: 1, - labels: 'phase1:label1,phase2:label2', - }, - }, - ], - }), - }, - ]; -} diff --git a/plugins/scaffolder-backend-module-gitlab/src/util.test.ts b/plugins/scaffolder-backend-module-gitlab/src/util.test.ts new file mode 100644 index 0000000000..0da094fb32 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/util.test.ts @@ -0,0 +1,92 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// import { InputError } from '@backstage/errors'; +import { + // parseRepoHost, + // getToken, + // convertDate, + getTopLevelParentGroup, +} from './util'; +import { Gitlab, GroupSchema } from '@gitbeaker/rest'; +// import { ScmIntegrations } from '@backstage/integration'; +// import { ConfigReader } from '@backstage/config'; + +// Mock the Gitlab client and its methods +jest.mock('@gitbeaker/rest', () => { + const mockShow = jest.fn(); + return { + Gitlab: jest.fn().mockImplementation(() => ({ + Groups: { + show: mockShow, + }, + })), + }; +}); + +describe('getTopLevelParentGroup', () => { + const config = { + gitlab: [ + { + host: 'gitlab.com', + token: 'withToken', + apiBaseUrl: 'gitlab.com/api/v4', + }, + { + host: 'gitlab.com', + apiBaseUrl: 'gitlab.com/api/v4', + }, + ], + }; + + it('should return top-level parent group when parent_id is present', async () => { + const mockGroupId = 123; + const mockParentGroupId = 456; + + const mockTopParentGroup: GroupSchema = { + id: mockGroupId, + parent_id: mockParentGroupId, + path: '', + description: '', + visibility: '', + share_with_group_lock: false, + require_two_factor_authentication: false, + two_factor_grace_period: 0, + project_creation_level: '', + subgroup_creation_level: '', + lfs_enabled: false, + default_branch_protection: 0, + request_access_enabled: false, + created_at: '', + avatar_url: '', + full_name: '', + full_path: '', + web_url: '', + name: '', + }; + + // Instance with token + const mockGitlabClient = new Gitlab({ + host: config.gitlab[0].host, + token: config.gitlab[0].token!, + }); + + const action = getTopLevelParentGroup(mockGitlabClient, mockGroupId); + + const result = await action; // Await the result + + expect(result).toEqual(mockTopParentGroup); + }); +}); From b24b0a2d30408bf7e280d13d94b95cd3c369e98d Mon Sep 17 00:00:00 2001 From: Elaine De-Mattos-Silva-Bezerra Date: Tue, 5 Dec 2023 22:24:11 +0100 Subject: [PATCH 04/18] feat: added initial tests Signed-off-by: Elaine Mattos --- .../sample-templates/template.yaml | 38 +++++- .../src/util.test.ts | 115 ++++++++++++++---- .../src/util.ts | 7 +- 3 files changed, 129 insertions(+), 31 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitlab/sample-templates/template.yaml b/plugins/scaffolder-backend-module-gitlab/sample-templates/template.yaml index 9078c33483..8ae328a8d2 100644 --- a/plugins/scaffolder-backend-module-gitlab/sample-templates/template.yaml +++ b/plugins/scaffolder-backend-module-gitlab/sample-templates/template.yaml @@ -20,6 +20,31 @@ spec: ui:autofocus: true ui:options: rows: 5 + projectId: + title: Project id + type: number + weight: + title: Issue weight + type: number + title: + title: Issue title + type: string + description: + title: Issue description + type: string + issueType: + title: Type + type: string + ui:help: 'issue, incident, test_case' + createdAt: + title: Creation date + type: string + format: date-time + dueDate: + title: Due date + type: string + format: date-time + - title: Choose a location required: - repoUrl @@ -39,16 +64,17 @@ spec: input: repoUrl: ${{ parameters.repoUrl }} # git.tech.rz.db.de?owner=devex-core&repo=test-repo token: ${{ secrets.USER_OAUTH_TOKEN }} - projectId: 52723821 - title: Test Issue + projectId: ${{ parameters.projectId }} # 52723821 + title: ${{ parameters.title }} + weight: ${{ parameters.weight }} assignees: - 11013327 - description: This is the description of the issue + description: ${{ parameters.description }} confidential: true - createdAt: 2022-09-27 18:00:00.000 - dueDate: 2024-09-28 12:00:00.000 - epicId: 1456 + createdAt: ${{ parameters.createdAt }} # "2023-03-11T03:45:40Z" + dueDate: ${{ parameters.dueDate }} # "2024-03-11T03:45:40Z" labels: test-label1,test-label2 + issueType: ${{ parameters.issueType }} output: links: - title: Link to new issue diff --git a/plugins/scaffolder-backend-module-gitlab/src/util.test.ts b/plugins/scaffolder-backend-module-gitlab/src/util.test.ts index 0da094fb32..c8307a6bd5 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/util.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/util.test.ts @@ -13,24 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -// import { InputError } from '@backstage/errors'; -import { - // parseRepoHost, - // getToken, - // convertDate, - getTopLevelParentGroup, -} from './util'; + +import * as util from './util'; import { Gitlab, GroupSchema } from '@gitbeaker/rest'; -// import { ScmIntegrations } from '@backstage/integration'; -// import { ConfigReader } from '@backstage/config'; // Mock the Gitlab client and its methods jest.mock('@gitbeaker/rest', () => { - const mockShow = jest.fn(); return { Gitlab: jest.fn().mockImplementation(() => ({ Groups: { - show: mockShow, + show: jest.fn(), + search: jest.fn(), }, })), }; @@ -51,13 +44,84 @@ describe('getTopLevelParentGroup', () => { ], }; - it('should return top-level parent group when parent_id is present', async () => { - const mockGroupId = 123; - const mockParentGroupId = 456; + it('should return the top-level parent group', async () => { + // Instance with token + const mockGitlabClient = new Gitlab({ + host: config.gitlab[0].host, + token: config.gitlab[0].token!, + }); + // Mocked nested groups + const mockGroups: GroupSchema[] = [ + { + id: 789, + parent_id: 0, + path: '', + description: '', + visibility: '', + share_with_group_lock: false, + require_two_factor_authentication: false, + two_factor_grace_period: 0, + project_creation_level: '', + subgroup_creation_level: '', + lfs_enabled: false, + default_branch_protection: 0, + request_access_enabled: false, + created_at: '', + avatar_url: '', + full_name: '', + full_path: '', + web_url: '', + name: '', + }, + { + id: 456, + parent_id: 789, + path: '', + description: '', + visibility: '', + share_with_group_lock: false, + require_two_factor_authentication: false, + two_factor_grace_period: 0, + project_creation_level: '', + subgroup_creation_level: '', + lfs_enabled: false, + default_branch_protection: 0, + request_access_enabled: false, + created_at: '', + avatar_url: '', + full_name: '', + full_path: '', + web_url: '', + name: '', + }, + { + id: 123, + parent_id: 456, + path: '', + description: '', + visibility: '', + share_with_group_lock: false, + require_two_factor_authentication: false, + two_factor_grace_period: 0, + project_creation_level: '', + subgroup_creation_level: '', + lfs_enabled: false, + default_branch_protection: 0, + request_access_enabled: false, + created_at: '', + avatar_url: '', + full_name: '', + full_path: '', + web_url: '', + name: '', + }, + ]; + + // Top level group const mockTopParentGroup: GroupSchema = { - id: mockGroupId, - parent_id: mockParentGroupId, + id: 789, + parent_id: 0, path: '', description: '', visibility: '', @@ -77,16 +141,21 @@ describe('getTopLevelParentGroup', () => { name: '', }; - // Instance with token - const mockGitlabClient = new Gitlab({ - host: config.gitlab[0].host, - token: config.gitlab[0].token!, - }); + const showSpy = jest.spyOn(mockGitlabClient.Groups, 'show'); - const action = getTopLevelParentGroup(mockGitlabClient, mockGroupId); + // Mock implementation of Groups.show + showSpy.mockImplementation( + async (groupId: string | number): Promise => { + const id = + typeof groupId === 'number' ? groupId : parseInt(groupId, 10); + const mockGroup = mockGroups.find(group => group.id === id) || null; + return mockGroup as GroupSchema; + }, + ); - const result = await action; // Await the result + const action = util.getTopLevelParentGroup(mockGitlabClient, 123); + const result = await action; expect(result).toEqual(mockTopParentGroup); }); }); diff --git a/plugins/scaffolder-backend-module-gitlab/src/util.ts b/plugins/scaffolder-backend-module-gitlab/src/util.ts index c967e1cee9..d100dfd8ef 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/util.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/util.ts @@ -22,6 +22,7 @@ import { import { z } from 'zod'; import commonGitlabConfig from './commonGitlabConfig'; import { Gitlab, GroupSchema } from '@gitbeaker/rest'; +import * as util from './util'; export const parseRepoHost = (repoUrl: string): string => { let parsed; @@ -143,9 +144,11 @@ export async function getTopLevelParentGroup( try { const topParentGroup = await client.Groups.show(groupId); if (topParentGroup.parent_id) { - return getTopLevelParentGroup(client, topParentGroup.parent_id as number); + return util.getTopLevelParentGroup( + client, + topParentGroup.parent_id as number, + ); } - return topParentGroup as GroupSchema; } catch (error: any) { throw new InputError( From ab534408668f8464b45a001c35a1f73e2e802267 Mon Sep 17 00:00:00 2001 From: Elaine De-Mattos-Silva-Bezerra Date: Sun, 10 Dec 2023 20:10:52 +0100 Subject: [PATCH 05/18] wip: add some more tests Signed-off-by: Elaine Mattos --- .../package.json | 3 +- .../actions/createGitlabIssueAction.test.ts | 212 ++++++++++++++++++ .../src/actions/createGitlabIssueAction.ts | 11 +- .../src/util.test.ts | 33 +-- .../src/util.ts | 2 +- yarn.lock | 11 +- 6 files changed, 252 insertions(+), 20 deletions(-) create mode 100644 plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json index 09efaf5af0..1316ed4539 100644 --- a/plugins/scaffolder-backend-module-gitlab/package.json +++ b/plugins/scaffolder-backend-module-gitlab/package.json @@ -43,7 +43,8 @@ "devDependencies": { "@backstage/backend-common": "workspace:^", "@backstage/cli": "workspace:^", - "@backstage/core-app-api": "workspace:^" + "@backstage/core-app-api": "workspace:^", + "jest-date-mock": "^1.0.8" }, "files": [ "dist" diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts new file mode 100644 index 0000000000..cd85454665 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts @@ -0,0 +1,212 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PassThrough } from 'stream'; +import { getVoidLogger } from '@backstage/backend-common'; +import { createGitlabIssueAction } from './createGitlabIssueAction'; +import { ConfigReader } from '@backstage/core-app-api'; +import { ScmIntegrations } from '@backstage/integration'; +import { advanceTo, clear } from 'jest-date-mock'; + +const mockGitlabClient = { + Issues: { + create: jest.fn(), + }, +}; +jest.mock('@gitbeaker/rest', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('gitlab:issues:create', () => { + beforeEach(() => { + jest.clearAllMocks(); + advanceTo(new Date(1990, 9, 24, 12, 0, 0)); // Set the desired date and time + }); + + afterEach(() => { + clear(); // Reset the date mock after each test + }); + + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'myIntegrationsToken', + apiBaseUrl: 'https://gitlab.com/api/v4', + }, + ], + }, + }); + const integrations = ScmIntegrations.fromConfig(config); + + const action = createGitlabIssueAction({ integrations }); + + it('should return a Gitlab issue when called with minimal input params', async () => { + const mockContext = { + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + title: 'Computer banks to rule the world', + // token: 'myAwesomeToken', + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 123, + 'Computer banks to rule the world', + { + issueType: undefined, + description: '', + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: undefined, + weight: undefined, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it('should return a Gitlab issue when called with oAuth Token', async () => { + const mockContext = { + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 123, + 'Computer banks to rule the world', + { + issueType: undefined, + description: '', + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: undefined, + weight: undefined, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it('should return a Gitlab issue when called with an epic id', async () => { + const mockContext = { + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + title: 'Instruments to sight the stars', + token: 'myAwesomeToken', + epicId: 1234, + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 123, + 'Computer banks to rule the world', + { + issueType: undefined, + description: '', + assigneeIds: [], + confidential: false, + epicId: 1234, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: undefined, + weight: undefined, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); +}); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts index 57f7d753e4..408d307612 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts @@ -158,7 +158,6 @@ export const createGitlabIssueAction = (options: { } = commonGitlabConfig.merge(issueInputProperties).parse(ctx.input); const { host } = parseRepoUrl(repoUrl, integrations); - const api = getClient({ host, integrations, token }); let isEpicScoped = false; @@ -178,8 +177,7 @@ export const createGitlabIssueAction = (options: { ); } } - - // TODO: do I really need the convertDate? + console.log('After epic test'); const mappedCreatedAt = convertDate( String(createdAt), new Date().toISOString(), @@ -202,6 +200,11 @@ export const createGitlabIssueAction = (options: { milestoneId, weight, }; + console.log('After setting issues options'); + console.log({ issueOptions }); + console.log(`Other options: ${projectId}, ${title}`); + + console.log({ token }); const response = (await api.Issues.create( projectId, @@ -209,6 +212,8 @@ export const createGitlabIssueAction = (options: { issueOptions, )) as IssueSchema; + console.log('After calling the issues endpoint'); + ctx.output('issueId', response.id); ctx.output('issueUrl', response.web_url); } catch (error: any) { diff --git a/plugins/scaffolder-backend-module-gitlab/src/util.test.ts b/plugins/scaffolder-backend-module-gitlab/src/util.test.ts index c8307a6bd5..0829d1321f 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/util.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/util.test.ts @@ -18,19 +18,26 @@ import * as util from './util'; import { Gitlab, GroupSchema } from '@gitbeaker/rest'; // Mock the Gitlab client and its methods -jest.mock('@gitbeaker/rest', () => { - return { - Gitlab: jest.fn().mockImplementation(() => ({ - Groups: { - show: jest.fn(), - search: jest.fn(), - }, - })), - }; -}); +const setupGitlabMock = () => { + jest.mock('@gitbeaker/rest', () => { + return { + Gitlab: jest.fn().mockImplementation(() => ({ + Groups: { + show: jest.fn(), + }, + })), + }; + }); +}; describe('getTopLevelParentGroup', () => { - const config = { + beforeEach(() => { + setupGitlabMock(); + }); + + afterEach(() => jest.resetAllMocks()); + + const mockConfig = { gitlab: [ { host: 'gitlab.com', @@ -47,8 +54,8 @@ describe('getTopLevelParentGroup', () => { it('should return the top-level parent group', async () => { // Instance with token const mockGitlabClient = new Gitlab({ - host: config.gitlab[0].host, - token: config.gitlab[0].token!, + host: mockConfig.gitlab[0].host, + token: mockConfig.gitlab[0].token!, }); // Mocked nested groups diff --git a/plugins/scaffolder-backend-module-gitlab/src/util.ts b/plugins/scaffolder-backend-module-gitlab/src/util.ts index d100dfd8ef..098aa05667 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/util.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/util.ts @@ -120,7 +120,7 @@ export function getClient(props: { }; gitlabOptions[tokenType] = requestToken; - + console.log({ gitlabOptions }); return new Gitlab(gitlabOptions); } diff --git a/yarn.lock b/yarn.lock index a17bae88f1..1df2e9a722 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8655,7 +8655,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-scaffolder-backend-module-gitlab@workspace:^, @backstage/plugin-scaffolder-backend-module-gitlab@workspace:plugins/scaffolder-backend-module-gitlab": +"@backstage/plugin-scaffolder-backend-module-gitlab@workspace:plugins/scaffolder-backend-module-gitlab": version: 0.0.0-use.local resolution: "@backstage/plugin-scaffolder-backend-module-gitlab@workspace:plugins/scaffolder-backend-module-gitlab" dependencies: @@ -8668,6 +8668,7 @@ __metadata: "@backstage/plugin-scaffolder-node": "workspace:^" "@gitbeaker/node": ^35.8.0 "@gitbeaker/rest": ^39.25.0 + jest-date-mock: ^1.0.8 yaml: ^2.0.0 zod: ^3.21.4 languageName: unknown @@ -26921,7 +26922,6 @@ __metadata: "@backstage/plugin-rollbar-backend": "workspace:^" "@backstage/plugin-scaffolder-backend": "workspace:^" "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "workspace:^" - "@backstage/plugin-scaffolder-backend-module-gitlab": "workspace:^" "@backstage/plugin-scaffolder-backend-module-rails": "workspace:^" "@backstage/plugin-search-backend": "workspace:^" "@backstage/plugin-search-backend-module-catalog": "workspace:^" @@ -31108,6 +31108,13 @@ __metadata: languageName: node linkType: hard +"jest-date-mock@npm:^1.0.8": + version: 1.0.8 + resolution: "jest-date-mock@npm:1.0.8" + checksum: 7b012870440b0df742d0b651435b91625dbbf02d916633a0c70c7deb5d5087f9aadc59847602312368826325909e70e95cd412896a0c9ee1d5ac382000bf5ef9 + languageName: node + linkType: hard + "jest-diff@npm:^28.1.3": version: 28.1.3 resolution: "jest-diff@npm:28.1.3" From 31904c79e4abfcea08a14fcbfdf907d53c86b059 Mon Sep 17 00:00:00 2001 From: Elaine De-Mattos-Silva-Bezerra Date: Mon, 18 Dec 2023 19:00:38 +0100 Subject: [PATCH 06/18] feat: add new tests Signed-off-by: Elaine Mattos --- .../actions/createGitlabIssueAction.test.ts | 56 +--- .../src/util.test.ts | 306 ++++++++++++------ 2 files changed, 217 insertions(+), 145 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts index cd85454665..ce57fac9e5 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts @@ -65,9 +65,8 @@ describe('gitlab:issues:create', () => { repoUrl: 'gitlab.com?repo=repo&owner=owner', projectId: 123, title: 'Computer banks to rule the world', - // token: 'myAwesomeToken', }, - workspacePath: 'lol', + workspacePath: 'seen2much', logger: getVoidLogger(), logStream: new PassThrough(), output: jest.fn(), @@ -117,7 +116,7 @@ describe('gitlab:issues:create', () => { title: 'Computer banks to rule the world', token: 'myAwesomeToken', }, - workspacePath: 'lol', + workspacePath: 'seen2much', logger: getVoidLogger(), logStream: new PassThrough(), output: jest.fn(), @@ -158,55 +157,4 @@ describe('gitlab:issues:create', () => { 'https://gitlab.com/hangar18-/issues/42', ); }); - - it('should return a Gitlab issue when called with an epic id', async () => { - const mockContext = { - input: { - repoUrl: 'gitlab.com?repo=repo&owner=owner', - projectId: 123, - title: 'Instruments to sight the stars', - token: 'myAwesomeToken', - epicId: 1234, - }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; - - mockGitlabClient.Issues.create.mockResolvedValue({ - id: 42, - web_url: 'https://gitlab.com/hangar18-/issues/42', - }); - - await action.handler({ - ...mockContext, - }); - - expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( - 123, - 'Computer banks to rule the world', - { - issueType: undefined, - description: '', - assigneeIds: [], - confidential: false, - epicId: 1234, - labels: '', - createdAt: new Date().toISOString(), - dueDate: undefined, - discussionToResolve: '', - mergeRequestToResolveDiscussionsOf: undefined, - milestoneId: undefined, - weight: undefined, - }, - ); - - expect(mockContext.output).toHaveBeenCalledWith('issueId', 42); - expect(mockContext.output).toHaveBeenCalledWith( - 'issueUrl', - 'https://gitlab.com/hangar18-/issues/42', - ); - }); }); diff --git a/plugins/scaffolder-backend-module-gitlab/src/util.test.ts b/plugins/scaffolder-backend-module-gitlab/src/util.test.ts index 0829d1321f..4de4eca786 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/util.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/util.test.ts @@ -16,6 +16,7 @@ import * as util from './util'; import { Gitlab, GroupSchema } from '@gitbeaker/rest'; +import { InputError } from '@backstage/errors'; // Mock the Gitlab client and its methods const setupGitlabMock = () => { @@ -25,11 +26,30 @@ const setupGitlabMock = () => { Groups: { show: jest.fn(), }, + Projects: { + show: jest.fn(), + }, + Epics: { + all: jest.fn(), + }, })), }; }); }; +const mockConfig = { + gitlab: [ + { + host: 'gitlab.com', + token: 'withToken', + apiBaseUrl: 'gitlab.com/api/v4', + }, + { + host: 'gitlab.com', + apiBaseUrl: 'gitlab.com/api/v4', + }, + ], +}; describe('getTopLevelParentGroup', () => { beforeEach(() => { setupGitlabMock(); @@ -37,96 +57,9 @@ describe('getTopLevelParentGroup', () => { afterEach(() => jest.resetAllMocks()); - const mockConfig = { - gitlab: [ - { - host: 'gitlab.com', - token: 'withToken', - apiBaseUrl: 'gitlab.com/api/v4', - }, - { - host: 'gitlab.com', - apiBaseUrl: 'gitlab.com/api/v4', - }, - ], - }; - - it('should return the top-level parent group', async () => { - // Instance with token - const mockGitlabClient = new Gitlab({ - host: mockConfig.gitlab[0].host, - token: mockConfig.gitlab[0].token!, - }); - - // Mocked nested groups - const mockGroups: GroupSchema[] = [ - { - id: 789, - parent_id: 0, - path: '', - description: '', - visibility: '', - share_with_group_lock: false, - require_two_factor_authentication: false, - two_factor_grace_period: 0, - project_creation_level: '', - subgroup_creation_level: '', - lfs_enabled: false, - default_branch_protection: 0, - request_access_enabled: false, - created_at: '', - avatar_url: '', - full_name: '', - full_path: '', - web_url: '', - name: '', - }, - { - id: 456, - parent_id: 789, - path: '', - description: '', - visibility: '', - share_with_group_lock: false, - require_two_factor_authentication: false, - two_factor_grace_period: 0, - project_creation_level: '', - subgroup_creation_level: '', - lfs_enabled: false, - default_branch_protection: 0, - request_access_enabled: false, - created_at: '', - avatar_url: '', - full_name: '', - full_path: '', - web_url: '', - name: '', - }, - { - id: 123, - parent_id: 456, - path: '', - description: '', - visibility: '', - share_with_group_lock: false, - require_two_factor_authentication: false, - two_factor_grace_period: 0, - project_creation_level: '', - subgroup_creation_level: '', - lfs_enabled: false, - default_branch_protection: 0, - request_access_enabled: false, - created_at: '', - avatar_url: '', - full_name: '', - full_path: '', - web_url: '', - name: '', - }, - ]; - - // Top level group - const mockTopParentGroup: GroupSchema = { + // Mocked nested groups + const mockGroups: GroupSchema[] = [ + { id: 789, parent_id: 0, path: '', @@ -146,7 +79,80 @@ describe('getTopLevelParentGroup', () => { full_path: '', web_url: '', name: '', - }; + }, + { + id: 456, + parent_id: 789, + path: '', + description: '', + visibility: '', + share_with_group_lock: false, + require_two_factor_authentication: false, + two_factor_grace_period: 0, + project_creation_level: '', + subgroup_creation_level: '', + lfs_enabled: false, + default_branch_protection: 0, + request_access_enabled: false, + created_at: '', + avatar_url: '', + full_name: '', + full_path: '', + web_url: '', + name: '', + }, + { + id: 123, + parent_id: 456, + path: '', + description: '', + visibility: '', + share_with_group_lock: false, + require_two_factor_authentication: false, + two_factor_grace_period: 0, + project_creation_level: '', + subgroup_creation_level: '', + lfs_enabled: false, + default_branch_protection: 0, + request_access_enabled: false, + created_at: '', + avatar_url: '', + full_name: '', + full_path: '', + web_url: '', + name: '', + }, + ]; + + // Top level group + const mockTopParentGroup: GroupSchema = { + id: 789, + parent_id: 0, + path: '', + description: '', + visibility: '', + share_with_group_lock: false, + require_two_factor_authentication: false, + two_factor_grace_period: 0, + project_creation_level: '', + subgroup_creation_level: '', + lfs_enabled: false, + default_branch_protection: 0, + request_access_enabled: false, + created_at: '', + avatar_url: '', + full_name: '', + full_path: '', + web_url: '', + name: '', + }; + + it('should return the top-level parent group if the input group has a parent in the hierarchy', async () => { + // Instance with token + const mockGitlabClient = new Gitlab({ + host: mockConfig.gitlab[0].host, + token: mockConfig.gitlab[0].token!, + }); const showSpy = jest.spyOn(mockGitlabClient.Groups, 'show'); @@ -165,4 +171,122 @@ describe('getTopLevelParentGroup', () => { const result = await action; expect(result).toEqual(mockTopParentGroup); }); + + it('should return the input group if it has no parents in the hierarchy', async () => { + // Instance with token + const mockGitlabClient = new Gitlab({ + host: mockConfig.gitlab[0].host, + token: mockConfig.gitlab[0].token!, + }); + + const showSpy = jest.spyOn(mockGitlabClient.Groups, 'show'); + + // Mock implementation of Groups.show + showSpy.mockImplementation( + async (groupId: string | number): Promise => { + const id = + typeof groupId === 'number' ? groupId : parseInt(groupId, 10); + const mockGroup = mockGroups.find(group => group.id === id) || null; + return mockGroup as GroupSchema; + }, + ); + + const action = util.getTopLevelParentGroup(mockGitlabClient, 789); + + const result = await action; + expect(result).toEqual(mockTopParentGroup); + }); +}); + +describe('checkEpicScope', () => { + afterEach(() => jest.resetAllMocks()); + + it('should return true if the project and epic are found', async () => { + const mockGitlabClient = new Gitlab({ + host: mockConfig.gitlab[0].host, + token: mockConfig.gitlab[0].token!, + }); + + const projectId = 123; + const epicId = 456; + + // Mock project and top-level parent group + const mockProject = { namespace: { id: 789 } }; + const mockTopParentGroup = { id: 789, name: 'MockGroup' }; + + mockGitlabClient.Projects.show.mockResolvedValue(mockProject); + mockGitlabClient.Groups.show.mockResolvedValue(mockTopParentGroup); + mockGitlabClient.Epics.all.mockResolvedValue([ + { id: epicId, group_id: 789 }, + ]); + + const result = await checkEpicScope(mockGitlabClient, projectId, epicId); + + expect(result).toBe(true); + expect(mockGitlabClient.Projects.show).toHaveBeenCalledWith(projectId); + expect(mockGitlabClient.Groups.show).toHaveBeenCalledWith( + mockProject.namespace.id, + ); + expect(mockGitlabClient.Epics.all).toHaveBeenCalledWith( + mockTopParentGroup.id, + ); + }); + + it('should throw InputError if the project is not found', async () => { + const mockClient = new Gitlab(); + const projectId = 123; + const epicId = 456; + + // Mocking the absence of the project + mockClient.Projects.show.mockResolvedValue(null); + + await expect(checkEpicScope(mockClient, projectId, epicId)).rejects.toThrow( + new InputError( + `Project with id ${projectId} not found. Check your GitLab instance.`, + ), + ); + + expect(mockClient.Projects.show).toHaveBeenCalledWith(projectId); + expect(mockClient.Groups.show).not.toHaveBeenCalled(); + expect(mockClient.Epics.all).not.toHaveBeenCalled(); + }); + + // Add more test cases as needed for different scenarios +}); + +describe('convertDate', () => { + it('should convert a valid input date with miliseconds to an ISO string', () => { + const inputDate = '1970-01-01T12:00:00.000Z'; + const defaultDate = '1978-10-09T12:00:00Z'; + + const result = util.convertDate(inputDate, defaultDate); + + expect(result).toEqual('1970-01-01T12:00:00.000Z'); + }); + + it('should convert a valid input date to an ISO string', () => { + const inputDate = '1970-01-01T12:00:00Z'; + const defaultDate = '1978-10-09T12:00:00Z'; + + const result = util.convertDate(inputDate, defaultDate); + + expect(result).toEqual('1970-01-01T12:00:00.000Z'); + }); + + it('should use default date if input date is undefined', () => { + const inputDate = undefined; + const defaultDate = '1970-01-01T12:00:00Z'; + + const result = util.convertDate(inputDate, defaultDate); + + expect(result).toEqual('1970-01-01T12:00:00.000Z'); + }); + + it('should throw an InputError if input date is invalid', () => { + const inputDate = 'invalidDate'; + const defaultDate = '2023-02-01T12:00:00Z'; + + // Expecting an InputError to be thrown + expect(() => util.convertDate(inputDate, defaultDate)).toThrow(InputError); + }); }); From 40dcabc994eac8287cbd511a0f47aa0a2a2e1789 Mon Sep 17 00:00:00 2001 From: Elaine De-Mattos-Silva-Bezerra Date: Mon, 18 Dec 2023 22:58:58 +0100 Subject: [PATCH 07/18] feat: add epic scope tests Signed-off-by: Elaine Mattos --- .../actions/createGitlabIssueAction.test.ts | 59 +++++- .../src/actions/createGitlabIssueAction.ts | 12 +- .../src/util.test.ts | 190 +++++++++++++----- .../src/util.ts | 4 - 4 files changed, 204 insertions(+), 61 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts index ce57fac9e5..acb19af65f 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts @@ -37,7 +37,7 @@ jest.mock('@gitbeaker/rest', () => ({ describe('gitlab:issues:create', () => { beforeEach(() => { jest.clearAllMocks(); - advanceTo(new Date(1990, 9, 24, 12, 0, 0)); // Set the desired date and time + advanceTo(new Date(1988, 5, 3, 12, 0, 0)); // Set the desired date and time }); afterEach(() => { @@ -157,4 +157,61 @@ describe('gitlab:issues:create', () => { 'https://gitlab.com/hangar18-/issues/42', ); }); + + it('should return a Gitlab issue when called with several input params', async () => { + const mockContext = { + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueType: 'incident', + title: 'Computer banks to rule the world', + description: + 'this issue should kickstart research on instruments to sight the stars', + dueDate: '1990-08-20T23:59:59Z', + token: 'myAwesomeToken', + assignees: [3, 14, 15], + labels: 'operation:mindcrime', + }, + workspacePath: 'seen2much', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 123, + 'Computer banks to rule the world', + { + issueType: 'incident', + description: + 'this issue should kickstart research on instruments to sight the stars', + assigneeIds: [3, 14, 15], + confidential: false, + epicId: undefined, + labels: 'operation:mindcrime', + createdAt: new Date().toISOString(), + dueDate: '1990-08-20T23:59:59.000Z', + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: undefined, + weight: undefined, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); }); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts index 408d307612..62dc94d14a 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts @@ -177,7 +177,6 @@ export const createGitlabIssueAction = (options: { ); } } - console.log('After epic test'); const mappedCreatedAt = convertDate( String(createdAt), new Date().toISOString(), @@ -200,11 +199,6 @@ export const createGitlabIssueAction = (options: { milestoneId, weight, }; - console.log('After setting issues options'); - console.log({ issueOptions }); - console.log(`Other options: ${projectId}, ${title}`); - - console.log({ token }); const response = (await api.Issues.create( projectId, @@ -212,14 +206,14 @@ export const createGitlabIssueAction = (options: { issueOptions, )) as IssueSchema; - console.log('After calling the issues endpoint'); - ctx.output('issueId', response.id); ctx.output('issueUrl', response.web_url); } catch (error: any) { if (error instanceof z.ZodError) { // Handling Zod validation errors - throw new InputError(`Validation error: ${error.message}`); + throw new InputError(`Validation error: ${error.message}`, { + validationErrors: error.errors, + }); } // Handling other errors throw new InputError(`Failed to create GitLab issue: ${error.message}`); diff --git a/plugins/scaffolder-backend-module-gitlab/src/util.test.ts b/plugins/scaffolder-backend-module-gitlab/src/util.test.ts index 4de4eca786..61c61e87c3 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/util.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/util.test.ts @@ -19,24 +19,26 @@ import { Gitlab, GroupSchema } from '@gitbeaker/rest'; import { InputError } from '@backstage/errors'; // Mock the Gitlab client and its methods -const setupGitlabMock = () => { - jest.mock('@gitbeaker/rest', () => { - return { - Gitlab: jest.fn().mockImplementation(() => ({ - Groups: { - show: jest.fn(), - }, - Projects: { - show: jest.fn(), - }, - Epics: { - all: jest.fn(), - }, - })), - }; - }); +const mockGitlabClient = { + Groups: { + show: jest.fn(), + }, + Projects: { + show: jest.fn(), + }, + Epics: { + all: jest.fn(), + }, }; +jest.mock('@gitbeaker/rest', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + const mockConfig = { gitlab: [ { @@ -51,10 +53,6 @@ const mockConfig = { ], }; describe('getTopLevelParentGroup', () => { - beforeEach(() => { - setupGitlabMock(); - }); - afterEach(() => jest.resetAllMocks()); // Mocked nested groups @@ -149,7 +147,7 @@ describe('getTopLevelParentGroup', () => { it('should return the top-level parent group if the input group has a parent in the hierarchy', async () => { // Instance with token - const mockGitlabClient = new Gitlab({ + const apiClient = new Gitlab({ host: mockConfig.gitlab[0].host, token: mockConfig.gitlab[0].token!, }); @@ -166,7 +164,7 @@ describe('getTopLevelParentGroup', () => { }, ); - const action = util.getTopLevelParentGroup(mockGitlabClient, 123); + const action = util.getTopLevelParentGroup(apiClient, 123); const result = await action; expect(result).toEqual(mockTopParentGroup); @@ -174,7 +172,7 @@ describe('getTopLevelParentGroup', () => { it('should return the input group if it has no parents in the hierarchy', async () => { // Instance with token - const mockGitlabClient = new Gitlab({ + const apiClient = new Gitlab({ host: mockConfig.gitlab[0].host, token: mockConfig.gitlab[0].token!, }); @@ -191,7 +189,7 @@ describe('getTopLevelParentGroup', () => { }, ); - const action = util.getTopLevelParentGroup(mockGitlabClient, 789); + const action = util.getTopLevelParentGroup(apiClient, 789); const result = await action; expect(result).toEqual(mockTopParentGroup); @@ -201,8 +199,8 @@ describe('getTopLevelParentGroup', () => { describe('checkEpicScope', () => { afterEach(() => jest.resetAllMocks()); - it('should return true if the project and epic are found', async () => { - const mockGitlabClient = new Gitlab({ + it('should return true if the project is inside the epic scope', async () => { + const apiClient = new Gitlab({ host: mockConfig.gitlab[0].host, token: mockConfig.gitlab[0].token!, }); @@ -210,17 +208,25 @@ describe('checkEpicScope', () => { const projectId = 123; const epicId = 456; - // Mock project and top-level parent group - const mockProject = { namespace: { id: 789 } }; - const mockTopParentGroup = { id: 789, name: 'MockGroup' }; + // Mock project, top-level parent group, and epic + const mockProject = { + id: 123, + name: 'You learn', + namespace: { id: 789 }, + path_with_namespace: 'at-once/you-learn', + }; + const mockTopParentGroup = { + id: 789, + name: 'LivingTwice', + full_path: 'at-once/you-learn', + }; + const mockEpic = { id: epicId, group_id: 789 }; mockGitlabClient.Projects.show.mockResolvedValue(mockProject); mockGitlabClient.Groups.show.mockResolvedValue(mockTopParentGroup); - mockGitlabClient.Epics.all.mockResolvedValue([ - { id: epicId, group_id: 789 }, - ]); + mockGitlabClient.Epics.all.mockResolvedValue([mockEpic]); - const result = await checkEpicScope(mockGitlabClient, projectId, epicId); + const result = await util.checkEpicScope(apiClient, projectId, epicId); expect(result).toBe(true); expect(mockGitlabClient.Projects.show).toHaveBeenCalledWith(projectId); @@ -232,26 +238,116 @@ describe('checkEpicScope', () => { ); }); - it('should throw InputError if the project is not found', async () => { - const mockClient = new Gitlab(); + it('should return false if the project is not inside the epic scope', async () => { + const apiClient = new Gitlab({ + host: mockConfig.gitlab[0].host, + token: mockConfig.gitlab[0].token!, + }); + + const projectId = 123; + const epicId = 45; + + // Mock project, top-level parent group, and epic + const mockProject = { + id: 123, + name: 'You learn', + namespace: { id: 32 }, + path_with_namespace: 'at-once/you-learn', + }; + const mockTopParentGroup = { + id: 32, + name: 'TheWalls', + full_path: 'you-built/within', + }; + + const mockEpic = { id: epicId, group_id: 32 }; + + mockGitlabClient.Projects.show.mockResolvedValue(mockProject); + mockGitlabClient.Groups.show.mockResolvedValue(mockTopParentGroup); + mockGitlabClient.Epics.all.mockResolvedValue([mockEpic]); + + const result = await util.checkEpicScope(apiClient, projectId, epicId); + + expect(result).toBe(false); + expect(mockGitlabClient.Projects.show).toHaveBeenCalledWith(projectId); + expect(mockGitlabClient.Groups.show).toHaveBeenCalledWith( + mockProject.namespace.id, + ); + expect(mockGitlabClient.Epics.all).toHaveBeenCalledWith( + mockTopParentGroup.id, + ); + }); + + it('should throw an InputError if the project is not found', async () => { + const apiClient = new Gitlab({ + host: mockConfig.gitlab[0].host, + token: mockConfig.gitlab[0].token!, + }); + const projectId = 123; const epicId = 456; - // Mocking the absence of the project - mockClient.Projects.show.mockResolvedValue(null); + mockGitlabClient.Projects.show.mockResolvedValue(null); - await expect(checkEpicScope(mockClient, projectId, epicId)).rejects.toThrow( - new InputError( - `Project with id ${projectId} not found. Check your GitLab instance.`, - ), - ); - - expect(mockClient.Projects.show).toHaveBeenCalledWith(projectId); - expect(mockClient.Groups.show).not.toHaveBeenCalled(); - expect(mockClient.Epics.all).not.toHaveBeenCalled(); + await expect( + util.checkEpicScope(apiClient, projectId, epicId), + ).rejects.toThrow(InputError); + expect(mockGitlabClient.Projects.show).toHaveBeenCalledWith(projectId); }); - // Add more test cases as needed for different scenarios + it('should throw an InputError if the top-level parent group is not found', async () => { + const apiClient = new Gitlab({ + host: mockConfig.gitlab[0].host, + token: mockConfig.gitlab[0].token!, + }); + + const projectId = 123; + const epicId = 456; + + mockGitlabClient.Projects.show.mockResolvedValue({ + id: 123, + name: 'You learn', + namespace: { id: 789 }, + path_with_namespace: 'at-once/you-learn', + }); + mockGitlabClient.Groups.show.mockResolvedValue(null); + + await expect( + util.checkEpicScope(apiClient, projectId, epicId), + ).rejects.toThrow(InputError); + expect(mockGitlabClient.Projects.show).toHaveBeenCalledWith(projectId); + expect(mockGitlabClient.Groups.show).toHaveBeenCalledWith(789); + }); + + it('should throw an InputError if the epic is not found', async () => { + const apiClient = new Gitlab({ + host: mockConfig.gitlab[0].host, + token: mockConfig.gitlab[0].token!, + }); + + const projectId = 123; + const epicId = 456; + + mockGitlabClient.Projects.show.mockResolvedValue({ + id: 123, + name: 'You learn', + namespace: { id: 789 }, + path_with_namespace: 'at-once/you-learn', + }); + mockGitlabClient.Groups.show.mockResolvedValue({ + id: 789, + name: 'LivingTwice', + full_path: 'at-once/you-learn', + }); + mockGitlabClient.Epics.all.mockResolvedValue([]); + + await expect( + util.checkEpicScope(apiClient, projectId, epicId), + ).rejects.toThrow(InputError); + expect(mockGitlabClient.Projects.show).toHaveBeenCalledWith(projectId); + expect(mockGitlabClient.Groups.show).toHaveBeenCalledWith(789); + expect(mockGitlabClient.Epics.all).toHaveBeenCalledWith(789); + }); }); describe('convertDate', () => { diff --git a/plugins/scaffolder-backend-module-gitlab/src/util.ts b/plugins/scaffolder-backend-module-gitlab/src/util.ts index 098aa05667..a4d819d490 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/util.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/util.ts @@ -120,7 +120,6 @@ export function getClient(props: { }; gitlabOptions[tokenType] = requestToken; - console.log({ gitlabOptions }); return new Gitlab(gitlabOptions); } @@ -177,12 +176,10 @@ export async function checkEpicScope( if (!topParentGroup) { throw new InputError(`Couldn't find a suitable top-level parent group.`); } - // Get the epic const epic = (await client.Epics.all(topParentGroup.id)).find( (x: any) => x.id === epicId, ); - if (!epic) { throw new InputError( `Epic with id ${epicId} not found in the top-level parent group ${topParentGroup.name}.`, @@ -191,7 +188,6 @@ export async function checkEpicScope( const epicGroup = await client.Groups.show(epic.group_id as number); const projectNamespace: string = project.path_with_namespace as string; - return projectNamespace.startsWith(epicGroup.full_path as string); } catch (error: any) { throw new InputError(`Could not find epic scope: ${error.message}`); From 99b109849dabae8b506653aad8be0f1def1a6ec3 Mon Sep 17 00:00:00 2001 From: Elaine De-Mattos-Silva-Bezerra Date: Mon, 18 Dec 2023 23:13:07 +0100 Subject: [PATCH 08/18] feat: remove unused code Signed-off-by: Elaine Mattos --- packages/backend/src/plugins/scaffolder.ts | 12 --- .../README.md | 2 - .../sample-templates/template.yaml | 82 ------------------- .../createGitlabGroupEnsureExistsAction.ts | 2 - 4 files changed, 98 deletions(-) delete mode 100644 plugins/scaffolder-backend-module-gitlab/sample-templates/template.yaml diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index fdddc26fd2..505a514344 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -23,13 +23,6 @@ import { Router } from 'express'; import type { PluginEnvironment } from '../types'; import { ScmIntegrations } from '@backstage/integration'; import { createConfluenceToMarkdownAction } from '@backstage/plugin-scaffolder-backend-module-confluence-to-markdown'; -import { - createGitlabProjectAccessTokenAction, - createGitlabProjectDeployTokenAction, - createGitlabProjectVariableAction, - createGitlabGroupEnsureExistsAction, - createGitlabIssueAction, -} from '@backstage/plugin-scaffolder-backend-module-gitlab'; export default async function createPlugin( env: PluginEnvironment, @@ -54,11 +47,6 @@ export default async function createPlugin( config: env.config, reader: env.reader, }), - createGitlabProjectAccessTokenAction({ integrations: integrations }), - createGitlabProjectDeployTokenAction({ integrations: integrations }), - createGitlabProjectVariableAction({ integrations: integrations }), - createGitlabGroupEnsureExistsAction({ integrations: integrations }), - createGitlabIssueAction({ integrations: integrations }), ]; return await createRouter({ diff --git a/plugins/scaffolder-backend-module-gitlab/README.md b/plugins/scaffolder-backend-module-gitlab/README.md index 9ca74079b2..c946c8b476 100644 --- a/plugins/scaffolder-backend-module-gitlab/README.md +++ b/plugins/scaffolder-backend-module-gitlab/README.md @@ -55,8 +55,6 @@ return await createRouter({ database: env.database, reader: env.reader, }); - -// TODO: incorporate Issues creation in example ``` After that you can use the action in your template: diff --git a/plugins/scaffolder-backend-module-gitlab/sample-templates/template.yaml b/plugins/scaffolder-backend-module-gitlab/sample-templates/template.yaml deleted file mode 100644 index 8ae328a8d2..0000000000 --- a/plugins/scaffolder-backend-module-gitlab/sample-templates/template.yaml +++ /dev/null @@ -1,82 +0,0 @@ -apiVersion: scaffolder.backstage.io/v1beta3 -kind: Template -metadata: - name: gitlab-demo - title: Gitlab DEMO - description: Scaffolder Gitlab Demo -spec: - owner: backstage/techdocs-core - type: service - - parameters: - - title: Fill in some steps - required: - - name - properties: - name: - title: Name - type: string - description: Unique name of the component - ui:autofocus: true - ui:options: - rows: 5 - projectId: - title: Project id - type: number - weight: - title: Issue weight - type: number - title: - title: Issue title - type: string - description: - title: Issue description - type: string - issueType: - title: Type - type: string - ui:help: 'issue, incident, test_case' - createdAt: - title: Creation date - type: string - format: date-time - dueDate: - title: Due date - type: string - format: date-time - - - title: Choose a location - required: - - repoUrl - properties: - repoUrl: - title: Repository Location - type: string - ui:field: RepoUrlPicker - ui:options: - allowedHosts: - - gitlab.com - - steps: - - id: gitlabIssue - name: Issues - action: gitlab:issues:create - input: - repoUrl: ${{ parameters.repoUrl }} # git.tech.rz.db.de?owner=devex-core&repo=test-repo - token: ${{ secrets.USER_OAUTH_TOKEN }} - projectId: ${{ parameters.projectId }} # 52723821 - title: ${{ parameters.title }} - weight: ${{ parameters.weight }} - assignees: - - 11013327 - description: ${{ parameters.description }} - confidential: true - createdAt: ${{ parameters.createdAt }} # "2023-03-11T03:45:40Z" - dueDate: ${{ parameters.dueDate }} # "2024-03-11T03:45:40Z" - labels: test-label1,test-label2 - issueType: ${{ parameters.issueType }} - output: - links: - - title: Link to new issue - url: ${{ steps['gitlabIssue'].output.issueUrl }} - diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.ts index cdc25b85d8..510871e028 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.ts @@ -60,8 +60,6 @@ export const createGitlabGroupEnsureExistsAction = (options: { const { path } = ctx.input; const { token, integrationConfig } = getToken(ctx.input, integrations); - console.log(`THIS IS THE TOKEN ${token}`); - console.log(`THIS IS THE PATH ${path}`); const api = new Gitlab({ host: integrationConfig.config.baseUrl, token: token, From 0404e98e5ef510f963eb9d65bd19f1a1b1e7a60f Mon Sep 17 00:00:00 2001 From: Elaine De-Mattos-Silva-Bezerra Date: Mon, 18 Dec 2023 23:15:25 +0100 Subject: [PATCH 09/18] feat: restore space Signed-off-by: Elaine Mattos --- .../src/actions/createGitlabGroupEnsureExistsAction.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.ts index 510871e028..74d3805254 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.ts @@ -60,6 +60,7 @@ export const createGitlabGroupEnsureExistsAction = (options: { const { path } = ctx.input; const { token, integrationConfig } = getToken(ctx.input, integrations); + const api = new Gitlab({ host: integrationConfig.config.baseUrl, token: token, From 604c9ddeefd455f4bf4b170d06c5bfdec984819d Mon Sep 17 00:00:00 2001 From: Elaine Mattos Date: Tue, 19 Dec 2023 08:00:11 +0100 Subject: [PATCH 10/18] fix: add barrel file and fix typos Signed-off-by: Elaine Mattos --- .changeset/flat-terms-provide.md | 5 +++++ .../createGitlabIssueAction.examples.ts | 3 ++- .../src/actions/index.ts | 20 +++++++++++++++++++ .../src/index.ts | 6 +----- 4 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 .changeset/flat-terms-provide.md create mode 100644 plugins/scaffolder-backend-module-gitlab/src/actions/index.ts diff --git a/.changeset/flat-terms-provide.md b/.changeset/flat-terms-provide.md new file mode 100644 index 0000000000..32578c74e3 --- /dev/null +++ b/.changeset/flat-terms-provide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': minor +--- + +Add Scaffolder custom action that creates GitLab issues: gitlab:issues:create diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.examples.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.examples.ts index 1be944a7c8..0044d02df7 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.examples.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.examples.ts @@ -48,7 +48,8 @@ export const examples: TemplateExample[] = [ ...commonGitlabConfigExample, projectId: 12, title: 'Test Issue', - assignees: -18, + assignees: ` + - 18 `, description: 'This is the description of the issue', createdAt: '2022-09-27 18:00:00.000', dueDate: '2022-09-28 12:00:00.000', diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/index.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/index.ts new file mode 100644 index 0000000000..f634c7a03b --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './createGitlabGroupEnsureExistsAction'; +export * from './createGitlabProjectDeployTokenAction'; +export * from './createGitlabProjectAccessTokenAction'; +export * from './createGitlabProjectVariableAction'; +export * from './createGitlabIssueAction'; diff --git a/plugins/scaffolder-backend-module-gitlab/src/index.ts b/plugins/scaffolder-backend-module-gitlab/src/index.ts index eadb1ba78d..844f753344 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/index.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/index.ts @@ -19,8 +19,4 @@ * * @packageDocumentation */ -export * from './actions/createGitlabGroupEnsureExistsAction'; -export * from './actions/createGitlabProjectDeployTokenAction'; -export * from './actions/createGitlabProjectAccessTokenAction'; -export * from './actions/createGitlabProjectVariableAction'; -export * from './actions/createGitlabIssueAction'; +export * from './actions'; From 3aa043af06881c4b7e8a38069ccf2d2c2cba62bb Mon Sep 17 00:00:00 2001 From: Elaine Mattos Date: Tue, 19 Dec 2023 14:32:20 +0100 Subject: [PATCH 11/18] chore: run prettier Signed-off-by: Elaine Mattos --- plugins/scaffolder-backend-module-gitlab/src/actions/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/index.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/index.ts index 0b1db4648a..237ef7c271 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/index.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/index.ts @@ -20,4 +20,3 @@ export * from './createGitlabProjectVariableAction'; export * from './createGitlabIssueAction'; export * from './gitlab'; export * from './gitlabMergeRequest'; - From ae92a9d0fc48b2b2c8a55bae2cd348b723d378b9 Mon Sep 17 00:00:00 2001 From: Elaine Mattos Date: Tue, 19 Dec 2023 14:59:10 +0100 Subject: [PATCH 12/18] chore: add api-report.md Signed-off-by: Elaine Mattos --- .../api-report.md | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-gitlab/api-report.md b/plugins/scaffolder-backend-module-gitlab/api-report.md index debc31412a..908ea43f7f 100644 --- a/plugins/scaffolder-backend-module-gitlab/api-report.md +++ b/plugins/scaffolder-backend-module-gitlab/api-report.md @@ -22,6 +22,36 @@ export const createGitlabGroupEnsureExistsAction: (options: { } >; +// Warning: (ae-missing-release-tag) "createGitlabIssueAction" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const createGitlabIssueAction: (options: { + integrations: ScmIntegrationRegistry; +}) => TemplateAction< + { + title: string; + repoUrl: string; + projectId: number; + token?: string | undefined; + assignees?: number[] | undefined; + confidential?: boolean | undefined; + description?: string | undefined; + createdAt?: string | undefined; + dueDate?: string | undefined; + discussionToResolve?: string | undefined; + epicId?: number | undefined; + labels?: string | undefined; + issueType?: string | undefined; + mergeRequestToResolveDiscussionsOf?: number | undefined; + milestoneId?: number | undefined; + weight?: number | undefined; + }, + { + issueUrl: string; + issueId: number; + } +>; + // @public export const createGitlabProjectAccessTokenAction: (options: { integrations: ScmIntegrationRegistry; @@ -139,7 +169,7 @@ export const createPublishGitlabMergeRequestAction: (options: { sourcePath?: string | undefined; targetPath?: string | undefined; token?: string | undefined; - commitAction?: 'update' | 'delete' | 'create' | undefined; + commitAction?: 'update' | 'create' | 'delete' | undefined; projectid?: string | undefined; removeSourceBranch?: boolean | undefined; assignee?: string | undefined; From e61ed0901c9fcd7e2473e314e35e73196025300a Mon Sep 17 00:00:00 2001 From: Elaine Mattos Date: Tue, 19 Dec 2023 19:03:14 +0100 Subject: [PATCH 13/18] fix: add public decorator Signed-off-by: Elaine Mattos --- .../scaffolder-backend-module-gitlab/api-report.md | 6 ++---- .../src/actions/createGitlabIssueAction.ts | 13 ++++++------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitlab/api-report.md b/plugins/scaffolder-backend-module-gitlab/api-report.md index 908ea43f7f..7f94daf6c9 100644 --- a/plugins/scaffolder-backend-module-gitlab/api-report.md +++ b/plugins/scaffolder-backend-module-gitlab/api-report.md @@ -22,9 +22,7 @@ export const createGitlabGroupEnsureExistsAction: (options: { } >; -// Warning: (ae-missing-release-tag) "createGitlabIssueAction" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const createGitlabIssueAction: (options: { integrations: ScmIntegrationRegistry; }) => TemplateAction< @@ -169,7 +167,7 @@ export const createPublishGitlabMergeRequestAction: (options: { sourcePath?: string | undefined; targetPath?: string | undefined; token?: string | undefined; - commitAction?: 'update' | 'create' | 'delete' | undefined; + commitAction?: 'update' | 'delete' | 'create' | undefined; projectid?: string | undefined; removeSourceBranch?: boolean | undefined; assignee?: string | undefined; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts index 62dc94d14a..e92344fb90 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts @@ -23,13 +23,6 @@ import { z } from 'zod'; import { checkEpicScope, convertDate, getClient, parseRepoUrl } from '../util'; import { Gitlab, CreateIssueOptions, IssueSchema } from '@gitbeaker/rest'; -/** - * Creates a `gitlab:issues:create` Scaffolder action. - * - * @param {} - Templating configuration. - * @public - */ - const enumIssueType = { ISSUE: 'issue', INCIDENT: 'incident', @@ -124,6 +117,12 @@ const issueOutputProperties = z.object({ issueId: z.number({ description: 'Issue Id' }), }); +/** + * Creates a `gitlab:issues:create` Scaffolder action. + * + * @param options - Templating configuration. + * @public + */ export const createGitlabIssueAction = (options: { integrations: ScmIntegrationRegistry; }) => { From 2a3360a77f7cbe2b8e9f76346665bfb5b411892f Mon Sep 17 00:00:00 2001 From: Elaine Mattos Date: Mon, 25 Dec 2023 18:32:27 +0100 Subject: [PATCH 14/18] feat: change simple enum to zod native enums Signed-off-by: Elaine Mattos --- .../createGitlabIssueAction.examples.ts | 7 ++--- .../src/actions/createGitlabIssueAction.ts | 30 ++++--------------- 2 files changed, 8 insertions(+), 29 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.examples.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.examples.ts index 0044d02df7..760caf0f9f 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.examples.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.examples.ts @@ -48,8 +48,7 @@ export const examples: TemplateExample[] = [ ...commonGitlabConfigExample, projectId: 12, title: 'Test Issue', - assignees: ` - - 18 `, + assignees: [18], description: 'This is the description of the issue', createdAt: '2022-09-27 18:00:00.000', dueDate: '2022-09-28 12:00:00.000', @@ -70,9 +69,7 @@ export const examples: TemplateExample[] = [ ...commonGitlabConfigExample, projectId: 12, title: 'Test Issue', - assignees: ` - - 18 - - 15 `, + assignees: [18, 15], description: 'This is the description of the issue', confidential: false, createdAt: '2022-09-27 18:00:00.000', diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts index e92344fb90..f4c77ceb35 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts @@ -23,11 +23,11 @@ import { z } from 'zod'; import { checkEpicScope, convertDate, getClient, parseRepoUrl } from '../util'; import { Gitlab, CreateIssueOptions, IssueSchema } from '@gitbeaker/rest'; -const enumIssueType = { - ISSUE: 'issue', - INCIDENT: 'incident', - TEST: 'test_case', -}; +enum IssueType { + ISSUE = 'issue', + INCIDENT = 'incident', + TEST = 'test_case', +} const issueInputProperties = z.object({ projectId: z.number().describe('Project Id'), @@ -67,27 +67,9 @@ const issueInputProperties = z.object({ .optional(), labels: z.string({ description: 'Labels to apply' }).optional(), issueType: z - .string({ + .nativeEnum(IssueType, { description: 'Type of the issue', }) - .refine(issueType => { - const isValid = Object.values(enumIssueType).includes(issueType); - if (!isValid) { - throw new z.ZodError([ - { - code: 'invalid_enum_value', - options: Object.values(enumIssueType), - path: ['issueType'], - message: `Invalid value for 'issueType'. Must be one of: ${Object.values( - enumIssueType, - ).join(', ')}`, - received: issueType, - }, - ]); - } - return isValid; - }) - .default(enumIssueType.ISSUE) .optional(), mergeRequestToResolveDiscussionsOf: z .number({ From 728e1a0b48877bbb6c9beeb27cf1bbed829cd600 Mon Sep 17 00:00:00 2001 From: Elaine Mattos Date: Mon, 25 Dec 2023 18:52:47 +0100 Subject: [PATCH 15/18] fix: change test input type Signed-off-by: Elaine Mattos --- .../src/actions/createGitlabIssueAction.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts index acb19af65f..fe9e556656 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts @@ -16,7 +16,7 @@ import { PassThrough } from 'stream'; import { getVoidLogger } from '@backstage/backend-common'; -import { createGitlabIssueAction } from './createGitlabIssueAction'; +import { createGitlabIssueAction, IssueType } from './createGitlabIssueAction'; import { ConfigReader } from '@backstage/core-app-api'; import { ScmIntegrations } from '@backstage/integration'; import { advanceTo, clear } from 'jest-date-mock'; @@ -163,7 +163,7 @@ describe('gitlab:issues:create', () => { input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', projectId: 123, - issueType: 'incident', + issueType: IssueType.INCIDENT, title: 'Computer banks to rule the world', description: 'this issue should kickstart research on instruments to sight the stars', From 578a5298656a546c4603841ddea007e94dd8a17b Mon Sep 17 00:00:00 2001 From: Elaine Mattos Date: Mon, 25 Dec 2023 19:06:01 +0100 Subject: [PATCH 16/18] fix: export enum type Signed-off-by: Elaine Mattos --- .../src/actions/createGitlabIssueAction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts index f4c77ceb35..c7d163bdd9 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts @@ -23,7 +23,7 @@ import { z } from 'zod'; import { checkEpicScope, convertDate, getClient, parseRepoUrl } from '../util'; import { Gitlab, CreateIssueOptions, IssueSchema } from '@gitbeaker/rest'; -enum IssueType { +export enum IssueType { ISSUE = 'issue', INCIDENT = 'incident', TEST = 'test_case', From fd3d01c94640c3e180e9cc69c487ce8a0f800c27 Mon Sep 17 00:00:00 2001 From: Elaine Mattos Date: Tue, 26 Dec 2023 10:07:57 +0100 Subject: [PATCH 17/18] fix: add api-report Signed-off-by: Elaine Mattos --- .../scaffolder-backend-module-gitlab/api-report.md | 12 +++++++++++- .../src/actions/createGitlabIssueAction.ts | 5 +++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-gitlab/api-report.md b/plugins/scaffolder-backend-module-gitlab/api-report.md index 7f94daf6c9..9ff9fb3dfc 100644 --- a/plugins/scaffolder-backend-module-gitlab/api-report.md +++ b/plugins/scaffolder-backend-module-gitlab/api-report.md @@ -39,7 +39,7 @@ export const createGitlabIssueAction: (options: { discussionToResolve?: string | undefined; epicId?: number | undefined; labels?: string | undefined; - issueType?: string | undefined; + issueType?: IssueType | undefined; mergeRequestToResolveDiscussionsOf?: number | undefined; milestoneId?: number | undefined; weight?: number | undefined; @@ -174,4 +174,14 @@ export const createPublishGitlabMergeRequestAction: (options: { }, JsonObject >; + +// @public +export enum IssueType { + // (undocumented) + INCIDENT = 'incident', + // (undocumented) + ISSUE = 'issue', + // (undocumented) + TEST = 'test_case', +} ``` diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts index c7d163bdd9..bc9c796253 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts @@ -23,6 +23,11 @@ import { z } from 'zod'; import { checkEpicScope, convertDate, getClient, parseRepoUrl } from '../util'; import { Gitlab, CreateIssueOptions, IssueSchema } from '@gitbeaker/rest'; +/** + * Gitlab issue types + * + * @public + */ export enum IssueType { ISSUE = 'issue', INCIDENT = 'incident', From 5e554f164f67f3cbbd7e9c00331eab668f12e531 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Wed, 10 Jan 2024 15:46:13 +0100 Subject: [PATCH 18/18] Update flat-terms-provide.md Signed-off-by: Ben Lambert --- .changeset/flat-terms-provide.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/flat-terms-provide.md b/.changeset/flat-terms-provide.md index 32578c74e3..f7a22fa7ea 100644 --- a/.changeset/flat-terms-provide.md +++ b/.changeset/flat-terms-provide.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-scaffolder-backend-module-gitlab': minor +'@backstage/plugin-scaffolder-backend-module-gitlab': patch --- -Add Scaffolder custom action that creates GitLab issues: gitlab:issues:create +Add Scaffolder custom action that creates GitLab issues called `gitlab:issues:create`