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:
blam
2021-02-25 09:13:40 +01:00
committed by Johan Haals
parent 20ba3205f0
commit 8c1e321684
6 changed files with 445 additions and 21 deletions
@@ -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 };
};