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] 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); + }); +});