feat: add new edit issue gitlab action
Signed-off-by: John Redwood <john.r.k.redwood@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend-module-gitlab': patch
|
||||
---
|
||||
|
||||
Added `gitlab:issues:edit` action to edit existing GitLab issues
|
||||
@@ -29,12 +29,12 @@ export const createGitlabIssueAction: (options: {
|
||||
}) => TemplateAction<
|
||||
{
|
||||
title: string;
|
||||
repoUrl: string;
|
||||
projectId: number;
|
||||
repoUrl: string;
|
||||
labels?: string | undefined;
|
||||
description?: string | undefined;
|
||||
weight?: number | undefined;
|
||||
token?: string | undefined;
|
||||
weight?: number | undefined;
|
||||
assignees?: number[] | undefined;
|
||||
createdAt?: string | undefined;
|
||||
confidential?: boolean | undefined;
|
||||
@@ -57,12 +57,12 @@ export const createGitlabProjectAccessTokenAction: (options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
}) => TemplateAction<
|
||||
{
|
||||
repoUrl: string;
|
||||
projectId: string | number;
|
||||
repoUrl: string;
|
||||
name?: string | undefined;
|
||||
token?: string | undefined;
|
||||
scopes?: string[] | undefined;
|
||||
expiresAt?: string | undefined;
|
||||
scopes?: string[] | undefined;
|
||||
accessLevel?: number | undefined;
|
||||
},
|
||||
{
|
||||
@@ -76,10 +76,10 @@ export const createGitlabProjectDeployTokenAction: (options: {
|
||||
}) => TemplateAction<
|
||||
{
|
||||
name: string;
|
||||
repoUrl: string;
|
||||
projectId: string | number;
|
||||
username?: string | undefined;
|
||||
repoUrl: string;
|
||||
token?: string | undefined;
|
||||
username?: string | undefined;
|
||||
scopes?: string[] | undefined;
|
||||
},
|
||||
{
|
||||
@@ -95,8 +95,8 @@ export const createGitlabProjectVariableAction: (options: {
|
||||
{
|
||||
key: string;
|
||||
value: string;
|
||||
repoUrl: string;
|
||||
projectId: string | number;
|
||||
repoUrl: string;
|
||||
variableType: string;
|
||||
raw?: boolean | undefined;
|
||||
token?: string | undefined;
|
||||
@@ -149,8 +149,8 @@ export function createPublishGitlabAction(options: {
|
||||
squash_option?:
|
||||
| 'always'
|
||||
| 'never'
|
||||
| 'default_off'
|
||||
| 'default_on'
|
||||
| 'default_off'
|
||||
| undefined;
|
||||
topics?: string[] | undefined;
|
||||
visibility?: 'internal' | 'private' | 'public' | undefined;
|
||||
@@ -207,8 +207,8 @@ export const createTriggerGitlabPipelineAction: (options: {
|
||||
}) => TemplateAction<
|
||||
{
|
||||
branch: string;
|
||||
repoUrl: string;
|
||||
projectId: number;
|
||||
repoUrl: string;
|
||||
tokenDescription: string;
|
||||
token?: string | undefined;
|
||||
},
|
||||
@@ -217,10 +217,54 @@ export const createTriggerGitlabPipelineAction: (options: {
|
||||
}
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const editGitlabIssueAction: (options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
}) => TemplateAction<
|
||||
{
|
||||
projectId: number;
|
||||
repoUrl: string;
|
||||
issueIid: number;
|
||||
title?: string | undefined;
|
||||
labels?: string | undefined;
|
||||
description?: string | undefined;
|
||||
token?: string | undefined;
|
||||
weight?: number | undefined;
|
||||
assignees?: number[] | undefined;
|
||||
addLabels?: string | undefined;
|
||||
confidential?: boolean | undefined;
|
||||
milestoneId?: number | undefined;
|
||||
removeLabels?: string | undefined;
|
||||
stateEvent?: IssueStateEvent | undefined;
|
||||
discussionLocked?: boolean | undefined;
|
||||
epicId?: number | undefined;
|
||||
dueDate?: string | undefined;
|
||||
updatedAt?: string | undefined;
|
||||
issueType?: IssueType | undefined;
|
||||
},
|
||||
{
|
||||
state: string;
|
||||
title: string;
|
||||
projectId: number;
|
||||
updatedAt: string;
|
||||
issueUrl: string;
|
||||
issueId: number;
|
||||
issueIid: number;
|
||||
}
|
||||
>;
|
||||
|
||||
// @public
|
||||
const gitlabModule: () => BackendFeature;
|
||||
export default gitlabModule;
|
||||
|
||||
// @public
|
||||
export enum IssueStateEvent {
|
||||
// (undocumented)
|
||||
CLOSE = 'close',
|
||||
// (undocumented)
|
||||
REOPEN = 'reopen',
|
||||
}
|
||||
|
||||
// @public
|
||||
export enum IssueType {
|
||||
// (undocumented)
|
||||
@@ -228,6 +272,8 @@ export enum IssueType {
|
||||
// (undocumented)
|
||||
ISSUE = 'issue',
|
||||
// (undocumented)
|
||||
TASK = 'task',
|
||||
// (undocumented)
|
||||
TEST = 'test_case',
|
||||
}
|
||||
```
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
import { ConfigReader } from '@backstage/core-app-api';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
|
||||
import { createGitlabIssueAction, IssueType } from './gitlabIssueCreate';
|
||||
import { IssueType } from '../commonGitlabConfig';
|
||||
import { createGitlabIssueAction } from './gitlabIssueCreate';
|
||||
|
||||
const mockGitlabClient = {
|
||||
Issues: {
|
||||
|
||||
@@ -17,22 +17,11 @@
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
|
||||
import commonGitlabConfig from '../commonGitlabConfig';
|
||||
import commonGitlabConfig, { IssueType } from '../commonGitlabConfig';
|
||||
import { examples } from './gitlabIssueCreate.examples';
|
||||
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',
|
||||
TEST = 'test_case',
|
||||
}
|
||||
import { CreateIssueOptions, IssueSchema } from '@gitbeaker/rest';
|
||||
|
||||
const issueInputProperties = z.object({
|
||||
projectId: z.number().describe('Project Id'),
|
||||
@@ -150,11 +139,7 @@ export const createGitlabIssueAction = (options: {
|
||||
let isEpicScoped = false;
|
||||
|
||||
if (epicId) {
|
||||
isEpicScoped = await checkEpicScope(
|
||||
api as any as InstanceType<typeof Gitlab>,
|
||||
projectId,
|
||||
epicId,
|
||||
);
|
||||
isEpicScoped = await checkEpicScope(api, projectId, epicId);
|
||||
|
||||
if (isEpicScoped) {
|
||||
ctx.logger.info('Epic is within Project Scope');
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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: 'Edit a GitLab issue with minimal options',
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
id: 'gitlabIssue',
|
||||
name: 'EditIssues',
|
||||
action: 'gitlab:issues:edit',
|
||||
input: {
|
||||
...commonGitlabConfigExample,
|
||||
projectId: 12,
|
||||
title: 'Modified Test Issue',
|
||||
description: 'This is a modified description of the issue',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
description: 'Edit a GitLab issue with assignees and date options',
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
id: 'gitlabIssue',
|
||||
name: 'EditIssues',
|
||||
action: 'gitlab:issues:edit',
|
||||
input: {
|
||||
...commonGitlabConfigExample,
|
||||
projectId: 12,
|
||||
title: 'Test Issue',
|
||||
assignees: [18],
|
||||
description: 'This is the edited description of the issue',
|
||||
updatedAt: '2024-05-10 18:00:00.000',
|
||||
dueDate: '2024-09-28',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
description: 'Create a GitLab Issue with several options',
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
id: 'gitlabIssue',
|
||||
name: 'EditIssues',
|
||||
action: 'gitlab:issues:edit',
|
||||
input: {
|
||||
...commonGitlabConfigExample,
|
||||
projectId: 12,
|
||||
title: 'Test Edit Issue',
|
||||
assignees: [18, 15],
|
||||
description: 'This is the description of the issue',
|
||||
confidential: false,
|
||||
updatedAt: '2024-05-10 18:00:00.000',
|
||||
dueDate: '2024-09-28',
|
||||
discussionLocked: true,
|
||||
epicId: 1,
|
||||
labels: 'phase1:label1,phase2:label2',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* 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 { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
|
||||
import { ConfigReader } from '@backstage/core-app-api';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { IssueType } from '../commonGitlabConfig';
|
||||
import { editGitlabIssueAction } from './gitlabIssueEdit';
|
||||
|
||||
const mockGitlabClient = {
|
||||
Issues: {
|
||||
edit: jest.fn(),
|
||||
},
|
||||
};
|
||||
jest.mock('@gitbeaker/rest', () => ({
|
||||
Gitlab: class {
|
||||
constructor() {
|
||||
return mockGitlabClient;
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
describe('gitlab:issues:edit', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.useFakeTimers({
|
||||
now: new Date(1988, 5, 3, 12, 0, 0),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
gitlab: [
|
||||
{
|
||||
host: 'gitlab.com',
|
||||
token: 'myIntegrationsToken',
|
||||
apiBaseUrl: 'https://gitlab.com/api/v4',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
|
||||
const action = editGitlabIssueAction({ integrations });
|
||||
|
||||
it('should return a Gitlab issue when called with minimal input params', async () => {
|
||||
const mockContext = createMockActionContext({
|
||||
input: {
|
||||
repoUrl: 'gitlab.com?repo=repo&owner=owner',
|
||||
projectId: 123,
|
||||
issueIid: 42,
|
||||
title: 'Computer banks to rule the world',
|
||||
},
|
||||
workspacePath: 'seen2much',
|
||||
});
|
||||
|
||||
mockGitlabClient.Issues.edit.mockResolvedValue({
|
||||
id: 123,
|
||||
iid: 42,
|
||||
web_url: 'https://gitlab.com/hangar18-/issues/42',
|
||||
});
|
||||
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
});
|
||||
|
||||
expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(123, 42, {
|
||||
title: 'Computer banks to rule the world',
|
||||
issueType: undefined,
|
||||
addLabels: undefined,
|
||||
removeLabels: undefined,
|
||||
description: undefined,
|
||||
assigneeIds: [],
|
||||
confidential: false,
|
||||
discussionLocked: false,
|
||||
epicId: undefined,
|
||||
labels: undefined,
|
||||
updatedAt: new Date().toISOString(),
|
||||
dueDate: undefined,
|
||||
milestoneId: undefined,
|
||||
weight: undefined,
|
||||
stateEvent: undefined,
|
||||
});
|
||||
|
||||
expect(mockContext.output).toHaveBeenCalledWith('issueId', 123);
|
||||
expect(mockContext.output).toHaveBeenCalledWith('issueIid', 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 = createMockActionContext({
|
||||
input: {
|
||||
repoUrl: 'gitlab.com?repo=repo&owner=owner',
|
||||
projectId: 123,
|
||||
issueIid: 42,
|
||||
title: 'Computer banks to rule the world',
|
||||
token: 'myAwesomeToken',
|
||||
},
|
||||
workspacePath: 'seen2much',
|
||||
});
|
||||
|
||||
mockGitlabClient.Issues.edit.mockResolvedValue({
|
||||
id: 123,
|
||||
iid: 42,
|
||||
web_url: 'https://gitlab.com/hangar18-/issues/42',
|
||||
});
|
||||
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
});
|
||||
expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(123, 42, {
|
||||
title: 'Computer banks to rule the world',
|
||||
issueType: undefined,
|
||||
addLabels: undefined,
|
||||
removeLabels: undefined,
|
||||
description: undefined,
|
||||
assigneeIds: [],
|
||||
confidential: false,
|
||||
discussionLocked: false,
|
||||
epicId: undefined,
|
||||
labels: undefined,
|
||||
updatedAt: new Date().toISOString(),
|
||||
dueDate: undefined,
|
||||
milestoneId: undefined,
|
||||
weight: undefined,
|
||||
stateEvent: undefined,
|
||||
});
|
||||
|
||||
expect(mockContext.output).toHaveBeenCalledWith('issueId', 123);
|
||||
expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42);
|
||||
expect(mockContext.output).toHaveBeenCalledWith(
|
||||
'issueUrl',
|
||||
'https://gitlab.com/hangar18-/issues/42',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return a Gitlab issue when modified with several input params', async () => {
|
||||
const mockContext = createMockActionContext({
|
||||
input: {
|
||||
repoUrl: 'gitlab.com?repo=repo&owner=owner',
|
||||
projectId: 123,
|
||||
issueIid: 42,
|
||||
issueType: IssueType.INCIDENT,
|
||||
title: 'Computer banks to rule the world',
|
||||
description:
|
||||
'this issue should kickstart research on instruments to sight the stars',
|
||||
dueDate: '2025-08-20',
|
||||
token: 'myAwesomeToken',
|
||||
assignees: [3, 14, 15],
|
||||
labels: 'operation:mindcrime',
|
||||
},
|
||||
workspacePath: 'seen2much',
|
||||
});
|
||||
|
||||
mockGitlabClient.Issues.edit.mockResolvedValue({
|
||||
id: 123,
|
||||
iid: 42,
|
||||
web_url: 'https://gitlab.com/hangar18-/issues/42',
|
||||
});
|
||||
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
});
|
||||
|
||||
expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(123, 42, {
|
||||
title: 'Computer banks to rule the world',
|
||||
issueType: 'incident',
|
||||
addLabels: undefined,
|
||||
removeLabels: undefined,
|
||||
description:
|
||||
'this issue should kickstart research on instruments to sight the stars',
|
||||
assigneeIds: [3, 14, 15],
|
||||
confidential: false,
|
||||
discussionLocked: false,
|
||||
epicId: undefined,
|
||||
labels: 'operation:mindcrime',
|
||||
updatedAt: new Date().toISOString(),
|
||||
dueDate: '2025-08-20',
|
||||
milestoneId: undefined,
|
||||
weight: undefined,
|
||||
stateEvent: undefined,
|
||||
});
|
||||
|
||||
expect(mockContext.output).toHaveBeenCalledWith('issueId', 123);
|
||||
expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42);
|
||||
expect(mockContext.output).toHaveBeenCalledWith(
|
||||
'issueUrl',
|
||||
'https://gitlab.com/hangar18-/issues/42',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
* 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 { createTemplateAction } from '@backstage/plugin-scaffolder-node';
|
||||
import commonGitlabConfig, {
|
||||
IssueType,
|
||||
IssueStateEvent,
|
||||
} from '../commonGitlabConfig';
|
||||
import { examples } from './gitlabIssueEdit.examples';
|
||||
import { z } from 'zod';
|
||||
import { checkEpicScope, convertDate, getClient, parseRepoUrl } from '../util';
|
||||
import { IssueSchema, EditIssueOptions } from '@gitbeaker/rest';
|
||||
|
||||
const editIssueInputProperties = z.object({
|
||||
projectId: z
|
||||
.number()
|
||||
.describe(
|
||||
'The global ID or URL-encoded path of the project owned by the authenticated user.',
|
||||
),
|
||||
issueIid: z.number().describe("The internal ID of a project's issue"),
|
||||
addLabels: z
|
||||
.string({
|
||||
description:
|
||||
'Comma-separated label names to add to an issue. If a label does not already exist, this creates a new project label and assigns it to the issue.',
|
||||
})
|
||||
.optional(),
|
||||
assignees: z
|
||||
.array(z.number(), {
|
||||
description: 'IDs of the users to assign the issue to.',
|
||||
})
|
||||
.optional(),
|
||||
confidential: z
|
||||
.boolean({ description: 'Updates an issue to be confidential.' })
|
||||
.optional(),
|
||||
description: z
|
||||
.string()
|
||||
.describe('The description of an issue. Limited to 1,048,576 characters.')
|
||||
.max(1048576)
|
||||
.optional(),
|
||||
discussionLocked: z
|
||||
.boolean({
|
||||
description:
|
||||
'Flag indicating if the issue’s discussion is locked. If the discussion is locked only project members can add or edit comments.',
|
||||
})
|
||||
.optional(),
|
||||
dueDate: z
|
||||
.string()
|
||||
.describe(
|
||||
'The due date. Date time string in the format YYYY-MM-DD, for example 2016-03-11.',
|
||||
)
|
||||
.regex(/^\d{4}-\d{2}-\d{2}$/, 'Invalid date format. Use YYYY-MM-DD')
|
||||
.optional(),
|
||||
epicId: z
|
||||
.number({
|
||||
description:
|
||||
'ID of the epic to add the issue to. Valid values are greater than or equal to 0.',
|
||||
})
|
||||
.min(0, 'Valid values should be equal or greater than zero')
|
||||
.optional(),
|
||||
issueType: z
|
||||
.nativeEnum(IssueType, {
|
||||
description:
|
||||
'Updates the type of issue. One of issue, incident, test_case or task.',
|
||||
})
|
||||
.optional(),
|
||||
labels: z
|
||||
.string({
|
||||
description:
|
||||
'Comma-separated label names for an issue. Set to an empty string to unassign all labels. If a label does not already exist, this creates a new project label and assigns it to the issue.',
|
||||
})
|
||||
.optional(),
|
||||
milestoneId: z
|
||||
.number({
|
||||
description:
|
||||
'The global ID of a milestone to assign the issue to. Set to 0 or provide an empty value to unassign a milestone',
|
||||
})
|
||||
.optional(),
|
||||
removeLabels: z
|
||||
.string({
|
||||
description: 'Comma-separated label names to remove from an issue.',
|
||||
})
|
||||
.optional(),
|
||||
stateEvent: z
|
||||
.nativeEnum(IssueStateEvent, {
|
||||
description:
|
||||
'The state event of an issue. To close the issue, use close, and to reopen it, use reopen.',
|
||||
})
|
||||
.optional(),
|
||||
title: z.string().describe('The title of an issue.').optional(),
|
||||
updatedAt: z
|
||||
.string()
|
||||
.describe(
|
||||
'When the issue was updated. Date time string, ISO 8601 formatted',
|
||||
)
|
||||
.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(),
|
||||
weight: z
|
||||
.number({ description: 'The issue weight' })
|
||||
.min(0, 'Valid values should be equal or greater than zero')
|
||||
.max(10, 'Valid values should be equal or less than 10')
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const editIssueOutputProperties = z.object({
|
||||
issueUrl: z.string({ description: 'Issue WebUrl' }),
|
||||
projectId: z.number({
|
||||
description: 'The project id the issue belongs to WebUrl',
|
||||
}),
|
||||
issueId: z.number({ description: 'The issues Id' }),
|
||||
issueIid: z.number({
|
||||
description: "The issues internal ID of a project's issue",
|
||||
}),
|
||||
state: z.string({ description: 'The state event of an issue' }),
|
||||
title: z.string({ description: 'The title of an issue.' }),
|
||||
updatedAt: z.string({ description: 'The last updated time of the issue.' }),
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a `gitlab:issues:edit` Scaffolder action.
|
||||
*
|
||||
* @param options - Templating configuration.
|
||||
* @public
|
||||
*/
|
||||
export const editGitlabIssueAction = (options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
}) => {
|
||||
const { integrations } = options;
|
||||
return createTemplateAction({
|
||||
id: 'gitlab:issues:edit',
|
||||
description: 'Edit a Gitlab issue.',
|
||||
examples,
|
||||
schema: {
|
||||
input: commonGitlabConfig.merge(editIssueInputProperties),
|
||||
output: editIssueOutputProperties,
|
||||
},
|
||||
async handler(ctx) {
|
||||
try {
|
||||
const {
|
||||
repoUrl,
|
||||
projectId,
|
||||
title,
|
||||
addLabels,
|
||||
removeLabels,
|
||||
issueIid,
|
||||
description,
|
||||
confidential = false,
|
||||
assignees = [],
|
||||
updatedAt = '',
|
||||
dueDate,
|
||||
discussionLocked = false,
|
||||
epicId,
|
||||
labels,
|
||||
issueType,
|
||||
milestoneId,
|
||||
stateEvent,
|
||||
weight,
|
||||
token,
|
||||
} = commonGitlabConfig.merge(editIssueInputProperties).parse(ctx.input);
|
||||
|
||||
const { host } = parseRepoUrl(repoUrl, integrations);
|
||||
const api = getClient({ host, integrations, token });
|
||||
|
||||
let isEpicScoped = false;
|
||||
|
||||
if (epicId) {
|
||||
isEpicScoped = await checkEpicScope(api, 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 mappedUpdatedAt = convertDate(
|
||||
String(updatedAt),
|
||||
new Date().toISOString(),
|
||||
);
|
||||
|
||||
const editIssueOptions: EditIssueOptions = {
|
||||
addLabels,
|
||||
assigneeIds: assignees,
|
||||
confidential,
|
||||
description,
|
||||
discussionLocked,
|
||||
dueDate,
|
||||
epicId: isEpicScoped ? epicId : undefined,
|
||||
issueType,
|
||||
labels,
|
||||
milestoneId,
|
||||
removeLabels,
|
||||
stateEvent,
|
||||
title,
|
||||
updatedAt: mappedUpdatedAt,
|
||||
weight,
|
||||
};
|
||||
|
||||
const response = (await api.Issues.edit(
|
||||
projectId,
|
||||
issueIid,
|
||||
editIssueOptions,
|
||||
)) as IssueSchema;
|
||||
|
||||
ctx.output('issueId', response.id);
|
||||
ctx.output('projectId', response.project_id);
|
||||
ctx.output('issueUrl', response.web_url);
|
||||
ctx.output('issueIid', response.iid);
|
||||
ctx.output('title', response.title);
|
||||
ctx.output('state', response.state);
|
||||
ctx.output('updatedAt', response.updated_at);
|
||||
} catch (error: any) {
|
||||
if (error instanceof z.ZodError) {
|
||||
// Handling Zod validation errors
|
||||
throw new InputError(`Validation error: ${error.message}`, {
|
||||
validationErrors: error.errors,
|
||||
});
|
||||
}
|
||||
// Handling other errors
|
||||
throw new InputError(
|
||||
`Failed to edit/modify GitLab issue: ${error.message}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -16,9 +16,11 @@
|
||||
export * from './gitlab';
|
||||
export * from './gitlabGroupEnsureExists';
|
||||
export * from './gitlabIssueCreate';
|
||||
export * from './gitlabIssueEdit';
|
||||
export * from './gitlabMergeRequest';
|
||||
export * from './gitlabPipelineTrigger';
|
||||
export * from './gitlabProjectAccessTokenCreate';
|
||||
export * from './gitlabProjectDeployTokenCreate';
|
||||
export * from './gitlabProjectVariableCreate';
|
||||
export * from './gitlabRepoPush';
|
||||
export { IssueType, IssueStateEvent } from '../commonGitlabConfig';
|
||||
|
||||
@@ -29,3 +29,25 @@ export const commonGitlabConfigExample = {
|
||||
repoUrl: 'gitlab.com?owner=namespace-or-owner&repo=project-name',
|
||||
token: '${{ secrets.USER_OAUTH_TOKEN }}',
|
||||
};
|
||||
|
||||
/**
|
||||
* Gitlab issue types as specified by gitlab api
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export enum IssueType {
|
||||
ISSUE = 'issue',
|
||||
INCIDENT = 'incident',
|
||||
TEST = 'test_case',
|
||||
TASK = 'task',
|
||||
}
|
||||
|
||||
/**
|
||||
* Gitlab issue state events for modifications
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export enum IssueStateEvent {
|
||||
CLOSE = 'close',
|
||||
REOPEN = 'reopen',
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
createPublishGitlabAction,
|
||||
createPublishGitlabMergeRequestAction,
|
||||
createTriggerGitlabPipelineAction,
|
||||
editGitlabIssueAction,
|
||||
} from './actions';
|
||||
|
||||
/**
|
||||
@@ -54,6 +55,7 @@ export const gitlabModule = createBackendModule({
|
||||
createGitlabProjectDeployTokenAction({ integrations }),
|
||||
createGitlabProjectVariableAction({ integrations }),
|
||||
createGitlabRepoPushAction({ integrations }),
|
||||
editGitlabIssueAction({ integrations }),
|
||||
createPublishGitlabAction({ config, integrations }),
|
||||
createPublishGitlabMergeRequestAction({ integrations }),
|
||||
createTriggerGitlabPipelineAction({ integrations }),
|
||||
|
||||
@@ -397,11 +397,11 @@ export const repoPickerValidation: (
|
||||
export const RepoUrlPickerFieldExtension: FieldExtensionComponent_2<
|
||||
string,
|
||||
{
|
||||
allowedHosts?: string[] | undefined;
|
||||
allowedOrganizations?: string[] | undefined;
|
||||
allowedOwners?: string[] | undefined;
|
||||
allowedOrganizations?: string[] | undefined;
|
||||
allowedProjects?: string[] | undefined;
|
||||
allowedRepos?: string[] | undefined;
|
||||
allowedHosts?: string[] | undefined;
|
||||
requestUserCredentials?:
|
||||
| {
|
||||
secretsKey: string;
|
||||
@@ -409,9 +409,9 @@ export const RepoUrlPickerFieldExtension: FieldExtensionComponent_2<
|
||||
| {
|
||||
azure?: string[] | undefined;
|
||||
github?: string[] | undefined;
|
||||
gitlab?: string[] | undefined;
|
||||
bitbucket?: string[] | undefined;
|
||||
gerrit?: string[] | undefined;
|
||||
gitlab?: string[] | undefined;
|
||||
gitea?: string[] | undefined;
|
||||
}
|
||||
| undefined;
|
||||
@@ -424,11 +424,11 @@ export const RepoUrlPickerFieldExtension: FieldExtensionComponent_2<
|
||||
export const RepoUrlPickerFieldSchema: FieldSchema<
|
||||
string,
|
||||
{
|
||||
allowedHosts?: string[] | undefined;
|
||||
allowedOrganizations?: string[] | undefined;
|
||||
allowedOwners?: string[] | undefined;
|
||||
allowedOrganizations?: string[] | undefined;
|
||||
allowedProjects?: string[] | undefined;
|
||||
allowedRepos?: string[] | undefined;
|
||||
allowedHosts?: string[] | undefined;
|
||||
requestUserCredentials?:
|
||||
| {
|
||||
secretsKey: string;
|
||||
@@ -436,9 +436,9 @@ export const RepoUrlPickerFieldSchema: FieldSchema<
|
||||
| {
|
||||
azure?: string[] | undefined;
|
||||
github?: string[] | undefined;
|
||||
gitlab?: string[] | undefined;
|
||||
bitbucket?: string[] | undefined;
|
||||
gerrit?: string[] | undefined;
|
||||
gitlab?: string[] | undefined;
|
||||
gitea?: string[] | undefined;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
Reference in New Issue
Block a user