Merge pull request #24894 from elaine-mattos/feat/gitlab-pipeline-trigger-action

Add scaffolder action to trigger GitLab pipelines
This commit is contained in:
Ben Lambert
2024-05-27 16:33:59 +02:00
committed by GitHub
24 changed files with 469 additions and 43 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-gitlab': patch
---
Add new `gitlab:pipeline:trigger` action to trigger GitLab pipelines.
@@ -149,8 +149,8 @@ export function createPublishGitlabAction(options: {
squash_option?:
| 'always'
| 'never'
| 'default_on'
| 'default_off'
| 'default_on'
| undefined;
topics?: string[] | undefined;
visibility?: 'internal' | 'private' | 'public' | undefined;
@@ -201,6 +201,22 @@ export const createPublishGitlabMergeRequestAction: (options: {
JsonObject
>;
// @public
export const createTriggerGitlabPipelineAction: (options: {
integrations: ScmIntegrationRegistry;
}) => TemplateAction<
{
branch: string;
repoUrl: string;
projectId: number;
tokenDescription: string;
token?: string | undefined;
},
{
pipelineUrl: string;
}
>;
// @public
const gitlabModule: () => BackendFeature;
export default gitlabModule;
@@ -14,12 +14,12 @@
* limitations under the License.
*/
import { createGitlabGroupEnsureExistsAction } from './createGitlabGroupEnsureExistsAction';
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
import { ConfigReader } from '@backstage/core-app-api';
import { ScmIntegrations } from '@backstage/integration';
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
import yaml from 'yaml';
import { examples } from './createGitlabGroupEnsureExistsAction.examples';
import { createGitlabGroupEnsureExistsAction } from './gitlabGroupEnsureExists';
import { examples } from './gitlabGroupEnsureExists.examples';
const mockGitlabClient = {
Groups: {
@@ -14,10 +14,10 @@
* limitations under the License.
*/
import { createGitlabGroupEnsureExistsAction } from './createGitlabGroupEnsureExistsAction';
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
import { ConfigReader } from '@backstage/core-app-api';
import { ScmIntegrations } from '@backstage/integration';
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
import { createGitlabGroupEnsureExistsAction } from './gitlabGroupEnsureExists';
const mockGitlabClient = {
Groups: {
@@ -14,14 +14,14 @@
* limitations under the License.
*/
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { Gitlab } from '@gitbeaker/node';
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import { GroupSchema } from '@gitbeaker/core/dist/types/resources/Groups';
import { Gitlab } from '@gitbeaker/node';
import { z } from 'zod';
import commonGitlabConfig from '../commonGitlabConfig';
import { getToken } from '../util';
import { z } from 'zod';
import { examples } from './createGitlabGroupEnsureExistsAction.examples';
import { examples } from './gitlabGroupEnsureExists.examples';
/**
* Creates an `gitlab:group:ensureExists` Scaffolder action.
@@ -14,10 +14,10 @@
* limitations under the License.
*/
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
import { createGitlabIssueAction, IssueType } from './createGitlabIssueAction';
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';
const mockGitlabClient = {
Issues: {
@@ -18,7 +18,7 @@ import { InputError } from '@backstage/errors';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import commonGitlabConfig from '../commonGitlabConfig';
import { examples } from './createGitlabIssueAction.examples';
import { examples } from './gitlabIssueCreate.examples';
import { z } from 'zod';
import { checkEpicScope, convertDate, getClient, parseRepoUrl } from '../util';
import { Gitlab, CreateIssueOptions, IssueSchema } from '@gitbeaker/rest';
@@ -0,0 +1,41 @@
/*
* 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: 'Trigger a GitLab Project Pipeline',
example: yaml.stringify({
steps: [
{
id: 'triggerPipeline',
name: 'Trigger Project Pipeline',
action: 'gitlab:pipeline:trigger',
input: {
...commonGitlabConfigExample,
projectId: 12,
tokenDescription:
'This is the text that will appear in the pipeline token',
token: 'glpt-xxxxxxxxxxxx',
branch: 'main',
},
},
],
}),
},
];
@@ -0,0 +1,235 @@
/*
* 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 { ConfigReader } from '@backstage/core-app-api';
import { ScmIntegrations } from '@backstage/integration';
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
import { createTriggerGitlabPipelineAction } from './gitlabPipelineTrigger';
const mockGitlabClient = {
PipelineTriggerTokens: {
create: jest.fn(),
trigger: jest.fn(),
remove: jest.fn(),
},
};
jest.mock('@gitbeaker/rest', () => ({
Gitlab: class {
constructor() {
return mockGitlabClient;
}
},
}));
describe('gitlab:pipeline:trigger', () => {
beforeEach(() => {
jest.resetModules();
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: 'glpat-abcdef',
apiBaseUrl: 'https://gitlab.com/api/v4',
},
],
},
});
const integrations = ScmIntegrations.fromConfig(config);
const action = createTriggerGitlabPipelineAction({ integrations });
it('should return a Pipeline Token Id', async () => {
const mockContext = createMockActionContext({
input: {
repoUrl: 'gitlab.com?repo=repo&owner=owner',
projectId: 123,
tokenDescription: 'My cool pipeline token',
branch: 'main',
},
workspacePath: 'seen2much',
});
mockGitlabClient.PipelineTriggerTokens.create.mockResolvedValue({
id: 42,
description: 'My cool pipeline token',
createdAt: new Date().toISOString(),
last_used: null,
token: 'glptt-abcdef',
updated_at: new Date().toISOString(),
owner: null,
});
mockGitlabClient.PipelineTriggerTokens.trigger.mockResolvedValue({
id: 99,
web_url: 'https://gitlab.com/hangar18-/pipelines/99',
});
await action.handler({
...mockContext,
});
expect(mockGitlabClient.PipelineTriggerTokens.create).toHaveBeenCalledWith(
123,
'My cool pipeline token',
);
expect(mockGitlabClient.PipelineTriggerTokens.trigger).toHaveBeenCalledWith(
123,
'main',
'glptt-abcdef',
);
expect(mockGitlabClient.PipelineTriggerTokens.remove).toHaveBeenCalledWith(
123,
42,
);
expect(mockContext.output).toHaveBeenCalledWith(
'pipelineUrl',
'https://gitlab.com/hangar18-/pipelines/99',
);
});
it('should throw error if pipeline token cannot be created', async () => {
const mockContext = createMockActionContext({
input: {
repoUrl: 'gitlab.com?repo=repo&owner=owner',
projectId: 123,
tokenDescription: 'My cool pipeline token',
branch: 'main',
},
workspacePath: 'seen2much',
});
mockGitlabClient.PipelineTriggerTokens.create.mockRejectedValue(
new Error('Failed to create token'),
);
await expect(
action.handler({
...mockContext,
}),
).rejects.toThrow('Failed to create token');
expect(mockGitlabClient.PipelineTriggerTokens.create).toHaveBeenCalledWith(
123,
'My cool pipeline token',
);
expect(
mockGitlabClient.PipelineTriggerTokens.trigger,
).not.toHaveBeenCalled();
expect(
mockGitlabClient.PipelineTriggerTokens.remove,
).not.toHaveBeenCalled();
});
it('throw error if pipeline cannot be triggered', async () => {
const mockContext = createMockActionContext({
input: {
repoUrl: 'gitlab.com?repo=repo&owner=owner',
projectId: 123,
tokenDescription: 'My cool pipeline token',
branch: 'main',
},
workspacePath: 'seen2much',
});
mockGitlabClient.PipelineTriggerTokens.create.mockResolvedValue({
id: 42,
description: 'My cool pipeline token',
createdAt: new Date().toISOString(),
last_used: null,
token: 'glptt-abcdef',
updated_at: new Date().toISOString(),
owner: null,
});
mockGitlabClient.PipelineTriggerTokens.trigger.mockRejectedValue(
new Error('Failed to trigger pipeline'),
);
await expect(
action.handler({
...mockContext,
}),
).rejects.toThrow('Failed to trigger pipeline');
expect(mockGitlabClient.PipelineTriggerTokens.create).toHaveBeenCalledWith(
123,
'My cool pipeline token',
);
expect(mockGitlabClient.PipelineTriggerTokens.trigger).toHaveBeenCalledWith(
123,
'main',
'glptt-abcdef',
);
expect(mockGitlabClient.PipelineTriggerTokens.remove).toHaveBeenCalledWith(
123,
42,
);
});
it('should clean up pipeline token on failure', async () => {
const mockContext = createMockActionContext({
input: {
repoUrl: 'gitlab.com?repo=repo&owner=owner',
projectId: 123,
tokenDescription: 'My cool pipeline token',
branch: 'main',
},
workspacePath: 'seen2much',
});
mockGitlabClient.PipelineTriggerTokens.create.mockResolvedValue({
id: 42,
description: 'My cool pipeline token',
createdAt: new Date().toISOString(),
last_used: null,
token: 'glptt-abcdef',
updated_at: new Date().toISOString(),
owner: null,
});
mockGitlabClient.PipelineTriggerTokens.trigger.mockRejectedValue(
new Error('Failed to trigger pipeline'),
);
await expect(
action.handler({
...mockContext,
}),
).rejects.toThrow('Failed to trigger pipeline');
expect(mockGitlabClient.PipelineTriggerTokens.remove).toHaveBeenCalledWith(
123,
42,
);
});
});
@@ -0,0 +1,126 @@
/*
* 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 {
ExpandedPipelineSchema,
PipelineTriggerTokenSchema,
} from '@gitbeaker/rest';
import { z } from 'zod';
import commonGitlabConfig from '../commonGitlabConfig';
import { getClient, parseRepoUrl } from '../util';
import { examples } from './gitlabPipelineTrigger.examples';
const pipelineInputProperties = z.object({
projectId: z.number().describe('Project Id'),
tokenDescription: z.string().describe('Pipeline token description'),
branch: z.string().describe('Project branch'),
});
const pipelineOutputProperties = z.object({
pipelineUrl: z.string({ description: 'Pipeline Url' }),
});
/**
* Creates a `gitlab:pipeline:trigger` Scaffolder action.
*
* @param options - Templating configuration.
* @public
*/
export const createTriggerGitlabPipelineAction = (options: {
integrations: ScmIntegrationRegistry;
}) => {
const { integrations } = options;
return createTemplateAction({
id: 'gitlab:pipeline:trigger',
description: 'Triggers a GitLab Pipeline.',
examples,
schema: {
input: commonGitlabConfig.merge(pipelineInputProperties),
output: pipelineOutputProperties,
},
async handler(ctx) {
let pipelineTokenResponse: PipelineTriggerTokenSchema | null = null;
const { repoUrl, projectId, tokenDescription, token, branch } =
commonGitlabConfig.merge(pipelineInputProperties).parse(ctx.input);
const { host } = parseRepoUrl(repoUrl, integrations);
const api = getClient({ host, integrations, token });
try {
// Create a pipeline token
pipelineTokenResponse = (await api.PipelineTriggerTokens.create(
projectId,
tokenDescription,
)) as PipelineTriggerTokenSchema;
if (!pipelineTokenResponse.token) {
ctx.logger.error('Failed to create pipeline token.');
return;
}
ctx.logger.info(
`Pipeline token id ${pipelineTokenResponse.id} created.`,
);
// Use the pipeline token to trigger the pipeline in the project
const pipelineTriggerResponse =
(await api.PipelineTriggerTokens.trigger(
projectId,
branch,
pipelineTokenResponse.token,
)) as ExpandedPipelineSchema;
if (!pipelineTriggerResponse.id) {
ctx.logger.error('Failed to trigger pipeline.');
return;
}
ctx.logger.info(`Pipeline id ${pipelineTriggerResponse.id} triggered.`);
ctx.output('pipelineUrl', pipelineTriggerResponse.web_url);
} 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 trigger Pipeline: ${error.message}`);
} finally {
// Delete the pipeline token if it was created
if (pipelineTokenResponse && pipelineTokenResponse.id) {
try {
await api.PipelineTriggerTokens.remove(
projectId,
pipelineTokenResponse.id,
);
ctx.logger.info(
`Deleted pipeline token ${pipelineTokenResponse.id}.`,
);
} catch (error: any) {
ctx.logger.error(
`Failed to delete pipeline token id ${pipelineTokenResponse.id}.`,
);
}
}
}
},
});
};
@@ -15,10 +15,10 @@
*/
import { ConfigReader } from '@backstage/config';
import { ScmIntegrations } from '@backstage/integration';
import yaml from 'yaml';
import { createGitlabProjectAccessTokenAction } from './createGitlabProjectAccessTokenAction'; // Adjust the import based on your project structure
import { examples } from './createGitlabProjectAccessTokenAction.examples';
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
import yaml from 'yaml';
import { createGitlabProjectAccessTokenAction } from './gitlabProjectAccessTokenCreate'; // Adjust the import based on your project structure
import { examples } from './gitlabProjectAccessTokenCreate.examples';
import { DateTime } from 'luxon';
@@ -21,7 +21,7 @@ import { AccessTokenScopes, Gitlab } from '@gitbeaker/rest';
import { DateTime } from 'luxon';
import { z } from 'zod';
import { getToken } from '../util';
import { examples } from './createGitlabProjectAccessTokenAction.examples';
import { examples } from './gitlabProjectAccessTokenCreate.examples';
/**
* Creates a `gitlab:projectAccessToken:create` Scaffolder action.
@@ -14,12 +14,12 @@
* limitations under the License.
*/
import { createGitlabProjectDeployTokenAction } from './createGitlabProjectDeployTokenAction';
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
import { ScmIntegrations } from '@backstage/integration';
import { ConfigReader } from '@backstage/config';
import { ScmIntegrations } from '@backstage/integration';
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
import yaml from 'yaml';
import { examples } from './createGitlabProjectDeployTokenAction.examples';
import { createGitlabProjectDeployTokenAction } from './gitlabProjectDeployTokenCreate';
import { examples } from './gitlabProjectDeployTokenCreate.examples';
const mockGitlabClient = {
ProjectDeployTokens: {
@@ -14,10 +14,10 @@
* limitations under the License.
*/
import { createGitlabProjectDeployTokenAction } from './createGitlabProjectDeployTokenAction';
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
import { ScmIntegrations } from '@backstage/integration';
import { ConfigReader } from '@backstage/config';
import { ScmIntegrations } from '@backstage/integration';
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
import { createGitlabProjectDeployTokenAction } from './gitlabProjectDeployTokenCreate';
const mockGitlabClient = {
ProjectDeployTokens: {
@@ -14,15 +14,15 @@
* limitations under the License.
*/
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import { Gitlab } from '@gitbeaker/node';
import { InputError } from '@backstage/errors';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import { DeployTokenScope } from '@gitbeaker/core/dist/types/templates/ResourceDeployTokens';
import { Gitlab } from '@gitbeaker/node';
import { z } from 'zod';
import commonGitlabConfig from '../commonGitlabConfig';
import { getToken } from '../util';
import { InputError } from '@backstage/errors';
import { z } from 'zod';
import { examples } from './createGitlabProjectDeployTokenAction.examples';
import { examples } from './gitlabProjectDeployTokenCreate.examples';
/**
* Creates a `gitlab:projectDeployToken:create` Scaffolder action.
@@ -14,12 +14,12 @@
* limitations under the License.
*/
import { createGitlabProjectVariableAction } from './createGitlabProjectVariableAction';
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
import { ScmIntegrations } from '@backstage/integration';
import { ConfigReader } from '@backstage/config';
import { ScmIntegrations } from '@backstage/integration';
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
import yaml from 'yaml';
import { examples } from './createGitlabProjectVariableAction.examples';
import { createGitlabProjectVariableAction } from './gitlabProjectVariableCreate';
import { examples } from './gitlabProjectVariableCreate.examples';
const mockGitlabClient = {
ProjectVariables: {
@@ -14,13 +14,13 @@
* limitations under the License.
*/
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import { Gitlab } from '@gitbeaker/node';
import { getToken } from '../util';
import commonGitlabConfig from '../commonGitlabConfig';
import { z } from 'zod';
import { examples } from './createGitlabProjectVariableAction.examples';
import commonGitlabConfig from '../commonGitlabConfig';
import { getToken } from '../util';
import { examples } from './gitlabProjectVariableCreate.examples';
/**
* Creates a `gitlab:projectVariable:create` Scaffolder action.
@@ -13,11 +13,12 @@
* 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';
export * from './gitlab';
export * from './gitlabGroupEnsureExists';
export * from './gitlabIssueCreate';
export * from './gitlabMergeRequest';
export * from './gitlabPipelineTrigger';
export * from './gitlabProjectAccessTokenCreate';
export * from './gitlabProjectDeployTokenCreate';
export * from './gitlabProjectVariableCreate';
export * from './gitlabRepoPush';
@@ -17,6 +17,7 @@ import {
coreServices,
createBackendModule,
} from '@backstage/backend-plugin-api';
import { ScmIntegrations } from '@backstage/integration';
import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha';
import {
createGitlabGroupEnsureExistsAction,
@@ -27,8 +28,8 @@ import {
createGitlabRepoPushAction,
createPublishGitlabAction,
createPublishGitlabMergeRequestAction,
createTriggerGitlabPipelineAction,
} from './actions';
import { ScmIntegrations } from '@backstage/integration';
/**
* @public
@@ -55,6 +56,7 @@ export const gitlabModule = createBackendModule({
createGitlabRepoPushAction({ integrations }),
createPublishGitlabAction({ config, integrations }),
createPublishGitlabMergeRequestAction({ integrations }),
createTriggerGitlabPipelineAction({ integrations }),
);
},
});