Merge pull request #5381 from msamad/msamad/add-repourlpicker-for-bitbucketcloud
Add repourlpicker and publisher for bitbucket cloud
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
apiVersion: backstage.io/v1beta2
|
||||
kind: Template
|
||||
metadata:
|
||||
name: bitbucket-demo
|
||||
title: Test Bitbucket RepoUrlPicker template
|
||||
description: scaffolder v1beta2 template demo publishing to bitbucket
|
||||
spec:
|
||||
owner: backstage/techdocs-core
|
||||
type: service
|
||||
|
||||
parameters:
|
||||
- title: Choose a location
|
||||
required:
|
||||
- repoUrl
|
||||
properties:
|
||||
repoUrl:
|
||||
title: Repository Location
|
||||
type: string
|
||||
ui:field: RepoUrlPicker
|
||||
ui:options:
|
||||
allowedHosts:
|
||||
- bitbucket.org
|
||||
- server.bitbucket.com
|
||||
- title: Fill in some steps
|
||||
required:
|
||||
- name
|
||||
- owner
|
||||
properties:
|
||||
name:
|
||||
title: Name
|
||||
type: string
|
||||
description: Unique name of the component
|
||||
ui:autofocus: true
|
||||
ui:options:
|
||||
rows: 5
|
||||
owner:
|
||||
title: Owner
|
||||
type: string
|
||||
description: Owner of the component
|
||||
ui:field: OwnerPicker
|
||||
ui:options:
|
||||
allowedKinds:
|
||||
- Group
|
||||
|
||||
steps:
|
||||
- id: fetch-base
|
||||
name: Fetch Base
|
||||
action: fetch:cookiecutter
|
||||
input:
|
||||
url: ./template
|
||||
values:
|
||||
name: '{{ parameters.name }}'
|
||||
owner: '{{ parameters.owner }}'
|
||||
|
||||
- id: fetch-docs
|
||||
name: Fetch Docs
|
||||
action: fetch:plain
|
||||
input:
|
||||
targetPath: ./community
|
||||
url: https://github.com/backstage/community/tree/main/backstage-community-sessions
|
||||
|
||||
- id: publish
|
||||
name: Publish
|
||||
action: publish:bitbucket
|
||||
input:
|
||||
description: 'This is {{ parameters.name }}'
|
||||
repoUrl: '{{ parameters.repoUrl }}'
|
||||
|
||||
- id: register
|
||||
name: Register
|
||||
action: catalog:register
|
||||
input:
|
||||
repoContentsUrl: '{{ steps.publish.output.repoContentsUrl }}'
|
||||
catalogInfoPath: '/catalog-info.yaml'
|
||||
|
||||
output:
|
||||
remoteUrl: '{{ steps.publish.output.remoteUrl }}'
|
||||
entityRef: '{{ steps.register.output.entityRef }}'
|
||||
@@ -0,0 +1,8 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: {{cookiecutter.name | jsonify}}
|
||||
spec:
|
||||
type: website
|
||||
lifecycle: experimental
|
||||
owner: {{cookiecutter.owner | jsonify}}
|
||||
+7
-1
@@ -69,7 +69,13 @@ export function createGithubActionsDispatchAction(options: {
|
||||
async handler(ctx) {
|
||||
const { repoUrl, workflowId, branchOrTagName } = ctx.input;
|
||||
|
||||
const { owner, repo, host } = parseRepoUrl(repoUrl);
|
||||
const { owner, repo, host } = parseRepoUrl(repoUrl, integrations);
|
||||
|
||||
if (!owner) {
|
||||
throw new InputError(
|
||||
`No owner provided for host: ${host}, and repo ${repo}`,
|
||||
);
|
||||
}
|
||||
|
||||
ctx.logger.info(
|
||||
`Dispatching workflow ${workflowId} for repo ${repoUrl} on ${branchOrTagName}`,
|
||||
|
||||
@@ -68,21 +68,21 @@ describe('publish:azure', () => {
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: { repoUrl: 'azure.com?repo=bob' },
|
||||
input: { repoUrl: 'dev.azure.com?repo=bob' },
|
||||
}),
|
||||
).rejects.toThrow(/missing owner/);
|
||||
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: { repoUrl: 'azure.com?owner=owner' },
|
||||
input: { repoUrl: 'dev.azure.com?owner=owner' },
|
||||
}),
|
||||
).rejects.toThrow(/missing repo/);
|
||||
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: { repoUrl: 'azure.com?owner=owner&repo=repo' },
|
||||
input: { repoUrl: 'dev.azure.com?owner=owner&repo=repo' },
|
||||
}),
|
||||
).rejects.toThrow(/missing organization/);
|
||||
});
|
||||
|
||||
@@ -80,7 +80,10 @@ export function createPublishAzureAction(options: {
|
||||
async handler(ctx) {
|
||||
const { repoUrl, defaultBranch = 'master' } = ctx.input;
|
||||
|
||||
const { owner, repo, host, organization } = parseRepoUrl(repoUrl);
|
||||
const { owner, repo, host, organization } = parseRepoUrl(
|
||||
repoUrl,
|
||||
integrations,
|
||||
);
|
||||
|
||||
if (!organization) {
|
||||
throw new InputError(
|
||||
|
||||
+60
-40
@@ -50,7 +50,7 @@ describe('publish:bitbucket', () => {
|
||||
const action = createPublishBitbucketAction({ integrations, config });
|
||||
const mockContext = {
|
||||
input: {
|
||||
repoUrl: 'bitbucket.org?repo=repo&owner=owner',
|
||||
repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo',
|
||||
repoVisibility: 'private',
|
||||
},
|
||||
workspacePath: 'lol',
|
||||
@@ -70,14 +70,21 @@ describe('publish:bitbucket', () => {
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: { repoUrl: 'bitbucket.com?repo=bob' },
|
||||
input: { repoUrl: 'bitbucket.org?project=project&repo=repo' },
|
||||
}),
|
||||
).rejects.toThrow(/missing owner/);
|
||||
).rejects.toThrow(/missing workspace/);
|
||||
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: { repoUrl: 'bitbucket.com?owner=owner' },
|
||||
input: { repoUrl: 'bitbucket.org?workspace=workspace&repo=repo' },
|
||||
}),
|
||||
).rejects.toThrow(/missing project/);
|
||||
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: { repoUrl: 'bitbucket.org?workspace=workspace&project=project' },
|
||||
}),
|
||||
).rejects.toThrow(/missing repo/);
|
||||
});
|
||||
@@ -86,7 +93,9 @@ describe('publish:bitbucket', () => {
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: { repoUrl: 'missing.com?repo=bob&owner=owner' },
|
||||
input: {
|
||||
repoUrl: 'missing.com?workspace=workspace&project=project&repo=repo',
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(/No matching integration configuration/);
|
||||
});
|
||||
@@ -96,7 +105,8 @@ describe('publish:bitbucket', () => {
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: {
|
||||
repoUrl: 'notoken.bitbucket.com?repo=bob&owner=owner',
|
||||
repoUrl:
|
||||
'notoken.bitbucket.com?workspace=workspace&project=project&repo=repo',
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(/Authorization has not been provided for Bitbucket/);
|
||||
@@ -106,22 +116,26 @@ describe('publish:bitbucket', () => {
|
||||
expect.assertions(2);
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/owner/repo',
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('Authorization')).toBe('Bearer tokenlols');
|
||||
expect(req.body).toEqual({ is_private: true, scm: 'git' });
|
||||
expect(req.body).toEqual({
|
||||
is_private: true,
|
||||
scm: 'git',
|
||||
project: { key: 'project' },
|
||||
});
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/owner/repo',
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/owner/repo',
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -131,14 +145,20 @@ describe('publish:bitbucket', () => {
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler(mockContext);
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
input: {
|
||||
...mockContext.input,
|
||||
repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should call the correct APIs when the host is hosted bitbucket', async () => {
|
||||
expect.assertions(2);
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://hosted.bitbucket.com/rest/api/1.0/projects/owner/repos',
|
||||
'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('Authorization')).toBe('Bearer thing');
|
||||
expect(req.body).toEqual({ public: false, name: 'repo' });
|
||||
@@ -169,7 +189,7 @@ describe('publish:bitbucket', () => {
|
||||
...mockContext,
|
||||
input: {
|
||||
...mockContext.input,
|
||||
repoUrl: 'hosted.bitbucket.com?owner=owner&repo=repo',
|
||||
repoUrl: 'hosted.bitbucket.com?project=project&repo=repo',
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -195,7 +215,7 @@ describe('publish:bitbucket', () => {
|
||||
expect.assertions(1);
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://hosted.bitbucket.com/rest/api/1.0/projects/owner/repos',
|
||||
'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos',
|
||||
(_, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(201),
|
||||
@@ -205,7 +225,7 @@ describe('publish:bitbucket', () => {
|
||||
},
|
||||
),
|
||||
rest.put(
|
||||
'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/owner/repos/repo/enabled',
|
||||
'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('Authorization')).toBe('Bearer thing');
|
||||
return res(ctx.status(204));
|
||||
@@ -217,7 +237,7 @@ describe('publish:bitbucket', () => {
|
||||
...mockContext,
|
||||
input: {
|
||||
...mockContext.input,
|
||||
repoUrl: 'hosted.bitbucket.com?owner=owner&repo=repo',
|
||||
repoUrl: 'hosted.bitbucket.com?project=project&repo=repo',
|
||||
enableLFS: true,
|
||||
},
|
||||
});
|
||||
@@ -226,7 +246,7 @@ describe('publish:bitbucket', () => {
|
||||
it('should report an error if enabling LFS fails', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://hosted.bitbucket.com/rest/api/1.0/projects/owner/repos',
|
||||
'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos',
|
||||
(_, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(201),
|
||||
@@ -236,7 +256,7 @@ describe('publish:bitbucket', () => {
|
||||
},
|
||||
),
|
||||
rest.put(
|
||||
'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/owner/repos/repo/enabled',
|
||||
'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled',
|
||||
(_, res, ctx) => {
|
||||
return res(ctx.status(500));
|
||||
},
|
||||
@@ -248,7 +268,7 @@ describe('publish:bitbucket', () => {
|
||||
...mockContext,
|
||||
input: {
|
||||
...mockContext.input,
|
||||
repoUrl: 'hosted.bitbucket.com?owner=owner&repo=repo',
|
||||
repoUrl: 'hosted.bitbucket.com?project=project&repo=repo',
|
||||
enableLFS: true,
|
||||
},
|
||||
}),
|
||||
@@ -259,7 +279,7 @@ describe('publish:bitbucket', () => {
|
||||
it('should call initAndPush with the correct values', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/owner/repo',
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
@@ -267,12 +287,12 @@ describe('publish:bitbucket', () => {
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/owner/repo',
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/owner/cloneurl',
|
||||
href: 'https://bitbucket.org/workspace/cloneurl',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -285,7 +305,7 @@ describe('publish:bitbucket', () => {
|
||||
|
||||
expect(initRepoAndPush).toHaveBeenCalledWith({
|
||||
dir: mockContext.workspacePath,
|
||||
remoteUrl: 'https://bitbucket.org/owner/cloneurl',
|
||||
remoteUrl: 'https://bitbucket.org/workspace/cloneurl',
|
||||
defaultBranch: 'master',
|
||||
auth: { username: 'x-token-auth', password: 'tokenlols' },
|
||||
logger: mockContext.logger,
|
||||
@@ -296,7 +316,7 @@ describe('publish:bitbucket', () => {
|
||||
it('should call initAndPush with the correct default branch', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/owner/repo',
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
@@ -304,12 +324,12 @@ describe('publish:bitbucket', () => {
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/owner/repo',
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/owner/cloneurl',
|
||||
href: 'https://bitbucket.org/workspace/cloneurl',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -328,7 +348,7 @@ describe('publish:bitbucket', () => {
|
||||
|
||||
expect(initRepoAndPush).toHaveBeenCalledWith({
|
||||
dir: mockContext.workspacePath,
|
||||
remoteUrl: 'https://bitbucket.org/owner/cloneurl',
|
||||
remoteUrl: 'https://bitbucket.org/workspace/cloneurl',
|
||||
defaultBranch: 'main',
|
||||
auth: { username: 'x-token-auth', password: 'tokenlols' },
|
||||
logger: mockContext.logger,
|
||||
@@ -371,7 +391,7 @@ describe('publish:bitbucket', () => {
|
||||
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/owner/repo',
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
@@ -379,12 +399,12 @@ describe('publish:bitbucket', () => {
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/owner/repo',
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/owner/cloneurl',
|
||||
href: 'https://bitbucket.org/workspace/cloneurl',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -397,7 +417,7 @@ describe('publish:bitbucket', () => {
|
||||
|
||||
expect(initRepoAndPush).toHaveBeenCalledWith({
|
||||
dir: mockContext.workspacePath,
|
||||
remoteUrl: 'https://bitbucket.org/owner/cloneurl',
|
||||
remoteUrl: 'https://bitbucket.org/workspace/cloneurl',
|
||||
auth: { username: 'x-token-auth', password: 'tokenlols' },
|
||||
logger: mockContext.logger,
|
||||
defaultBranch: 'master',
|
||||
@@ -437,7 +457,7 @@ describe('publish:bitbucket', () => {
|
||||
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/owner/repo',
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
@@ -445,12 +465,12 @@ describe('publish:bitbucket', () => {
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/owner/repo',
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/owner/cloneurl',
|
||||
href: 'https://bitbucket.org/workspace/cloneurl',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -463,7 +483,7 @@ describe('publish:bitbucket', () => {
|
||||
|
||||
expect(initRepoAndPush).toHaveBeenCalledWith({
|
||||
dir: mockContext.workspacePath,
|
||||
remoteUrl: 'https://bitbucket.org/owner/cloneurl',
|
||||
remoteUrl: 'https://bitbucket.org/workspace/cloneurl',
|
||||
auth: { username: 'x-token-auth', password: 'tokenlols' },
|
||||
logger: mockContext.logger,
|
||||
defaultBranch: 'master',
|
||||
@@ -475,7 +495,7 @@ describe('publish:bitbucket', () => {
|
||||
it('should call outputs with the correct urls', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/owner/repo',
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
@@ -483,12 +503,12 @@ describe('publish:bitbucket', () => {
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/owner/repo',
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/owner/cloneurl',
|
||||
href: 'https://bitbucket.org/workspace/cloneurl',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -501,11 +521,11 @@ describe('publish:bitbucket', () => {
|
||||
|
||||
expect(mockContext.output).toHaveBeenCalledWith(
|
||||
'remoteUrl',
|
||||
'https://bitbucket.org/owner/cloneurl',
|
||||
'https://bitbucket.org/workspace/cloneurl',
|
||||
);
|
||||
expect(mockContext.output).toHaveBeenCalledWith(
|
||||
'repoContentsUrl',
|
||||
'https://bitbucket.org/owner/repo/src/master',
|
||||
'https://bitbucket.org/workspace/repo/src/master',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,13 +26,21 @@ import { getRepoSourceDirectory, parseRepoUrl } from './util';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
const createBitbucketCloudRepository = async (opts: {
|
||||
owner: string;
|
||||
workspace: string;
|
||||
project: string;
|
||||
repo: string;
|
||||
description: string;
|
||||
repoVisibility: 'private' | 'public';
|
||||
authorization: string;
|
||||
}) => {
|
||||
const { owner, repo, description, repoVisibility, authorization } = opts;
|
||||
const {
|
||||
workspace,
|
||||
project,
|
||||
repo,
|
||||
description,
|
||||
repoVisibility,
|
||||
authorization,
|
||||
} = opts;
|
||||
|
||||
const options: RequestInit = {
|
||||
method: 'POST',
|
||||
@@ -40,6 +48,7 @@ const createBitbucketCloudRepository = async (opts: {
|
||||
scm: 'git',
|
||||
description: description,
|
||||
is_private: repoVisibility === 'private',
|
||||
project: { key: project },
|
||||
}),
|
||||
headers: {
|
||||
Authorization: authorization,
|
||||
@@ -50,7 +59,7 @@ const createBitbucketCloudRepository = async (opts: {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(
|
||||
`https://api.bitbucket.org/2.0/repositories/${owner}/${repo}`,
|
||||
`https://api.bitbucket.org/2.0/repositories/${workspace}/${repo}`,
|
||||
options,
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -80,7 +89,7 @@ const createBitbucketCloudRepository = async (opts: {
|
||||
|
||||
const createBitbucketServerRepository = async (opts: {
|
||||
host: string;
|
||||
owner: string;
|
||||
project: string;
|
||||
repo: string;
|
||||
description: string;
|
||||
repoVisibility: 'private' | 'public';
|
||||
@@ -89,7 +98,7 @@ const createBitbucketServerRepository = async (opts: {
|
||||
}) => {
|
||||
const {
|
||||
host,
|
||||
owner,
|
||||
project,
|
||||
repo,
|
||||
description,
|
||||
authorization,
|
||||
@@ -113,7 +122,7 @@ const createBitbucketServerRepository = async (opts: {
|
||||
|
||||
try {
|
||||
const baseUrl = apiBaseUrl ? apiBaseUrl : `https://${host}/rest/api/1.0`;
|
||||
response = await fetch(`${baseUrl}/projects/${owner}/repos`, options);
|
||||
response = await fetch(`${baseUrl}/projects/${project}/repos`, options);
|
||||
} catch (e) {
|
||||
throw new Error(`Unable to create repository, ${e}`);
|
||||
}
|
||||
@@ -160,10 +169,10 @@ const getAuthorizationHeader = (config: BitbucketIntegrationConfig) => {
|
||||
const performEnableLFS = async (opts: {
|
||||
authorization: string;
|
||||
host: string;
|
||||
owner: string;
|
||||
project: string;
|
||||
repo: string;
|
||||
}) => {
|
||||
const { authorization, host, owner, repo } = opts;
|
||||
const { authorization, host, project, repo } = opts;
|
||||
|
||||
const options: RequestInit = {
|
||||
method: 'PUT',
|
||||
@@ -173,7 +182,7 @@ const performEnableLFS = async (opts: {
|
||||
};
|
||||
|
||||
const { ok, status, statusText } = await fetch(
|
||||
`https://${host}/rest/git-lfs/admin/projects/${owner}/repos/${repo}/enabled`,
|
||||
`https://${host}/rest/git-lfs/admin/projects/${project}/repos/${repo}/enabled`,
|
||||
options,
|
||||
);
|
||||
|
||||
@@ -258,7 +267,26 @@ export function createPublishBitbucketAction(options: {
|
||||
enableLFS = false,
|
||||
} = ctx.input;
|
||||
|
||||
const { owner, repo, host } = parseRepoUrl(repoUrl);
|
||||
const { workspace, project, repo, host } = parseRepoUrl(
|
||||
repoUrl,
|
||||
integrations,
|
||||
);
|
||||
|
||||
// Workspace is only required for bitbucket cloud
|
||||
if (host === 'bitbucket.org') {
|
||||
if (!workspace) {
|
||||
throw new InputError(
|
||||
`Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing workspace`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Project is required for both bitbucket cloud and bitbucket server
|
||||
if (!project) {
|
||||
throw new InputError(
|
||||
`Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing project`,
|
||||
);
|
||||
}
|
||||
|
||||
const integrationConfig = integrations.bitbucket.byHost(host);
|
||||
|
||||
@@ -279,7 +307,8 @@ export function createPublishBitbucketAction(options: {
|
||||
const { remoteUrl, repoContentsUrl } = await createMethod({
|
||||
authorization,
|
||||
host,
|
||||
owner,
|
||||
workspace: workspace || '',
|
||||
project,
|
||||
repo,
|
||||
repoVisibility,
|
||||
description,
|
||||
@@ -311,7 +340,7 @@ export function createPublishBitbucketAction(options: {
|
||||
});
|
||||
|
||||
if (enableLFS && host !== 'bitbucket.org') {
|
||||
await performEnableLFS({ authorization, host, owner, repo });
|
||||
await performEnableLFS({ authorization, host, project, repo });
|
||||
}
|
||||
|
||||
ctx.output('remoteUrl', remoteUrl);
|
||||
|
||||
@@ -144,7 +144,13 @@ export function createPublishGithubAction(options: {
|
||||
topics,
|
||||
} = ctx.input;
|
||||
|
||||
const { owner, repo, host } = parseRepoUrl(repoUrl);
|
||||
const { owner, repo, host } = parseRepoUrl(repoUrl, integrations);
|
||||
|
||||
if (!owner) {
|
||||
throw new InputError(
|
||||
`No owner provided for host: ${host}, and repo ${repo}`,
|
||||
);
|
||||
}
|
||||
|
||||
const credentialsProvider = credentialsProviders.get(host);
|
||||
const integrationConfig = integrations.github.byHost(host);
|
||||
|
||||
+7
-1
@@ -173,7 +173,13 @@ export const createPublishGithubPullRequestAction = ({
|
||||
sourcePath,
|
||||
} = ctx.input;
|
||||
|
||||
const { owner, repo, host } = parseRepoUrl(repoUrl);
|
||||
const { owner, repo, host } = parseRepoUrl(repoUrl, integrations);
|
||||
|
||||
if (!owner) {
|
||||
throw new InputError(
|
||||
`No owner provided for host: ${host}, and repo ${repo}`,
|
||||
);
|
||||
}
|
||||
|
||||
const client = await clientFactory({ integrations, host, owner, repo });
|
||||
const fileRoot = sourcePath
|
||||
|
||||
@@ -84,7 +84,13 @@ export function createPublishGitlabAction(options: {
|
||||
defaultBranch = 'master',
|
||||
} = ctx.input;
|
||||
|
||||
const { owner, repo, host } = parseRepoUrl(repoUrl);
|
||||
const { owner, repo, host } = parseRepoUrl(repoUrl, integrations);
|
||||
|
||||
if (!owner) {
|
||||
throw new InputError(
|
||||
`No owner provided for host: ${host}, and repo ${repo}`,
|
||||
);
|
||||
}
|
||||
|
||||
const integrationConfig = integrations.gitlab.byHost(host);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { join as joinPath, normalize as normalizePath } from 'path';
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
|
||||
export const getRepoSourceDirectory = (
|
||||
workspacePath: string,
|
||||
@@ -33,11 +34,16 @@ export const getRepoSourceDirectory = (
|
||||
export type RepoSpec = {
|
||||
repo: string;
|
||||
host: string;
|
||||
owner: string;
|
||||
owner?: string;
|
||||
organization?: string;
|
||||
workspace?: string;
|
||||
project?: string;
|
||||
};
|
||||
|
||||
export const parseRepoUrl = (repoUrl: string): RepoSpec => {
|
||||
export const parseRepoUrl = (
|
||||
repoUrl: string,
|
||||
integrations: ScmIntegrationRegistry,
|
||||
): RepoSpec => {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(`https://${repoUrl}`);
|
||||
@@ -47,13 +53,40 @@ export const parseRepoUrl = (repoUrl: string): RepoSpec => {
|
||||
);
|
||||
}
|
||||
const host = parsed.host;
|
||||
const owner = parsed.searchParams.get('owner');
|
||||
const owner = parsed.searchParams.get('owner') ?? undefined;
|
||||
const organization = parsed.searchParams.get('organization') ?? undefined;
|
||||
const workspace = parsed.searchParams.get('workspace') ?? undefined;
|
||||
const project = parsed.searchParams.get('project') ?? undefined;
|
||||
|
||||
if (!owner) {
|
||||
const type = integrations.byHost(host)?.type;
|
||||
|
||||
if (!type) {
|
||||
throw new InputError(
|
||||
`Invalid repo URL passed to publisher: ${repoUrl}, missing owner`,
|
||||
`No matching integration configuration for host ${host}, please check your integrations config`,
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'bitbucket') {
|
||||
if (host === 'bitbucket.org') {
|
||||
if (!workspace) {
|
||||
throw new InputError(
|
||||
`Invalid repo URL passed to publisher: ${repoUrl}, missing workspace`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!project) {
|
||||
throw new InputError(
|
||||
`Invalid repo URL passed to publisher: ${repoUrl}, missing project`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (!owner) {
|
||||
throw new InputError(
|
||||
`Invalid repo URL passed to publisher: ${repoUrl}, missing owner`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const repo = parsed.searchParams.get('repo');
|
||||
if (!repo) {
|
||||
throw new InputError(
|
||||
@@ -61,7 +94,5 @@ export const parseRepoUrl = (repoUrl: string): RepoSpec => {
|
||||
);
|
||||
}
|
||||
|
||||
const organization = parsed.searchParams.get('organization') ?? undefined;
|
||||
|
||||
return { host, owner, repo, organization };
|
||||
return { host, owner, repo, organization, workspace, project };
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ import { RepoSpec } from '../actions/builtin/publish/util';
|
||||
import { DatabaseTaskStore } from './DatabaseTaskStore';
|
||||
import { StorageTaskBroker } from './StorageTaskBroker';
|
||||
import { TaskWorker } from './TaskWorker';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
|
||||
async function createStore(): Promise<DatabaseTaskStore> {
|
||||
const manager = DatabaseManager.fromConfig(
|
||||
@@ -41,6 +42,14 @@ describe('TaskWorker', () => {
|
||||
let storage: DatabaseTaskStore;
|
||||
let actionRegistry = new TemplateActionRegistry();
|
||||
|
||||
const integrations = ScmIntegrations.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
github: [{ host: 'github.com', token: 'token' }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
storage = await createStore();
|
||||
});
|
||||
@@ -65,6 +74,7 @@ describe('TaskWorker', () => {
|
||||
workingDirectory: os.tmpdir(),
|
||||
actionRegistry,
|
||||
taskBroker: broker,
|
||||
integrations,
|
||||
});
|
||||
const { taskId } = await broker.dispatch({
|
||||
steps: [{ id: 'test', name: 'test', action: 'not-found-action' }],
|
||||
@@ -90,6 +100,7 @@ describe('TaskWorker', () => {
|
||||
workingDirectory: os.tmpdir(),
|
||||
actionRegistry,
|
||||
taskBroker: broker,
|
||||
integrations,
|
||||
});
|
||||
|
||||
const { taskId } = await broker.dispatch({
|
||||
@@ -142,6 +153,7 @@ describe('TaskWorker', () => {
|
||||
workingDirectory: os.tmpdir(),
|
||||
actionRegistry,
|
||||
taskBroker: broker,
|
||||
integrations,
|
||||
});
|
||||
|
||||
const { taskId } = await broker.dispatch({
|
||||
@@ -177,6 +189,7 @@ describe('TaskWorker', () => {
|
||||
workingDirectory: os.tmpdir(),
|
||||
actionRegistry,
|
||||
taskBroker: broker,
|
||||
integrations,
|
||||
});
|
||||
|
||||
const { taskId } = await broker.dispatch({
|
||||
@@ -210,6 +223,7 @@ describe('TaskWorker', () => {
|
||||
workingDirectory: os.tmpdir(),
|
||||
actionRegistry,
|
||||
taskBroker: broker,
|
||||
integrations,
|
||||
});
|
||||
|
||||
const { taskId } = await broker.dispatch({
|
||||
@@ -243,6 +257,7 @@ describe('TaskWorker', () => {
|
||||
workingDirectory: os.tmpdir(),
|
||||
actionRegistry,
|
||||
taskBroker: broker,
|
||||
integrations,
|
||||
});
|
||||
|
||||
const { taskId } = await broker.dispatch({
|
||||
@@ -331,6 +346,7 @@ describe('TaskWorker', () => {
|
||||
workingDirectory: os.tmpdir(),
|
||||
actionRegistry,
|
||||
taskBroker: broker,
|
||||
integrations,
|
||||
});
|
||||
|
||||
const { taskId } = await broker.dispatch({
|
||||
@@ -388,6 +404,12 @@ describe('TaskWorker', () => {
|
||||
organization: {
|
||||
type: 'string',
|
||||
},
|
||||
workspace: {
|
||||
type: 'string',
|
||||
},
|
||||
project: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -396,7 +418,10 @@ describe('TaskWorker', () => {
|
||||
async handler(ctx) {
|
||||
ctx.output('host', ctx.input.destination.host);
|
||||
ctx.output('repo', ctx.input.destination.repo);
|
||||
ctx.output('owner', ctx.input.destination.owner);
|
||||
|
||||
if (ctx.input.destination.owner) {
|
||||
ctx.output('owner', ctx.input.destination.owner);
|
||||
}
|
||||
|
||||
if (ctx.input.destination.host !== 'github.com') {
|
||||
throw new Error(
|
||||
@@ -410,7 +435,10 @@ describe('TaskWorker', () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (ctx.input.destination.owner !== 'owner') {
|
||||
if (
|
||||
ctx.input.destination.owner &&
|
||||
ctx.input.destination.owner !== 'owner'
|
||||
) {
|
||||
throw new Error(
|
||||
`expected repo to be "owner" got ${ctx.input.destination.owner}`,
|
||||
);
|
||||
@@ -425,6 +453,7 @@ describe('TaskWorker', () => {
|
||||
workingDirectory: os.tmpdir(),
|
||||
actionRegistry,
|
||||
taskBroker: broker,
|
||||
integrations,
|
||||
});
|
||||
|
||||
const { taskId } = await broker.dispatch({
|
||||
|
||||
@@ -27,12 +27,14 @@ import { parseRepoUrl } from '../actions/builtin/publish/util';
|
||||
import { TemplateActionRegistry } from '../actions/TemplateActionRegistry';
|
||||
import { isTruthy } from './helper';
|
||||
import { Task, TaskBroker } from './types';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
|
||||
type Options = {
|
||||
logger: Logger;
|
||||
taskBroker: TaskBroker;
|
||||
workingDirectory: string;
|
||||
actionRegistry: TemplateActionRegistry;
|
||||
integrations: ScmIntegrations;
|
||||
};
|
||||
|
||||
export class TaskWorker {
|
||||
@@ -45,11 +47,11 @@ export class TaskWorker {
|
||||
// scary right now, so we're going to lock it off like the component API is
|
||||
// in the frontend until we can work out a nice way to do it.
|
||||
this.handlebars.registerHelper('parseRepoUrl', repoUrl => {
|
||||
return JSON.stringify(parseRepoUrl(repoUrl));
|
||||
return JSON.stringify(parseRepoUrl(repoUrl, options.integrations));
|
||||
});
|
||||
|
||||
this.handlebars.registerHelper('projectSlug', repoUrl => {
|
||||
const { owner, repo } = parseRepoUrl(repoUrl);
|
||||
const { owner, repo } = parseRepoUrl(repoUrl, options.integrations);
|
||||
return `${owner}/${repo}`;
|
||||
});
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ export async function createRouter(
|
||||
taskBroker,
|
||||
actionRegistry,
|
||||
workingDirectory,
|
||||
integrations,
|
||||
});
|
||||
workers.push(worker);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { FieldProps } from '@rjsf/core';
|
||||
import { scaffolderApiRef } from '../../../api';
|
||||
import { scmIntegrationsApiRef } from '@backstage/integration-react';
|
||||
import { useAsync } from 'react-use';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import InputLabel from '@material-ui/core/InputLabel';
|
||||
@@ -31,6 +32,8 @@ function splitFormData(url: string | undefined) {
|
||||
let owner = undefined;
|
||||
let repo = undefined;
|
||||
let organization = undefined;
|
||||
let workspace = undefined;
|
||||
let project = undefined;
|
||||
|
||||
try {
|
||||
if (url) {
|
||||
@@ -40,12 +43,15 @@ function splitFormData(url: string | undefined) {
|
||||
repo = parsed.searchParams.get('repo') || undefined;
|
||||
// This is azure dev ops specific. not used for any other provider.
|
||||
organization = parsed.searchParams.get('organization') || undefined;
|
||||
// These are bitbucket specific, not used for any other provider.
|
||||
workspace = parsed.searchParams.get('workspace') || undefined;
|
||||
project = parsed.searchParams.get('project') || undefined;
|
||||
}
|
||||
} catch {
|
||||
/* ok */
|
||||
}
|
||||
|
||||
return { host, owner, repo, organization };
|
||||
return { host, owner, repo, organization, workspace, project };
|
||||
}
|
||||
|
||||
function serializeFormData(data: {
|
||||
@@ -53,10 +59,13 @@ function serializeFormData(data: {
|
||||
owner?: string;
|
||||
repo?: string;
|
||||
organization?: string;
|
||||
workspace?: string;
|
||||
project?: string;
|
||||
}) {
|
||||
if (!data.host) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (data.owner) {
|
||||
params.set('owner', data.owner);
|
||||
@@ -67,6 +76,12 @@ function serializeFormData(data: {
|
||||
if (data.organization) {
|
||||
params.set('organization', data.organization);
|
||||
}
|
||||
if (data.workspace) {
|
||||
params.set('workspace', data.workspace);
|
||||
}
|
||||
if (data.project) {
|
||||
params.set('project', data.project);
|
||||
}
|
||||
|
||||
return `${data.host}?${params.toString()}`;
|
||||
}
|
||||
@@ -77,25 +92,31 @@ export const RepoUrlPicker = ({
|
||||
rawErrors,
|
||||
formData,
|
||||
}: FieldProps<string>) => {
|
||||
const api = useApi(scaffolderApiRef);
|
||||
const scaffolderApi = useApi(scaffolderApiRef);
|
||||
const integrationApi = useApi(scmIntegrationsApiRef);
|
||||
const allowedHosts = uiSchema['ui:options']?.allowedHosts as string[];
|
||||
|
||||
const { value: integrations, loading } = useAsync(async () => {
|
||||
return await api.getIntegrationsList({ allowedHosts });
|
||||
return await scaffolderApi.getIntegrationsList({ allowedHosts });
|
||||
});
|
||||
|
||||
const { host, owner, repo, organization } = splitFormData(formData);
|
||||
const { host, owner, repo, organization, workspace, project } = splitFormData(
|
||||
formData,
|
||||
);
|
||||
const updateHost = useCallback(
|
||||
(evt: React.ChangeEvent<{ name?: string; value: unknown }>) =>
|
||||
(evt: React.ChangeEvent<{ name?: string; value: unknown }>) => {
|
||||
onChange(
|
||||
serializeFormData({
|
||||
host: evt.target.value as string,
|
||||
owner,
|
||||
repo,
|
||||
organization,
|
||||
workspace,
|
||||
project,
|
||||
}),
|
||||
),
|
||||
[onChange, owner, repo, organization],
|
||||
);
|
||||
},
|
||||
[onChange, owner, repo, organization, workspace, project],
|
||||
);
|
||||
|
||||
const updateOwner = useCallback(
|
||||
@@ -106,9 +127,11 @@ export const RepoUrlPicker = ({
|
||||
owner: evt.target.value as string,
|
||||
repo,
|
||||
organization,
|
||||
workspace,
|
||||
project,
|
||||
}),
|
||||
),
|
||||
[onChange, host, repo, organization],
|
||||
[onChange, host, repo, organization, workspace, project],
|
||||
);
|
||||
|
||||
const updateRepo = useCallback(
|
||||
@@ -119,9 +142,11 @@ export const RepoUrlPicker = ({
|
||||
owner,
|
||||
repo: evt.target.value as string,
|
||||
organization,
|
||||
workspace,
|
||||
project,
|
||||
}),
|
||||
),
|
||||
[onChange, host, owner, organization],
|
||||
[onChange, host, owner, organization, workspace, project],
|
||||
);
|
||||
|
||||
const updateOrganization = useCallback(
|
||||
@@ -132,9 +157,41 @@ export const RepoUrlPicker = ({
|
||||
owner,
|
||||
repo,
|
||||
organization: evt.target.value as string,
|
||||
workspace,
|
||||
project,
|
||||
}),
|
||||
),
|
||||
[onChange, host, owner, repo],
|
||||
[onChange, host, owner, repo, workspace, project],
|
||||
);
|
||||
|
||||
const updateWorkspace = useCallback(
|
||||
(evt: React.ChangeEvent<{ name?: string; value: unknown }>) =>
|
||||
onChange(
|
||||
serializeFormData({
|
||||
host,
|
||||
owner,
|
||||
repo,
|
||||
organization,
|
||||
workspace: evt.target.value as string,
|
||||
project,
|
||||
}),
|
||||
),
|
||||
[onChange, host, owner, repo, organization, project],
|
||||
);
|
||||
|
||||
const updateProject = useCallback(
|
||||
(evt: React.ChangeEvent<{ name?: string; value: unknown }>) =>
|
||||
onChange(
|
||||
serializeFormData({
|
||||
host,
|
||||
owner,
|
||||
repo,
|
||||
organization,
|
||||
workspace,
|
||||
project: evt.target.value as string,
|
||||
}),
|
||||
),
|
||||
[onChange, host, owner, repo, organization, workspace],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -145,10 +202,21 @@ export const RepoUrlPicker = ({
|
||||
owner,
|
||||
repo,
|
||||
organization,
|
||||
workspace,
|
||||
project,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [onChange, integrations, host, owner, repo, organization]);
|
||||
}, [
|
||||
onChange,
|
||||
integrations,
|
||||
host,
|
||||
owner,
|
||||
repo,
|
||||
organization,
|
||||
workspace,
|
||||
project,
|
||||
]);
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
@@ -179,6 +247,7 @@ export const RepoUrlPicker = ({
|
||||
The host where the repository will be created
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
{/* Show this for dev.azure.com only */}
|
||||
{host === 'dev.azure.com' && (
|
||||
<FormControl
|
||||
margin="normal"
|
||||
@@ -194,17 +263,60 @@ export const RepoUrlPicker = ({
|
||||
<FormHelperText>The name of the organization</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !owner}
|
||||
>
|
||||
<InputLabel htmlFor="ownerInput">Owner</InputLabel>
|
||||
<Input id="ownerInput" onChange={updateOwner} value={owner} />
|
||||
<FormHelperText>
|
||||
The organization, user or project that this repo will belong to
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
{host && integrationApi.byHost(host)?.type === 'bitbucket' && (
|
||||
<>
|
||||
{/* Show this for bitbucket.org only */}
|
||||
{host === 'bitbucket.org' && (
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !workspace}
|
||||
>
|
||||
<InputLabel htmlFor="wokrspaceInput">Workspace</InputLabel>
|
||||
<Input
|
||||
id="wokrspaceInput"
|
||||
onChange={updateWorkspace}
|
||||
value={workspace}
|
||||
/>
|
||||
<FormHelperText>
|
||||
The workspace where the repository will be created
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !project}
|
||||
>
|
||||
<InputLabel htmlFor="wokrspaceInput">Project</InputLabel>
|
||||
<Input
|
||||
id="wokrspaceInput"
|
||||
onChange={updateProject}
|
||||
value={project}
|
||||
/>
|
||||
<FormHelperText>
|
||||
The project where the repository will be created
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</>
|
||||
)}
|
||||
{/* Show this for all hosts except bitbucket */}
|
||||
{host && integrationApi.byHost(host)?.type !== 'bitbucket' && (
|
||||
<>
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !owner}
|
||||
>
|
||||
<InputLabel htmlFor="ownerInput">Owner</InputLabel>
|
||||
<Input id="ownerInput" onChange={updateOwner} value={owner} />
|
||||
<FormHelperText>
|
||||
The organization, user or project that this repo will belong to
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</>
|
||||
)}
|
||||
{/* Show this for all hosts */}
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
import { repoPickerValidation } from './validation';
|
||||
import { FieldValidation } from '@rjsf/core';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { ConfigReader } from '@backstage/core-app-api';
|
||||
import { ApiHolder } from '@backstage/core-plugin-api';
|
||||
|
||||
describe('RepoPicker Validation', () => {
|
||||
const fieldValidator = () =>
|
||||
@@ -23,30 +26,60 @@ describe('RepoPicker Validation', () => {
|
||||
addError: jest.fn(),
|
||||
} as unknown as FieldValidation);
|
||||
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
bitbucket: [
|
||||
{
|
||||
host: 'bitbucket.org',
|
||||
},
|
||||
{
|
||||
host: 'server.bitbucket.com',
|
||||
},
|
||||
],
|
||||
github: [
|
||||
{
|
||||
host: 'github.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const scmIntegrations = ScmIntegrations.fromConfig(config);
|
||||
|
||||
const apiHolderMock: jest.Mocked<ApiHolder> = {
|
||||
get: jest.fn().mockImplementation(() => {
|
||||
return scmIntegrations;
|
||||
}),
|
||||
};
|
||||
|
||||
it('validates when no repo', () => {
|
||||
const mockFieldValidation = fieldValidator();
|
||||
|
||||
repoPickerValidation('github.com?owner=a', mockFieldValidation);
|
||||
repoPickerValidation('github.com?owner=a', mockFieldValidation, {
|
||||
apiHolder: apiHolderMock,
|
||||
});
|
||||
|
||||
expect(mockFieldValidation.addError).toHaveBeenCalledWith(
|
||||
'Incomplete repository location provided',
|
||||
'Incomplete repository location provided, repo not provided',
|
||||
);
|
||||
});
|
||||
|
||||
it('validates when no owner', () => {
|
||||
const mockFieldValidation = fieldValidator();
|
||||
|
||||
repoPickerValidation('github.com?repo=a', mockFieldValidation);
|
||||
repoPickerValidation('github.com?repo=a', mockFieldValidation, {
|
||||
apiHolder: apiHolderMock,
|
||||
});
|
||||
|
||||
expect(mockFieldValidation.addError).toHaveBeenCalledWith(
|
||||
'Incomplete repository location provided',
|
||||
'Incomplete repository location provided, owner not provided',
|
||||
);
|
||||
});
|
||||
|
||||
it('validates when not a real url', () => {
|
||||
const mockFieldValidation = fieldValidator();
|
||||
|
||||
repoPickerValidation('', mockFieldValidation);
|
||||
repoPickerValidation('', mockFieldValidation, { apiHolder: apiHolderMock });
|
||||
|
||||
expect(mockFieldValidation.addError).toHaveBeenCalledWith(
|
||||
'Unable to parse the Repository URL',
|
||||
@@ -56,8 +89,116 @@ describe('RepoPicker Validation', () => {
|
||||
it('validates properly with proper input', () => {
|
||||
const mockFieldValidation = fieldValidator();
|
||||
|
||||
repoPickerValidation('github.com?owner=a&repo=b', mockFieldValidation);
|
||||
repoPickerValidation('github.com?owner=a&repo=b', mockFieldValidation, {
|
||||
apiHolder: apiHolderMock,
|
||||
});
|
||||
|
||||
expect(mockFieldValidation.addError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('validates when no workspace, project or repo provided for bitbucket cloud', () => {
|
||||
const mockFieldValidation = fieldValidator();
|
||||
|
||||
repoPickerValidation('bitbucket.org', mockFieldValidation, {
|
||||
apiHolder: apiHolderMock,
|
||||
});
|
||||
|
||||
expect(mockFieldValidation.addError).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'Incomplete repository location provided, workspace not provided',
|
||||
);
|
||||
expect(mockFieldValidation.addError).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'Incomplete repository location provided, project not provided',
|
||||
);
|
||||
expect(mockFieldValidation.addError).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'Incomplete repository location provided, repo not provided',
|
||||
);
|
||||
});
|
||||
|
||||
it('validates when no workspace provided for bitbucket cloud', () => {
|
||||
const mockFieldValidation = fieldValidator();
|
||||
|
||||
repoPickerValidation(
|
||||
'bitbucket.org?project=p&repo=r',
|
||||
mockFieldValidation,
|
||||
{ apiHolder: apiHolderMock },
|
||||
);
|
||||
|
||||
expect(mockFieldValidation.addError).toHaveBeenCalledWith(
|
||||
'Incomplete repository location provided, workspace not provided',
|
||||
);
|
||||
});
|
||||
|
||||
it('validates when no project provided for bitbucket cloud', () => {
|
||||
const mockFieldValidation = fieldValidator();
|
||||
|
||||
repoPickerValidation(
|
||||
'bitbucket.org?workspace=w&repo=r',
|
||||
mockFieldValidation,
|
||||
{ apiHolder: apiHolderMock },
|
||||
);
|
||||
|
||||
expect(mockFieldValidation.addError).toHaveBeenCalledWith(
|
||||
'Incomplete repository location provided, project not provided',
|
||||
);
|
||||
});
|
||||
|
||||
it('validates when no repo provided for bitbucket cloud', () => {
|
||||
const mockFieldValidation = fieldValidator();
|
||||
|
||||
repoPickerValidation(
|
||||
'bitbucket.org?workspace=w&project=p',
|
||||
mockFieldValidation,
|
||||
{ apiHolder: apiHolderMock },
|
||||
);
|
||||
|
||||
expect(mockFieldValidation.addError).toHaveBeenCalledWith(
|
||||
'Incomplete repository location provided, repo not provided',
|
||||
);
|
||||
});
|
||||
|
||||
it('validates when no project or repo provided for bitbucket server', () => {
|
||||
const mockFieldValidation = fieldValidator();
|
||||
|
||||
repoPickerValidation('server.bitbucket.com', mockFieldValidation, {
|
||||
apiHolder: apiHolderMock,
|
||||
});
|
||||
|
||||
expect(mockFieldValidation.addError).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'Incomplete repository location provided, project not provided',
|
||||
);
|
||||
expect(mockFieldValidation.addError).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'Incomplete repository location provided, repo not provided',
|
||||
);
|
||||
});
|
||||
|
||||
it('validates when no project provided for bitbucket server', () => {
|
||||
const mockFieldValidation = fieldValidator();
|
||||
|
||||
repoPickerValidation('server.bitbucket.com?repo=r', mockFieldValidation, {
|
||||
apiHolder: apiHolderMock,
|
||||
});
|
||||
|
||||
expect(mockFieldValidation.addError).toHaveBeenCalledWith(
|
||||
'Incomplete repository location provided, project not provided',
|
||||
);
|
||||
});
|
||||
|
||||
it('validates when no repo provided for bitbucket server', () => {
|
||||
const mockFieldValidation = fieldValidator();
|
||||
|
||||
repoPickerValidation(
|
||||
'server.bitbucket.com?project=p',
|
||||
mockFieldValidation,
|
||||
{ apiHolder: apiHolderMock },
|
||||
);
|
||||
|
||||
expect(mockFieldValidation.addError).toHaveBeenCalledWith(
|
||||
'Incomplete repository location provided, repo not provided',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,15 +15,53 @@
|
||||
*/
|
||||
|
||||
import { FieldValidation } from '@rjsf/core';
|
||||
import { ApiHolder } from '@backstage/core-plugin-api';
|
||||
import { scmIntegrationsApiRef } from '@backstage/integration-react';
|
||||
|
||||
export const repoPickerValidation = (
|
||||
value: string,
|
||||
validation: FieldValidation,
|
||||
context: { apiHolder: ApiHolder },
|
||||
) => {
|
||||
try {
|
||||
const { host, searchParams } = new URL(`https://${value}`);
|
||||
if (!host || !searchParams.get('owner') || !searchParams.get('repo')) {
|
||||
validation.addError('Incomplete repository location provided');
|
||||
|
||||
const integrationApi = context.apiHolder.get(scmIntegrationsApiRef);
|
||||
|
||||
if (!host) {
|
||||
validation.addError(
|
||||
'Incomplete repository location provided, host not provided',
|
||||
);
|
||||
} else {
|
||||
if (integrationApi?.byHost(host)?.type === 'bitbucket') {
|
||||
// workspace is only applicable for bitbucket cloud
|
||||
if (host === 'bitbucket.org' && !searchParams.get('workspace')) {
|
||||
validation.addError(
|
||||
'Incomplete repository location provided, workspace not provided',
|
||||
);
|
||||
}
|
||||
|
||||
if (!searchParams.get('project')) {
|
||||
validation.addError(
|
||||
'Incomplete repository location provided, project not provided',
|
||||
);
|
||||
}
|
||||
}
|
||||
// For anything other than bitbucket
|
||||
else {
|
||||
if (!searchParams.get('owner')) {
|
||||
validation.addError(
|
||||
'Incomplete repository location provided, owner not provided',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Do this for all hosts
|
||||
if (!searchParams.get('repo')) {
|
||||
validation.addError(
|
||||
'Incomplete repository location provided, repo not provided',
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
validation.addError('Unable to parse the Repository URL');
|
||||
|
||||
Reference in New Issue
Block a user