chore: create gerrit module

Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
blam
2023-12-06 12:56:44 +01:00
parent 81f57ff279
commit 823dee3b23
15 changed files with 98 additions and 800 deletions
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
@@ -0,0 +1,5 @@
# @backstage/plugin-scaffolder-backend-module-gerrit
The gerrit module for [@backstage/plugin-scaffolder-backend](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend).
_This plugin was created through the Backstage CLI_
@@ -0,0 +1,42 @@
{
"name": "@backstage/plugin-scaffolder-backend-module-gerrit",
"description": "The gerrit module for @backstage/plugin-scaffolder-backend",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "backend-plugin-module"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/plugin-scaffolder-node": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/integration": "workspace:^",
"@backstage/config": "workspace:^",
"node-fetch": "^2.6.7",
"yaml": "^2.0.0"
},
"devDependencies": {
"@backstage/backend-common": "workspace:^",
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"msw": "^1.0.0"
},
"files": [
"dist"
]
}
@@ -14,8 +14,9 @@
* limitations under the License.
*/
jest.mock('../helpers', () => {
jest.mock('@backstage/plugin-scaffolder-node', () => {
return {
...jest.requireActual('@backstage/plugin-scaffolder-node'),
initRepoAndPush: jest.fn().mockResolvedValue({
commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6',
}),
@@ -34,7 +35,7 @@ import { ScmIntegrations } from '@backstage/integration';
import { ConfigReader } from '@backstage/config';
import { getVoidLogger } from '@backstage/backend-common';
import { PassThrough } from 'stream';
import { initRepoAndPush } from '../helpers';
import { initRepoAndPush } from '@backstage/plugin-scaffolder-node';
describe('publish:gerrit', () => {
const config = new ConfigReader({
@@ -22,10 +22,13 @@ import {
getGerritRequestOptions,
ScmIntegrationRegistry,
} from '@backstage/integration';
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import { getRepoSourceDirectory, parseRepoUrl } from './util';
import {
createTemplateAction,
initRepoAndPush,
getRepoSourceDirectory,
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
import fetch, { Response, RequestInit } from 'node-fetch';
import { initRepoAndPush } from '../helpers';
import { examples } from './gerrit.examples';
const createGerritProject = async (
@@ -14,14 +14,19 @@
* limitations under the License.
*/
jest.mock('../helpers');
jest.mock('@backstage/plugin-scaffolder-node', () => {
return {
...jest.requireActual('@backstage/plugin-scaffolder-node'),
commitAndPushRepo: jest.fn(),
};
});
import { createPublishGerritReviewAction } from './gerritReview';
import { ScmIntegrations } from '@backstage/integration';
import { ConfigReader } from '@backstage/config';
import { getVoidLogger } from '@backstage/backend-common';
import { PassThrough } from 'stream';
import { commitAndPushRepo } from '../helpers';
import { commitAndPushRepo } from '@backstage/plugin-scaffolder-node';
describe('publish:gerrit:review', () => {
const config = new ConfigReader({
@@ -18,9 +18,12 @@ import crypto from 'crypto';
import { InputError } from '@backstage/errors';
import { Config } from '@backstage/config';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import { getRepoSourceDirectory, parseRepoUrl } from './util';
import { commitAndPushRepo } from '../helpers';
import {
createTemplateAction,
commitAndPushRepo,
getRepoSourceDirectory,
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
const generateGerritChangeId = (): string => {
const changeId = crypto.randomBytes(20).toString('hex');
@@ -13,8 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// const executeBitMask = 0o000111;
// const res = fileMode & executeBitMask;
// return res > 0;
// };
export * from './gerrit';
export * from './gerritReview';
@@ -0,0 +1,23 @@
/*
* 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.
*/
/**
* The gerrit module for @backstage/plugin-scaffolder-backend.
*
* @packageDocumentation
*/
export * from './actions';
@@ -1,319 +0,0 @@
/*
* 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 { Git, getVoidLogger } from '@backstage/backend-common';
import { commitAndPushRepo, entityRefToName, initRepoAndPush } from './helpers';
jest.mock('@backstage/backend-common', () => ({
Git: {
fromAuth: jest.fn().mockReturnValue({
init: jest.fn(),
add: jest.fn(),
checkout: jest.fn(),
commit: jest
.fn()
.mockResolvedValue('220f19cc36b551763d157f1b5e4a4b446165dbd6'),
fetch: jest.fn(),
addRemote: jest.fn(),
push: jest.fn(),
}),
},
getVoidLogger: jest.requireActual('@backstage/backend-common').getVoidLogger,
}));
const mockedGit = Git.fromAuth({
logger: getVoidLogger(),
});
describe('initRepoAndPush', () => {
afterEach(() => {
jest.clearAllMocks();
});
describe('with minimal parameters', () => {
beforeEach(async () => {
await initRepoAndPush({
dir: '/test/repo/dir/',
remoteUrl: 'git@github.com:test/repo.git',
auth: {
username: 'test-user',
password: 'test-password',
},
logger: getVoidLogger(),
});
});
it('initializes the repo', () => {
expect(mockedGit.init).toHaveBeenCalledWith({
dir: '/test/repo/dir/',
defaultBranch: 'master',
});
});
it('stages all files in the repo', () => {
expect(mockedGit.add).toHaveBeenCalledWith({
dir: '/test/repo/dir/',
filepath: '.',
});
});
it('creates an initial commit', () => {
expect(mockedGit.commit).toHaveBeenCalledWith({
dir: '/test/repo/dir/',
message: 'Initial commit',
author: {
name: 'Scaffolder',
email: 'scaffolder@backstage.io',
},
committer: {
name: 'Scaffolder',
email: 'scaffolder@backstage.io',
},
});
});
it('adds the appropriate remote', () => {
expect(mockedGit.addRemote).toHaveBeenCalledWith({
dir: '/test/repo/dir/',
url: 'git@github.com:test/repo.git',
remote: 'origin',
});
});
it('pushes to the remote', () => {
expect(mockedGit.push).toHaveBeenCalledWith({
dir: '/test/repo/dir/',
remote: 'origin',
});
});
});
it('with token', async () => {
await initRepoAndPush({
dir: '/test/repo/dir/',
remoteUrl: 'git@github.com:test/repo.git',
auth: {
token: 'test-token',
},
logger: getVoidLogger(),
});
expect(mockedGit.init).toHaveBeenCalledWith({
dir: '/test/repo/dir/',
defaultBranch: 'master',
});
});
it('allows overriding the default branch', async () => {
await initRepoAndPush({
dir: '/test/repo/dir/',
defaultBranch: 'trunk',
remoteUrl: 'git@github.com:test/repo.git',
auth: {
username: 'test-user',
password: 'test-password',
},
logger: getVoidLogger(),
});
expect(mockedGit.init).toHaveBeenCalledWith({
dir: '/test/repo/dir/',
defaultBranch: 'trunk',
});
});
it('allows overriding the author', async () => {
await initRepoAndPush({
dir: '/test/repo/dir/',
gitAuthorInfo: {
name: 'Custom Scaffolder Author',
email: 'scaffolder@example.org',
},
remoteUrl: 'git@github.com:test/repo.git',
auth: {
username: 'test-user',
password: 'test-password',
},
logger: getVoidLogger(),
});
expect(mockedGit.commit).toHaveBeenCalledWith({
dir: '/test/repo/dir/',
message: 'Initial commit',
author: {
name: 'Custom Scaffolder Author',
email: 'scaffolder@example.org',
},
committer: {
name: 'Custom Scaffolder Author',
email: 'scaffolder@example.org',
},
});
});
});
describe('commitAndPushRepo', () => {
afterEach(() => {
jest.clearAllMocks();
});
describe('with minimal parameters', () => {
beforeEach(async () => {
await commitAndPushRepo({
dir: '/test/repo/dir/',
auth: {
username: 'test-user',
password: 'test-password',
},
logger: getVoidLogger(),
commitMessage: 'commit message',
});
});
it('fetches commits', () => {
expect(mockedGit.fetch).toHaveBeenCalledWith({
dir: '/test/repo/dir/',
});
});
it('checkouts to master', () => {
expect(mockedGit.checkout).toHaveBeenCalledWith({
dir: '/test/repo/dir/',
ref: 'master',
});
});
it('stages all files in the repo', () => {
expect(mockedGit.add).toHaveBeenCalledWith({
dir: '/test/repo/dir/',
filepath: '.',
});
});
it('creates a commit', () => {
expect(mockedGit.commit).toHaveBeenCalledWith({
dir: '/test/repo/dir/',
message: 'commit message',
author: {
name: 'Scaffolder',
email: 'scaffolder@backstage.io',
},
committer: {
name: 'Scaffolder',
email: 'scaffolder@backstage.io',
},
});
});
it('pushes to the remote', () => {
expect(mockedGit.push).toHaveBeenCalledWith({
dir: '/test/repo/dir/',
remote: 'origin',
remoteRef: 'refs/heads/master',
});
});
});
it('allows overriding the default branch', async () => {
await commitAndPushRepo({
dir: '/test/repo/dir/',
auth: {
username: 'test-user',
password: 'test-password',
},
logger: getVoidLogger(),
commitMessage: 'commit message',
branch: 'otherbranch',
});
expect(mockedGit.checkout).toHaveBeenCalledWith({
dir: '/test/repo/dir/',
ref: 'otherbranch',
});
expect(mockedGit.push).toHaveBeenCalledWith({
dir: '/test/repo/dir/',
remote: 'origin',
remoteRef: 'refs/heads/otherbranch',
});
});
it('allows overriding the remote ref', async () => {
await commitAndPushRepo({
dir: '/test/repo/dir/',
auth: {
username: 'test-user',
password: 'test-password',
},
logger: getVoidLogger(),
commitMessage: 'commit message',
remoteRef: 'refs/for/master',
});
expect(mockedGit.checkout).toHaveBeenCalledWith({
dir: '/test/repo/dir/',
ref: 'master',
});
expect(mockedGit.push).toHaveBeenCalledWith({
dir: '/test/repo/dir/',
remote: 'origin',
remoteRef: 'refs/for/master',
});
});
it('allows overriding the author', async () => {
await commitAndPushRepo({
dir: '/test/repo/dir/',
commitMessage: 'commit message',
gitAuthorInfo: {
name: 'Custom Scaffolder Author',
email: 'scaffolder@example.org',
},
auth: {
username: 'test-user',
password: 'test-password',
},
logger: getVoidLogger(),
branch: 'master',
});
expect(mockedGit.commit).toHaveBeenCalledWith({
dir: '/test/repo/dir/',
message: 'commit message',
author: {
name: 'Custom Scaffolder Author',
email: 'scaffolder@example.org',
},
committer: {
name: 'Custom Scaffolder Author',
email: 'scaffolder@example.org',
},
});
});
});
describe('entityRefToName', () => {
it.each([
'user:default/catpants',
'group:default/catpants',
'user:catpants',
'default/catpants',
'user:custom/catpants',
'group:custom/catpants',
'catpants',
])('should parse: "%s"', (entityName: string) => {
expect(entityRefToName(entityName)).toEqual('catpants');
});
});
@@ -1,253 +0,0 @@
/*
* 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 { Git } from '@backstage/backend-common';
import { Config } from '@backstage/config';
import { assertError } from '@backstage/errors';
import { Octokit } from 'octokit';
import { Logger } from 'winston';
export async function initRepoAndPush({
dir,
remoteUrl,
auth,
logger,
defaultBranch = 'master',
commitMessage = 'Initial commit',
gitAuthorInfo,
}: {
dir: string;
remoteUrl: string;
// For use cases where token has to be used with Basic Auth
// it has to be provided as password together with a username
// which may be a fixed value defined by the provider.
auth: { username: string; password: string } | { token: string };
logger: Logger;
defaultBranch?: string;
commitMessage?: string;
gitAuthorInfo?: { name?: string; email?: string };
}): Promise<{ commitHash: string }> {
const git = Git.fromAuth({
...auth,
logger,
});
await git.init({
dir,
defaultBranch,
});
await git.add({ dir, filepath: '.' });
// use provided info if possible, otherwise use fallbacks
const authorInfo = {
name: gitAuthorInfo?.name ?? 'Scaffolder',
email: gitAuthorInfo?.email ?? 'scaffolder@backstage.io',
};
const commitHash = await git.commit({
dir,
message: commitMessage,
author: authorInfo,
committer: authorInfo,
});
await git.addRemote({
dir,
url: remoteUrl,
remote: 'origin',
});
await git.push({
dir,
remote: 'origin',
});
return { commitHash };
}
export async function commitAndPushRepo({
dir,
auth,
logger,
commitMessage,
gitAuthorInfo,
branch = 'master',
remoteRef,
}: {
dir: string;
// For use cases where token has to be used with Basic Auth
// it has to be provided as password together with a username
// which may be a fixed value defined by the provider.
auth: { username: string; password: string } | { token: string };
logger: Logger;
commitMessage: string;
gitAuthorInfo?: { name?: string; email?: string };
branch?: string;
remoteRef?: string;
}): Promise<{ commitHash: string }> {
const git = Git.fromAuth({
...auth,
logger,
});
await git.fetch({ dir });
await git.checkout({ dir, ref: branch });
await git.add({ dir, filepath: '.' });
// use provided info if possible, otherwise use fallbacks
const authorInfo = {
name: gitAuthorInfo?.name ?? 'Scaffolder',
email: gitAuthorInfo?.email ?? 'scaffolder@backstage.io',
};
const commitHash = await git.commit({
dir,
message: commitMessage,
author: authorInfo,
committer: authorInfo,
});
await git.push({
dir,
remote: 'origin',
remoteRef: remoteRef ?? `refs/heads/${branch}`,
});
return { commitHash };
}
type BranchProtectionOptions = {
client: Octokit;
owner: string;
repoName: string;
logger: Logger;
requireCodeOwnerReviews: boolean;
requiredStatusCheckContexts?: string[];
bypassPullRequestAllowances?: {
users?: string[];
teams?: string[];
apps?: string[];
};
requiredApprovingReviewCount?: number;
restrictions?: {
users: string[];
teams: string[];
apps?: string[];
};
requireBranchesToBeUpToDate?: boolean;
requiredConversationResolution?: boolean;
defaultBranch?: string;
enforceAdmins?: boolean;
dismissStaleReviews?: boolean;
requiredCommitSigning?: boolean;
};
export const enableBranchProtectionOnDefaultRepoBranch = async ({
repoName,
client,
owner,
logger,
requireCodeOwnerReviews,
bypassPullRequestAllowances,
requiredApprovingReviewCount,
restrictions,
requiredStatusCheckContexts = [],
requireBranchesToBeUpToDate = true,
requiredConversationResolution = false,
defaultBranch = 'master',
enforceAdmins = true,
dismissStaleReviews = false,
requiredCommitSigning = false,
}: BranchProtectionOptions): Promise<void> => {
const tryOnce = async () => {
try {
await client.rest.repos.updateBranchProtection({
mediaType: {
/**
* 👇 we need this preview because allowing a custom
* reviewer count on branch protection is a preview
* feature
*
* More here: https://docs.github.com/en/rest/overview/api-previews#require-multiple-approving-reviews
*/
previews: ['luke-cage-preview'],
},
owner,
repo: repoName,
branch: defaultBranch,
required_status_checks: {
strict: requireBranchesToBeUpToDate,
contexts: requiredStatusCheckContexts,
},
restrictions: restrictions ?? null,
enforce_admins: enforceAdmins,
required_pull_request_reviews: {
required_approving_review_count: requiredApprovingReviewCount,
require_code_owner_reviews: requireCodeOwnerReviews,
bypass_pull_request_allowances: bypassPullRequestAllowances,
dismiss_stale_reviews: dismissStaleReviews,
},
required_conversation_resolution: requiredConversationResolution,
});
if (requiredCommitSigning) {
await client.rest.repos.createCommitSignatureProtection({
owner,
repo: repoName,
branch: defaultBranch,
});
}
} catch (e) {
assertError(e);
if (
e.message.includes(
'Upgrade to GitHub Pro or make this repository public to enable this feature',
)
) {
logger.warn(
'Branch protection was not enabled as it requires GitHub Pro for private repositories',
);
} else {
throw e;
}
}
};
try {
await tryOnce();
} catch (e) {
if (!e.message.includes('Branch not found')) {
throw e;
}
// GitHub has eventual consistency. Fail silently, wait, and try again.
await new Promise(resolve => setTimeout(resolve, 600));
await tryOnce();
}
};
export function getGitCommitMessage(
gitCommitMessage: string | undefined,
config: Config,
): string | undefined {
return gitCommitMessage
? gitCommitMessage
: config.getOptionalString('scaffolder.defaultCommitMessage');
}
export function entityRefToName(name: string): string {
return name.replace(/^.*[:/]/g, '');
}
@@ -1,32 +0,0 @@
/*
* 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.
*/
export { createPublishAzureAction } from './azure';
export { createPublishBitbucketAction } from './bitbucket';
export { createPublishBitbucketCloudAction } from './bitbucketCloud';
export { createPublishBitbucketServerAction } from './bitbucketServer';
export { createPublishBitbucketServerPullRequestAction } from './bitbucketServerPullRequest';
export { createPublishGerritAction } from './gerrit';
export { createPublishGerritReviewAction } from './gerritReview';
export { createPublishGithubAction } from './github';
export { createPublishGithubPullRequestAction } from './githubPullRequest';
export type {
CreateGithubPullRequestClientFactoryInput,
CreateGithubPullRequestActionOptions,
OctokitWithPullRequestPluginClient,
} from './githubPullRequest';
export { createPublishGitlabAction } from './gitlab';
export { createPublishGitlabMergeRequestAction } from './gitlabMergeRequest';
@@ -1,181 +0,0 @@
/*
* 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 path from 'path';
import { getRepoSourceDirectory, isExecutable, parseRepoUrl } from './util';
import { ScmIntegrations } from '@backstage/integration';
import { ConfigReader } from '@backstage/config';
describe('getRepoSourceDirectory', () => {
it('should return workspace root if no sub folder is given', () => {
expect(
getRepoSourceDirectory(path.join('/', 'var', 'workspace'), undefined),
).toEqual(path.join('/', 'var', 'workspace'));
});
it('should return path in workspace if sub folder is given', () => {
expect(
getRepoSourceDirectory(
path.join('/', 'var', 'workspace'),
path.join('path', 'of', 'subfolder'),
),
).toEqual(path.join('/', 'var', 'workspace', 'path', 'of', 'subfolder'));
});
it('should not allow traversal outside the workspace root', () => {
// We have to construct the path manually here, as path.join would mitigate the path traversal
expect(
getRepoSourceDirectory(
path.join('/', 'var', 'workspace'),
`..${path.sep}secret`,
),
).toEqual(path.join('/', 'var', 'workspace', 'secret'));
expect(
getRepoSourceDirectory(
path.join('/', 'var', 'workspace'),
`.${path.sep}path${path.sep}..${path.sep}..${path.sep}secret`,
),
).toEqual(path.join('/', 'var', 'workspace', 'secret'));
expect(
getRepoSourceDirectory(
path.join('/', 'var', 'workspace'),
path.join('/', 'absolute', 'secret'),
),
).toEqual(path.join('/', 'var', 'workspace', 'absolute', 'secret'));
});
});
describe('isExecutable', () => {
it('should return true for file mode 777', () => {
expect(isExecutable(0o100777)).toBe(true);
});
it('should return true for file mode 775', () => {
expect(isExecutable(0o100775)).toBe(true);
});
it('should return true for file mode 755', () => {
expect(isExecutable(0o100755)).toBe(true);
});
it('should return true for file mode 700', () => {
expect(isExecutable(0o100700)).toBe(true);
});
it('should return true for file mode 770', () => {
expect(isExecutable(0o100770)).toBe(true);
});
it('should return true for file mode 670', () => {
expect(isExecutable(0o100670)).toBe(true);
});
it('should return false for file mode 644', () => {
expect(isExecutable(0o100644)).toBe(false);
});
it('should return false for file mode 600', () => {
expect(isExecutable(0o100600)).toBe(false);
});
it('should return false for file mode 640', () => {
expect(isExecutable(0o100640)).toBe(false);
});
});
describe('parseRepoUrl', () => {
const config = new ConfigReader({
integrations: {
gitlab: [
{
host: 'www.gitlab.com',
token: 'token',
apiBaseUrl: 'https://api.gitlab.com',
},
],
bitbucket: [
{
host: 'www.bitbucket.net',
apiBaseUrl: 'https://api.hosted.bitbucket.net',
},
{
host: 'www.bitbucket.org',
username: 'username',
appPassword: 'password',
apiBaseUrl: 'https://api.hosted.bitbucket.org',
},
],
},
});
const integrations = ScmIntegrations.fromConfig(config);
it('should throw an exception if no parameters are passed (gitlab)', () => {
expect(() => parseRepoUrl('www.gitlab.com', integrations)).toThrow(
'Invalid repo URL passed to publisher: https://www.gitlab.com/, missing owner',
);
});
it('should return the project ID in the `project` parameter (gitlab)', () => {
expect(
parseRepoUrl('www.gitlab.com?project=234', integrations),
).toStrictEqual({
host: 'www.gitlab.com',
organization: undefined,
owner: undefined,
project: '234',
repo: null,
workspace: undefined,
});
});
it('should throw an exception if the repo parameter is missing, but owner parameter is set', () => {
expect(() =>
parseRepoUrl('www.gitlab.com?owner=owner', integrations),
).toThrow(
'Invalid repo URL passed to publisher: https://www.gitlab.com/?owner=owner, missing repo',
);
});
it('should throw an exception if the owner parameter is missing, but repo parameter is set (gitlab)', () => {
expect(() =>
parseRepoUrl('www.gitlab.com?repo=repo', integrations),
).toThrow(
'Invalid repo URL passed to publisher: https://www.gitlab.com/?repo=repo, missing owner',
);
});
it('should return the workspace, project and repo (bitbucket.org)', () => {
expect(
parseRepoUrl(
'www.bitbucket.org?project=project&workspace=workspace&repo=repo',
integrations,
),
).toStrictEqual({
host: 'www.bitbucket.org',
organization: undefined,
owner: undefined,
project: 'project',
repo: 'repo',
workspace: 'workspace',
});
});
it('should return the project and repo (another bitbucket instance)', () => {
expect(
parseRepoUrl('www.bitbucket.net?project=project&repo=repo', integrations),
).toStrictEqual({
host: 'www.bitbucket.net',
organization: undefined,
owner: undefined,
project: 'project',
repo: 'repo',
workspace: undefined,
});
});
it('should throw an exception if the workspace parameter is missing for bitbucket.org instance (bitbucket.org)', () => {
expect(() =>
parseRepoUrl('www.bitbucket.org?project=project&repo=repo', integrations),
).toThrow(
'Invalid repo URL passed to publisher: https://www.bitbucket.org/?project=project&repo=repo, missing workspace',
);
});
});
+3
View File
@@ -50,12 +50,15 @@
"@backstage/plugin-scaffolder-common": "workspace:^",
"@backstage/types": "workspace:^",
"fs-extra": "10.1.0",
"globby": "^11.0.0",
"jsonschema": "^1.2.6",
"p-limit": "^3.1.0",
"winston": "^3.2.1",
"zod": "^3.21.4",
"zod-to-json-schema": "^3.20.4"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/config": "workspace:^"
},