Merge branch 'backstage:master' into master

This commit is contained in:
JGoggers
2024-04-11 17:57:20 +02:00
committed by GitHub
11 changed files with 753 additions and 5 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-gitlab': patch
---
Add examples for `publish:gitlab:merge-request` scaffolder action & improve related tests
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-gitlab': patch
---
Add examples for `gitlab:group:ensureExists` scaffolder action & improve related tests
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-badges': patch
---
Update README to fix invalid import command
+1 -2
View File
@@ -88,8 +88,7 @@ yarn --cwd packages/app add @backstage/plugin-badges
This plugin requires explicit registration, so you will need to add it to your App's `plugins.ts` file:
```ts
// ...
export { badgesPlugin } from '@backstage/plugin-badges';
import { badgesPlugin } from '@backstage/plugin-badges';
```
If you don't have a `plugins.ts` file see: [troubleshooting](#troubleshooting)
@@ -0,0 +1,198 @@
/*
* 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 { 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 yaml from 'yaml';
import { examples } from './createGitlabGroupEnsureExistsAction.examples';
const mockGitlabClient = {
Groups: {
search: jest.fn(),
create: jest.fn(),
},
};
jest.mock('@gitbeaker/node', () => ({
Gitlab: class {
constructor() {
return mockGitlabClient;
}
},
}));
describe('gitlab:group:ensureExists', () => {
const mockContext = createMockActionContext();
afterEach(() => {
jest.resetAllMocks();
});
it(`Should ${examples[0].description}`, async () => {
mockGitlabClient.Groups.search.mockResolvedValue([]);
mockGitlabClient.Groups.create.mockResolvedValue({
id: 3,
full_path: 'group1',
});
const config = new ConfigReader({
integrations: {
gitlab: [
{
host: 'gitlab.com',
token: 'tokenlols',
apiBaseUrl: 'https://api.gitlab.com',
},
],
},
});
const integrations = ScmIntegrations.fromConfig(config);
const action = createGitlabGroupEnsureExistsAction({ integrations });
await action.handler({
...mockContext,
input: yaml.parse(examples[0].example).steps[0].input,
});
expect(mockGitlabClient.Groups.create).toHaveBeenCalledWith(
'group1',
'group1',
{},
);
expect(mockContext.output).toHaveBeenCalledWith('groupId', 3);
});
it(`Should ${examples[1].description}`, async () => {
mockGitlabClient.Groups.search.mockResolvedValue([
{
id: 1,
full_path: 'group1',
},
]);
mockGitlabClient.Groups.create.mockResolvedValue({
id: 3,
full_path: 'group1/group2',
});
const config = new ConfigReader({
integrations: {
gitlab: [
{
host: 'gitlab.com',
token: 'tokenlols',
apiBaseUrl: 'https://api.gitlab.com',
},
],
},
});
const integrations = ScmIntegrations.fromConfig(config);
const action = createGitlabGroupEnsureExistsAction({ integrations });
await action.handler({
...mockContext,
input: yaml.parse(examples[1].example).steps[0].input,
});
expect(mockGitlabClient.Groups.create).toHaveBeenCalledWith(
'group2',
'group2',
{
parent_id: 1,
},
);
expect(mockContext.output).toHaveBeenCalledWith('groupId', 3);
});
it(`Should ${examples[2].description}`, async () => {
mockGitlabClient.Groups.search.mockResolvedValue([
{
id: 1,
full_path: 'group1',
},
{
id: 2,
full_path: 'group1/group2',
},
]);
mockGitlabClient.Groups.create.mockResolvedValue({
id: 3,
full_path: 'group1/group2/group3',
});
const config = new ConfigReader({
integrations: {
gitlab: [
{
host: 'gitlab.com',
token: 'tokenlols',
apiBaseUrl: 'https://api.gitlab.com',
},
],
},
});
const integrations = ScmIntegrations.fromConfig(config);
const action = createGitlabGroupEnsureExistsAction({ integrations });
await action.handler({
...mockContext,
input: yaml.parse(examples[2].example).steps[0].input,
});
expect(mockGitlabClient.Groups.create).toHaveBeenCalledWith(
'group3',
'group3',
{
parent_id: 2,
},
);
expect(mockContext.output).toHaveBeenCalledWith('groupId', 3);
});
it(`Should ${examples[3].description}`, async () => {
const config = new ConfigReader({
integrations: {
gitlab: [
{
host: 'gitlab.com',
token: 'tokenlols',
apiBaseUrl: 'https://api.gitlab.com',
},
],
},
});
const integrations = ScmIntegrations.fromConfig(config);
const action = createGitlabGroupEnsureExistsAction({ integrations });
await action.handler({
...mockContext,
isDryRun: yaml.parse(examples[3].example).steps[0].isDryRun,
input: yaml.parse(examples[3].example).steps[0].input,
});
expect(mockGitlabClient.Groups.search).not.toHaveBeenCalled();
expect(mockGitlabClient.Groups.create).not.toHaveBeenCalled();
expect(mockContext.output).toHaveBeenCalledWith('groupId', 42);
});
});
@@ -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';
export const examples: TemplateExample[] = [
{
description: 'Creating a group at the top level',
example: yaml.stringify({
steps: [
{
id: 'gitlabGroup',
name: 'Group',
action: 'gitlab:group:ensureExists',
input: {
repoUrl: 'gitlab.com',
path: ['group1'],
},
},
],
}),
},
{
description: 'Create a group nested within another group',
example: yaml.stringify({
steps: [
{
id: 'gitlabGroup',
name: 'Group',
action: 'gitlab:group:ensureExists',
input: {
repoUrl: 'gitlab.com',
path: ['group1', 'group2'],
},
},
],
}),
},
{
description: 'Create a group nested within multiple other groups',
example: yaml.stringify({
steps: [
{
id: 'gitlabGroup',
name: 'Group',
action: 'gitlab:group:ensureExists',
input: {
repoUrl: 'gitlab.com',
path: ['group1', 'group2', 'group3'],
},
},
],
}),
},
{
description: 'Create a group in dry run mode',
example: yaml.stringify({
steps: [
{
id: 'gitlabGroup',
name: 'Group',
action: 'gitlab:group:ensureExists',
isDryRun: true,
input: {
repoUrl: 'https://gitlab.com/my-repo',
path: ['group1', 'group2', 'group3'],
},
},
],
}),
},
];
@@ -21,6 +21,7 @@ import { GroupSchema } from '@gitbeaker/core/dist/types/resources/Groups';
import commonGitlabConfig from '../commonGitlabConfig';
import { getToken } from '../util';
import { z } from 'zod';
import { examples } from './createGitlabGroupEnsureExistsAction.examples';
/**
* Creates an `gitlab:group:ensureExists` Scaffolder action.
@@ -36,6 +37,7 @@ export const createGitlabGroupEnsureExistsAction = (options: {
id: 'gitlab:group:ensureExists',
description: 'Ensures a Gitlab group exists',
supportsDryRun: true,
examples,
schema: {
input: commonGitlabConfig.merge(
z.object({
@@ -0,0 +1,305 @@
/*
* 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 { createRootLogger } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import { ScmIntegrations } from '@backstage/integration';
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
import { createPublishGitlabMergeRequestAction } from './gitlabMergeRequest';
import { createMockDirectory } from '@backstage/backend-test-utils';
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
import { examples } from './gitlabMergeRequest.examples';
import yaml from 'yaml';
// Make sure root logger is initialized ahead of FS mock
createRootLogger();
const mockGitlabClient = {
Namespaces: {
show: jest.fn(),
},
Branches: {
create: jest.fn(),
},
Commits: {
create: jest.fn(),
},
MergeRequests: {
create: jest.fn(async (_: any) => {
return {
default_branch: 'main',
};
}),
},
Projects: {
create: jest.fn(),
show: jest.fn(async (_: any) => {
return {
default_branch: 'main',
};
}),
},
Users: {
current: jest.fn(),
username: jest.fn(async (user: string) => {
const users: string[] = ['John Smith', 'my-assignee'];
if (!users.includes(user)) throw new Error('user does not exist');
else
return [
{
id: 123,
},
];
}),
},
};
jest.mock('@gitbeaker/node', () => ({
Gitlab: class {
constructor() {
return mockGitlabClient;
}
},
}));
describe('createGitLabMergeRequest', () => {
let instance: TemplateAction<any>;
const mockDir = createMockDirectory();
const workspacePath = mockDir.resolve('workspace');
beforeEach(() => {
jest.clearAllMocks();
mockDir.clear();
const config = new ConfigReader({
integrations: {
gitlab: [
{
host: 'gitlab.com',
token: 'token',
apiBaseUrl: 'https://api.gitlab.com',
},
{
host: 'hosted.gitlab.com',
apiBaseUrl: 'https://api.hosted.gitlab.com',
},
],
},
});
const integrations = ScmIntegrations.fromConfig(config);
instance = createPublishGitlabMergeRequestAction({ integrations });
});
describe('createGitLabMergeRequestWithAssignee', () => {
it(`Should ${examples[0].description}`, async () => {
mockDir.setContent({
[workspacePath]: {
source: { 'foo.txt': 'Hello there!' },
irrelevant: { 'bar.txt': 'Nothing to see here' },
},
});
const input = yaml.parse(examples[0].example).steps[0].input;
const ctx = createMockActionContext({ input, workspacePath });
await instance.handler(ctx);
expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith(
'owner/repo',
'new-mr',
'main',
'Create my new MR',
{
description: 'This MR is really good',
removeSourceBranch: false,
assigneeId: 123,
},
);
});
it('assignee is not set when a valid assignee username is not passed in options', async () => {
const input = {
repoUrl: 'gitlab.com?repo=repo&owner=owner',
title: 'Create my new MR',
branchName: 'new-mr',
description: 'This is an important change',
removeSourceBranch: false,
targetPath: 'Subdirectory',
assingnee: 'John Doe',
};
mockDir.setContent({
[workspacePath]: {
source: { 'foo.txt': 'Hello there!' },
irrelevant: { 'bar.txt': 'Nothing to see here' },
},
});
const ctx = createMockActionContext({ input, workspacePath });
await instance.handler(ctx);
expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith(
'owner/repo',
'new-mr',
'main',
'Create my new MR',
{
description: 'This is an important change',
removeSourceBranch: false,
assigneeId: undefined,
},
);
});
});
describe('createGitLabMergeRequestWithRemoveBranch', () => {
it(`Should ${examples[1].description}`, async () => {
const input = yaml.parse(examples[1].example).steps[0].input;
mockDir.setContent({
[workspacePath]: {
source: { 'foo.txt': 'Hello there!' },
irrelevant: { 'bar.txt': 'Nothing to see here' },
},
});
const ctx = createMockActionContext({ input, workspacePath });
await instance.handler(ctx);
expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith(
'owner/repo',
'new-mr',
'main',
'Create my new MR',
{ description: 'This MR is really good', removeSourceBranch: true },
);
});
});
describe('createGitLabMergeRequestWithSpecifiedTargetBranch', () => {
it(`Should ${examples[2].description}`, async () => {
const input = yaml.parse(examples[2].example).steps[0].input;
mockDir.setContent({
[workspacePath]: {
source: { 'foo.txt': 'Hello there!' },
irrelevant: { 'bar.txt': 'Nothing to see here' },
},
});
const ctx = createMockActionContext({ input, workspacePath });
await instance.handler(ctx);
expect(mockGitlabClient.Projects.show).not.toHaveBeenCalled();
expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith(
'owner/repo',
'new-mr',
'test',
);
expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith(
'owner/repo',
'new-mr',
'test',
'Create my new MR',
{ description: 'This MR is really good', removeSourceBranch: false },
);
expect(ctx.output).toHaveBeenCalledWith('targetBranchName', 'test');
});
});
describe('createGitLabMergeRequestWithCommitAction', () => {
it(`Should ${examples[3].description}`, async () => {
const input = yaml.parse(examples[3].example).steps[0].input;
mockDir.setContent({
[workspacePath]: {
source: { 'foo.txt': 'Hello there!' },
irrelevant: { 'bar.txt': 'Nothing to see here' },
},
});
const ctx = createMockActionContext({ input, workspacePath });
await instance.handler(ctx);
expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith(
'owner/repo',
'new-mr',
'Create my new MR',
[
{
action: 'create',
filePath: 'source/foo.txt',
content: 'SGVsbG8gdGhlcmUh',
encoding: 'base64',
execute_filemode: false,
},
],
);
});
it(`Should ${examples[4].description}`, async () => {
const input = yaml.parse(examples[4].example).steps[0].input;
mockDir.setContent({
[workspacePath]: {
source: { 'foo.txt': 'Hello there!' },
irrelevant: { 'bar.txt': 'Nothing to see here' },
},
});
const ctx = createMockActionContext({ input, workspacePath });
await instance.handler(ctx);
expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith(
'owner/repo',
'new-mr',
'Create my new MR',
[
{
action: 'delete',
filePath: 'source/foo.txt',
content: 'SGVsbG8gdGhlcmUh',
encoding: 'base64',
execute_filemode: false,
},
],
);
});
it(`Should ${examples[5].description}`, async () => {
const input = yaml.parse(examples[5].example).steps[0].input;
mockDir.setContent({
[workspacePath]: {
source: { 'foo.txt': 'Hello there!' },
irrelevant: { 'bar.txt': 'Nothing to see here' },
},
});
const ctx = createMockActionContext({ input, workspacePath });
await instance.handler(ctx);
expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith(
'owner/repo',
'new-mr',
'Create my new MR',
[
{
action: 'update',
filePath: 'source/foo.txt',
content: 'SGVsbG8gdGhlcmUh',
encoding: 'base64',
execute_filemode: false,
},
],
);
});
});
});
@@ -0,0 +1,142 @@
/*
* 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';
export const examples: TemplateExample[] = [
{
description: 'Create a merge request with a specific assignee',
example: yaml.stringify({
steps: [
{
id: 'createMergeRequest',
action: 'publish:gitlab:merge-request',
name: 'Create a Merge Request',
input: {
repoUrl: 'gitlab.com?repo=repo&owner=owner',
title: 'Create my new MR',
description: 'This MR is really good',
sourcePath: './path/to/my/changes',
branchName: 'new-mr',
assignee: 'my-assignee',
},
},
],
}),
},
{
description:
'Create a merge request with removal of source branch after merge',
example: yaml.stringify({
steps: [
{
id: 'createMergeRequest',
action: 'publish:gitlab:merge-request',
name: 'Create a Merge Request',
input: {
repoUrl: 'gitlab.com?repo=repo&owner=owner',
title: 'Create my new MR',
description: 'This MR is really good',
sourcePath: './path/to/my/changes',
branchName: 'new-mr',
removeSourceBranch: true,
},
},
],
}),
},
{
description: 'Create a merge request with a target branch',
example: yaml.stringify({
steps: [
{
id: 'createMergeRequest',
action: 'publish:gitlab:merge-request',
name: 'Create a Merge Request',
input: {
repoUrl: 'gitlab.com?repo=repo&owner=owner',
title: 'Create my new MR',
description: 'This MR is really good',
sourcePath: './path/to/my/changes',
branchName: 'new-mr',
targetBranchName: 'test',
targetPath: 'Subdirectory',
},
},
],
}),
},
{
description: 'Create a merge request with a commit action as create',
example: yaml.stringify({
steps: [
{
id: 'createMergeRequest',
action: 'publish:gitlab:merge-request',
name: 'Create a Merge Request',
input: {
repoUrl: 'gitlab.com?repo=repo&owner=owner',
title: 'Create my new MR',
branchName: 'new-mr',
description: 'MR description',
commitAction: 'create',
targetPath: 'source',
},
},
],
}),
},
{
description: 'Create a merge request with a commit action as delete',
example: yaml.stringify({
steps: [
{
id: 'createMergeRequest',
action: 'publish:gitlab:merge-request',
name: 'Create a Merge Request',
input: {
repoUrl: 'gitlab.com?repo=repo&owner=owner',
title: 'Create my new MR',
branchName: 'new-mr',
description: 'MR description',
commitAction: 'delete',
targetPath: 'source',
},
},
],
}),
},
{
description: 'Create a merge request with a commit action as update',
example: yaml.stringify({
steps: [
{
id: 'createMergeRequest',
action: 'publish:gitlab:merge-request',
name: 'Create a Merge Request',
input: {
repoUrl: 'gitlab.com?repo=repo&owner=owner',
title: 'Create my new MR',
branchName: 'new-mr',
description: 'MR description',
commitAction: 'update',
targetPath: 'source',
},
},
],
}),
},
];
@@ -25,6 +25,7 @@ import { ScmIntegrationRegistry } from '@backstage/integration';
import { InputError } from '@backstage/errors';
import { resolveSafeChildPath } from '@backstage/backend-common';
import { createGitlabApi } from './helpers';
import { examples } from './gitlabMergeRequest.examples';
/**
* Create a new action that creates a gitlab merge request.
@@ -52,6 +53,7 @@ export const createPublishGitlabMergeRequestAction = (options: {
assignee?: string;
}>({
id: 'publish:gitlab:merge-request',
examples,
schema: {
input: {
required: ['repoUrl', 'branchName'],
+3 -3
View File
@@ -43644,8 +43644,8 @@ __metadata:
linkType: hard
"testcontainers@npm:^10.0.0":
version: 10.7.2
resolution: "testcontainers@npm:10.7.2"
version: 10.8.2
resolution: "testcontainers@npm:10.8.2"
dependencies:
"@balena/dockerignore": ^1.0.2
"@types/dockerode": ^3.3.24
@@ -43662,7 +43662,7 @@ __metadata:
ssh-remote-port-forward: ^1.0.4
tar-fs: ^3.0.5
tmp: ^0.2.1
checksum: b4650509e3e072b96cd8ed75fdeeaa8a51a81cc9d45aabec26d4915cf9942e051f14fce4d23a29e70a007c2e8b3ce46f72dac602962f21c5a8d27976c846ef24
checksum: 3a97ad705c98abc5066577699be5e791090ef2a5c45fee3a203196fa09d0cb66c7709f6ae8168c025ed97a07f1f5f0676a34e748680514e732e90671720d34c6
languageName: node
linkType: hard