feat: porting over the other existing publishers to the new action format
Signed-off-by: Johan Haals <johan.haals@gmail.com>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { TemplateAction } from '../../types';
|
||||
import { initRepoAndPush } from '../../../stages/publish/helpers';
|
||||
import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces';
|
||||
import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api';
|
||||
import { parseRepoUrl } from './util';
|
||||
|
||||
export function createPublishAzureAction(options: {
|
||||
integrations: ScmIntegrations;
|
||||
}): TemplateAction<{
|
||||
repoUrl: string;
|
||||
description?: string;
|
||||
}> {
|
||||
const { integrations } = options;
|
||||
|
||||
return {
|
||||
id: 'publish:azure',
|
||||
parameterSchema: {
|
||||
type: 'object',
|
||||
required: ['repoUrl'],
|
||||
properties: {
|
||||
repoUrl: {
|
||||
title: 'Repository Location',
|
||||
type: 'string',
|
||||
},
|
||||
description: {
|
||||
title: 'Repository Description',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
async handler(ctx) {
|
||||
const { owner, repo, host } = parseRepoUrl(ctx.parameters.repoUrl);
|
||||
const integrationConfig = integrations.azure.byHost(host);
|
||||
|
||||
if (!integrationConfig) {
|
||||
throw new InputError(
|
||||
`No matching integration configuration for host ${host}, please check your Integrations config`,
|
||||
);
|
||||
}
|
||||
if (!integrationConfig.config.token) {
|
||||
throw new InputError(`No token provided for Azure Integration ${host}`);
|
||||
}
|
||||
const authHandler = getPersonalAccessTokenHandler(
|
||||
integrationConfig.config.token,
|
||||
);
|
||||
|
||||
const webApi = new WebApi(`https://${host}/${owner}`, authHandler);
|
||||
const client = await webApi.getGitApi();
|
||||
const createOptions: GitRepositoryCreateOptions = { name: repo };
|
||||
const { remoteUrl } = await client.createRepository(createOptions, owner);
|
||||
|
||||
if (!remoteUrl) {
|
||||
throw new InputError(
|
||||
'No remote URL returned from create repository for Azure',
|
||||
);
|
||||
}
|
||||
|
||||
// blam: Repo contents is serialized into the path,
|
||||
// so it's just the base path I think
|
||||
const repoContentsUrl = remoteUrl;
|
||||
|
||||
await initRepoAndPush({
|
||||
dir: ctx.workspacePath,
|
||||
remoteUrl,
|
||||
auth: {
|
||||
username: 'notempty',
|
||||
password: integrationConfig.config.token,
|
||||
},
|
||||
logger: ctx.logger,
|
||||
});
|
||||
|
||||
ctx.output('remoteUrl', remoteUrl);
|
||||
ctx.output('repoContentsUrl', repoContentsUrl);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import {
|
||||
BitbucketIntegrationConfig,
|
||||
ScmIntegrations,
|
||||
} from '@backstage/integration';
|
||||
import { TemplateAction } from '../../types';
|
||||
import { initRepoAndPush } from '../../../stages/publish/helpers';
|
||||
import { parseRepoUrl } from './util';
|
||||
|
||||
const createBitbucketCloudRepository = async (opts: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
description: string;
|
||||
repoVisibility: 'private' | 'public';
|
||||
authorization: string;
|
||||
}) => {
|
||||
const { owner, repo, description, repoVisibility, authorization } = opts;
|
||||
|
||||
let response: Response;
|
||||
|
||||
const options: RequestInit = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
scm: 'git',
|
||||
description: description,
|
||||
is_private: repoVisibility === 'private',
|
||||
}),
|
||||
headers: {
|
||||
Authorization: authorization,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
try {
|
||||
response = await fetch(
|
||||
`https://api.bitbucket.org/2.0/repositories/${owner}/${repo}`,
|
||||
options,
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(`Unable to create repository, ${e}`);
|
||||
}
|
||||
if (response.status === 200) {
|
||||
const r = await response.json();
|
||||
let remoteUrl = '';
|
||||
for (const link of r.links.clone) {
|
||||
if (link.name === 'https') {
|
||||
remoteUrl = link.href;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO use the urlReader to get the default branch
|
||||
const repoContentsUrl = `${r.links.html.href}/src/master`;
|
||||
return { remoteUrl, repoContentsUrl };
|
||||
}
|
||||
throw new Error(`Not a valid response code ${await response.text()}`);
|
||||
};
|
||||
|
||||
const createBitbucketServerRepository = async (opts: {
|
||||
host: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
description: string;
|
||||
repoVisibility: 'private' | 'public';
|
||||
authorization: string;
|
||||
}) => {
|
||||
const {
|
||||
host,
|
||||
owner,
|
||||
repo,
|
||||
description,
|
||||
authorization,
|
||||
repoVisibility,
|
||||
} = opts;
|
||||
|
||||
let response: Response;
|
||||
const options: RequestInit = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: repo,
|
||||
description: description,
|
||||
is_private: repoVisibility === 'private',
|
||||
}),
|
||||
headers: {
|
||||
Authorization: authorization,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
response = await fetch(
|
||||
`https://${host}/rest/api/1.0/projects/${owner}/repos`,
|
||||
options,
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(`Unable to create repository, ${e}`);
|
||||
}
|
||||
if (response.status === 201) {
|
||||
const r = await response.json();
|
||||
let remoteUrl = '';
|
||||
for (const link of r.links.clone) {
|
||||
if (link.name === 'http') {
|
||||
remoteUrl = link.href;
|
||||
}
|
||||
}
|
||||
const repoContentsUrl = `${r.links.self[0].href}`;
|
||||
return { remoteUrl, repoContentsUrl };
|
||||
}
|
||||
throw new Error(`Not a valid response code ${await response.text()}`);
|
||||
};
|
||||
|
||||
const getAuthorizationHeader = (config: BitbucketIntegrationConfig) => {
|
||||
if (config.username && config.appPassword) {
|
||||
const buffer = Buffer.from(
|
||||
`${config.username}:${config.appPassword}`,
|
||||
'utf8',
|
||||
);
|
||||
|
||||
return `Basic ${buffer.toString('base64')}`;
|
||||
}
|
||||
|
||||
if (config.token) {
|
||||
return `Bearer ${config.token}`;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Authorization has not been provided for Bitbucket. Please add either username + appPassword or token to the Integrations config`,
|
||||
);
|
||||
};
|
||||
|
||||
export function createPublishBitbucketAction(options: {
|
||||
integrations: ScmIntegrations;
|
||||
repoVisibility: 'private' | 'public';
|
||||
}): TemplateAction<{
|
||||
repoUrl: string;
|
||||
description: string;
|
||||
}> {
|
||||
const { integrations, repoVisibility } = options;
|
||||
|
||||
return {
|
||||
id: 'publish:bitbucket',
|
||||
parameterSchema: {
|
||||
type: 'object',
|
||||
required: ['repoUrl'],
|
||||
properties: {
|
||||
repoUrl: {
|
||||
title: 'Repository Location',
|
||||
type: 'string',
|
||||
},
|
||||
description: {
|
||||
title: 'Repository Description',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
async handler(ctx) {
|
||||
const { repoUrl, description } = ctx.parameters;
|
||||
|
||||
const { owner, repo, host } = parseRepoUrl(repoUrl);
|
||||
|
||||
const integrationConfig = integrations.bitbucket.byHost(host);
|
||||
|
||||
if (!integrationConfig) {
|
||||
throw new InputError(
|
||||
`No matching integration configuration for host ${host}, please check your Integrations config`,
|
||||
);
|
||||
}
|
||||
|
||||
const authorization = getAuthorizationHeader(integrationConfig.config);
|
||||
|
||||
const createMethod =
|
||||
host === 'bitbucket.org'
|
||||
? createBitbucketCloudRepository
|
||||
: createBitbucketServerRepository;
|
||||
|
||||
const { remoteUrl, repoContentsUrl } = await createMethod({
|
||||
authorization,
|
||||
host,
|
||||
owner,
|
||||
repo,
|
||||
repoVisibility,
|
||||
description,
|
||||
});
|
||||
|
||||
await initRepoAndPush({
|
||||
dir: ctx.workspacePath,
|
||||
remoteUrl,
|
||||
auth: {
|
||||
username: integrationConfig.config.username
|
||||
? integrationConfig.config.username
|
||||
: 'x-token-auth',
|
||||
password: integrationConfig.config.appPassword
|
||||
? integrationConfig.config.appPassword
|
||||
: integrationConfig.config.token ?? '',
|
||||
},
|
||||
logger: ctx.logger,
|
||||
});
|
||||
|
||||
ctx.output('remoteUrl', remoteUrl);
|
||||
ctx.output('repoContentsUrl', repoContentsUrl);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { TemplateAction } from '../../types';
|
||||
import { initRepoAndPush } from '../../../stages/publish/helpers';
|
||||
import { parseRepoUrl } from './util';
|
||||
|
||||
export function createPublishGithubAction(options: {
|
||||
integrations: ScmIntegrations;
|
||||
@@ -63,27 +64,7 @@ export function createPublishGithubAction(options: {
|
||||
async handler(ctx) {
|
||||
const { repoUrl, description, access } = ctx.parameters;
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(`https://${repoUrl}`);
|
||||
} catch (error) {
|
||||
throw new InputError(
|
||||
`Invalid repo URL passed to publish:github, got ${repoUrl}, ${error}`,
|
||||
);
|
||||
}
|
||||
const host = parsed.host;
|
||||
const owner = parsed.searchParams.get('owner');
|
||||
if (!owner) {
|
||||
throw new InputError(
|
||||
`Invalid repo URL passed to publish:github, missing owner`,
|
||||
);
|
||||
}
|
||||
const repo = parsed.searchParams.get('repo');
|
||||
if (!repo) {
|
||||
throw new InputError(
|
||||
`Invalid repo URL passed to publish:github, missing repo`,
|
||||
);
|
||||
}
|
||||
const { owner, repo, host } = parseRepoUrl(repoUrl);
|
||||
|
||||
const credentialsProvider = credentialsProviders.get(host);
|
||||
const integrationConfig = integrations.github.byHost(host);
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { TemplateAction } from '../../types';
|
||||
import { Gitlab } from '@gitbeaker/node';
|
||||
import { initRepoAndPush } from '../../../stages/publish/helpers';
|
||||
import { parseRepoUrl } from './util';
|
||||
|
||||
export function createPublishGitlabAction(options: {
|
||||
integrations: ScmIntegrations;
|
||||
repoVisibility: 'private' | 'internal' | 'public';
|
||||
}): TemplateAction<{
|
||||
repoUrl: string;
|
||||
}> {
|
||||
const { integrations, repoVisibility } = options;
|
||||
|
||||
return {
|
||||
id: 'publish:gitlab',
|
||||
parameterSchema: {
|
||||
type: 'object',
|
||||
required: ['repoUrl'],
|
||||
properties: {
|
||||
repoUrl: {
|
||||
title: 'Repository Location',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
async handler(ctx) {
|
||||
const { repoUrl } = ctx.parameters;
|
||||
|
||||
const { owner, repo, host } = parseRepoUrl(repoUrl);
|
||||
|
||||
const integrationConfig = integrations.gitlab.byHost(host);
|
||||
|
||||
if (!integrationConfig) {
|
||||
throw new InputError(
|
||||
`No matching integration configuration for host ${host}, please check your Integrations config`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!integrationConfig.config.token) {
|
||||
throw new InputError(
|
||||
`No token provided for GitLab Integration ${host}`,
|
||||
);
|
||||
}
|
||||
|
||||
const client = new Gitlab({
|
||||
host: integrationConfig.config.baseUrl,
|
||||
token: integrationConfig.config.token,
|
||||
});
|
||||
|
||||
let { id: targetNamespace } = (await client.Namespaces.show(owner)) as {
|
||||
id: number;
|
||||
};
|
||||
|
||||
if (!targetNamespace) {
|
||||
const { id } = (await client.Users.current()) as {
|
||||
id: number;
|
||||
};
|
||||
targetNamespace = id;
|
||||
}
|
||||
|
||||
const { http_url_to_repo } = await client.Projects.create({
|
||||
namespace_id: targetNamespace,
|
||||
name: repo,
|
||||
visibility: repoVisibility,
|
||||
});
|
||||
|
||||
const remoteUrl = (http_url_to_repo as string).replace(/\.git$/, '');
|
||||
const repoContentsUrl = `${remoteUrl}/-/blob/master`;
|
||||
|
||||
await initRepoAndPush({
|
||||
dir: ctx.workspacePath,
|
||||
remoteUrl,
|
||||
auth: {
|
||||
username: 'oauth2',
|
||||
password: integrationConfig.config.token,
|
||||
},
|
||||
logger: ctx.logger,
|
||||
});
|
||||
|
||||
ctx.output('remoteUrl', remoteUrl);
|
||||
ctx.output('repoContentsUrl', repoContentsUrl);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -15,3 +15,6 @@
|
||||
*/
|
||||
|
||||
export { createPublishGithubAction } from './github';
|
||||
export { createPublishAzureAction } from './azure';
|
||||
export { createPublishGitlabAction } from './gitlab';
|
||||
export { createPublishBitbucketAction } from './bitbucket';
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
export const parseRepoUrl = (repoUrl: string) => {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(`https://${repoUrl}`);
|
||||
} catch (error) {
|
||||
throw new InputError(
|
||||
`Invalid repo URL passed to publisher, got ${repoUrl}, ${error}`,
|
||||
);
|
||||
}
|
||||
const host = parsed.host;
|
||||
const owner = parsed.searchParams.get('owner');
|
||||
|
||||
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(
|
||||
`Invalid repo URL passed to publisher: ${repoUrl}, missing repo`,
|
||||
);
|
||||
}
|
||||
|
||||
return { host, owner, repo };
|
||||
};
|
||||
Reference in New Issue
Block a user