@@ -22,7 +22,6 @@ import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { msw } from '@backstage/test-utils';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
|
||||
describe('Bitbucket Publisher', () => {
|
||||
const logger = getVoidLogger();
|
||||
@@ -59,19 +58,11 @@ describe('Bitbucket Publisher', () => {
|
||||
),
|
||||
);
|
||||
|
||||
const publisher = new BitbucketPublisher(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
bitbucket: [
|
||||
{
|
||||
host: 'bitbucket.org',
|
||||
username: 'fake-user',
|
||||
appPassword: 'fake-token',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
const publisher = await BitbucketPublisher.fromConfig({
|
||||
host: 'bitbucket.org',
|
||||
username: 'fake-user',
|
||||
appPassword: 'fake-token',
|
||||
});
|
||||
|
||||
const result = await publisher.publish({
|
||||
values: {
|
||||
@@ -126,18 +117,10 @@ describe('Bitbucket Publisher', () => {
|
||||
),
|
||||
);
|
||||
|
||||
const publisher = new BitbucketPublisher(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
bitbucket: [
|
||||
{
|
||||
host: 'bitbucket.mycompany.com',
|
||||
token: 'fake-token',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
const publisher = await BitbucketPublisher.fromConfig({
|
||||
host: 'bitbucket.mycompany.com',
|
||||
token: 'fake-token',
|
||||
});
|
||||
|
||||
const result = await publisher.publish({
|
||||
values: {
|
||||
|
||||
@@ -17,79 +17,50 @@
|
||||
import { PublisherBase, PublisherOptions, PublisherResult } from './types';
|
||||
import { initRepoAndPush } from './helpers';
|
||||
import fetch from 'cross-fetch';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
BitbucketIntegrationConfig,
|
||||
readBitbucketIntegrationConfigs,
|
||||
} from '@backstage/integration';
|
||||
import { BitbucketIntegrationConfig } from '@backstage/integration';
|
||||
import gitUrlParse from 'git-url-parse';
|
||||
|
||||
// TODO(blam): We should probably start to use a bitbucket client here that we can change
|
||||
// the baseURL to point at on-prem or public bitbucket versions like we do for
|
||||
// github and ghe
|
||||
// the baseURL to point at on-prem or public bitbucket versions like we do for
|
||||
// github and ghe. There's to much logic and not enough types here for us to say that this way is better than using
|
||||
// a supported bitbucket client if one exists.
|
||||
export class BitbucketPublisher implements PublisherBase {
|
||||
private readonly host?: string;
|
||||
private readonly username?: string;
|
||||
private readonly token?: string;
|
||||
private readonly appPassword?: string;
|
||||
private readonly integrations: BitbucketIntegrationConfig[];
|
||||
static async fromConfig(config: BitbucketIntegrationConfig) {
|
||||
return new BitbucketPublisher(
|
||||
config.host,
|
||||
config.token,
|
||||
config.appPassword,
|
||||
config.username,
|
||||
);
|
||||
}
|
||||
|
||||
static fromConfig(config: Config) {
|
||||
return new BitbucketPublisher(config);
|
||||
}
|
||||
constructor(config: Config) {
|
||||
this.integrations = readBitbucketIntegrationConfigs(
|
||||
config.getOptionalConfigArray('integrations.bitbucket') ?? [],
|
||||
);
|
||||
this.token = config.getOptionalString('scaffolder.bitbucket.api.token');
|
||||
this.host = config.getOptionalString('scaffolder.bitbucket.api.host');
|
||||
this.username = config.getOptionalString(
|
||||
'scaffolder.bitbucket.api.username',
|
||||
);
|
||||
this.appPassword = config.getOptionalString(
|
||||
'scaffolder.bitbucket.api.appPassword',
|
||||
);
|
||||
}
|
||||
constructor(
|
||||
private readonly host: string,
|
||||
private readonly token?: string,
|
||||
private readonly appPassword?: string,
|
||||
private readonly username?: string,
|
||||
) {}
|
||||
|
||||
async publish({
|
||||
values,
|
||||
directory,
|
||||
logger,
|
||||
}: PublisherOptions): Promise<PublisherResult> {
|
||||
const { resource: hostname, owner: project, name } = gitUrlParse(
|
||||
values.storePath,
|
||||
);
|
||||
|
||||
const token = this.getToken(hostname);
|
||||
const appPassword = this.getAppPassword(hostname);
|
||||
const username = this.getUsername(hostname);
|
||||
|
||||
if (!username && !appPassword && !token) {
|
||||
throw new Error('Cannot create repository without bitbucket credentials');
|
||||
}
|
||||
const host = this.getHost(hostname);
|
||||
|
||||
if (!host) {
|
||||
throw new Error('No host provided to create the remote repository');
|
||||
}
|
||||
const { owner: project, name } = gitUrlParse(values.storePath);
|
||||
|
||||
const description = values.description as string;
|
||||
const result = await this.createRemote({
|
||||
project,
|
||||
name,
|
||||
description,
|
||||
host,
|
||||
username,
|
||||
token,
|
||||
appPassword,
|
||||
});
|
||||
|
||||
await initRepoAndPush({
|
||||
dir: directory,
|
||||
remoteUrl: result.remoteUrl,
|
||||
auth: {
|
||||
username: username ? username : 'x-token-auth',
|
||||
password: appPassword ? appPassword : token ?? '',
|
||||
username: this.username ? this.username : 'x-token-auth',
|
||||
password: this.appPassword ? this.appPassword : this.token ?? '',
|
||||
},
|
||||
logger,
|
||||
});
|
||||
@@ -97,15 +68,11 @@ export class BitbucketPublisher implements PublisherBase {
|
||||
}
|
||||
|
||||
private async createRemote(opts: {
|
||||
username?: string;
|
||||
token?: string;
|
||||
appPassword?: string;
|
||||
project: string;
|
||||
name: string;
|
||||
description: string;
|
||||
host: string;
|
||||
}): Promise<PublisherResult> {
|
||||
if (opts.host === 'bitbucket.org') {
|
||||
if (this.host === 'bitbucket.org') {
|
||||
return this.createBitbucketCloudRepository(opts);
|
||||
}
|
||||
return this.createBitbucketServerRepository(opts);
|
||||
@@ -115,22 +82,11 @@ export class BitbucketPublisher implements PublisherBase {
|
||||
project: string;
|
||||
name: string;
|
||||
description: string;
|
||||
username?: string;
|
||||
appPassword?: string;
|
||||
}): Promise<PublisherResult> {
|
||||
const { project, name, description, username, appPassword } = opts;
|
||||
if (!appPassword) {
|
||||
throw new Error(
|
||||
'appPassword is required to create the remote repository',
|
||||
);
|
||||
}
|
||||
|
||||
if (!username) {
|
||||
throw new Error('username is required to create the remote repository');
|
||||
}
|
||||
const { project, name, description } = opts;
|
||||
|
||||
let response: Response;
|
||||
const buffer = Buffer.from(`${username}:${appPassword}`, 'utf8');
|
||||
const buffer = Buffer.from(`${this.username}:${this.appPassword}`, 'utf8');
|
||||
|
||||
const options: RequestInit = {
|
||||
method: 'POST',
|
||||
@@ -171,13 +127,8 @@ export class BitbucketPublisher implements PublisherBase {
|
||||
project: string;
|
||||
name: string;
|
||||
description: string;
|
||||
token?: string;
|
||||
host: string;
|
||||
}): Promise<PublisherResult> {
|
||||
const { project, name, description, token, host } = opts;
|
||||
if (!token) {
|
||||
throw new Error('No token provided to create the remote repository');
|
||||
}
|
||||
const { project, name, description } = opts;
|
||||
|
||||
let response: Response;
|
||||
const options: RequestInit = {
|
||||
@@ -187,13 +138,13 @@ export class BitbucketPublisher implements PublisherBase {
|
||||
description: description,
|
||||
}),
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
try {
|
||||
response = await fetch(
|
||||
`https://${host}/rest/api/1.0/projects/${project}/repos`,
|
||||
`https://${this.host}/rest/api/1.0/projects/${project}/repos`,
|
||||
options,
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -212,25 +163,4 @@ export class BitbucketPublisher implements PublisherBase {
|
||||
}
|
||||
throw new Error(`Not a valid response code ${await response.text()}`);
|
||||
}
|
||||
|
||||
private getToken(host: string): string | undefined {
|
||||
return this.token || this.integrations.find(c => c.host === host)?.token;
|
||||
}
|
||||
|
||||
private getUsername(host: string): string | undefined {
|
||||
return (
|
||||
this.username || this.integrations.find(c => c.host === host)?.username
|
||||
);
|
||||
}
|
||||
|
||||
private getAppPassword(host: string): string | undefined {
|
||||
return (
|
||||
this.appPassword ||
|
||||
this.integrations.find(c => c.host === host)?.appPassword
|
||||
);
|
||||
}
|
||||
|
||||
private getHost(host: string): string | undefined {
|
||||
return this.host || this.integrations.find(c => c.host === host)?.host;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,21 +38,16 @@ describe('GitHub Publisher', () => {
|
||||
});
|
||||
|
||||
describe('with public repo visibility', () => {
|
||||
const publisher = new GithubPublisher(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
github: [
|
||||
{
|
||||
token: 'fake-token',
|
||||
host: 'github.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
describe('publish: createRemoteInGithub', () => {
|
||||
it('should use octokit to create a repo in an organisation if the organisation property is set', async () => {
|
||||
const publisher = await GithubPublisher.fromConfig(
|
||||
{
|
||||
token: 'fake-token',
|
||||
host: 'github.com',
|
||||
},
|
||||
{ repoVisibility: 'public' },
|
||||
);
|
||||
|
||||
mockGithubClient.repos.createInOrg.mockResolvedValue({
|
||||
data: {
|
||||
clone_url: 'https://github.com/backstage/backstage.git',
|
||||
@@ -64,7 +59,7 @@ describe('GitHub Publisher', () => {
|
||||
},
|
||||
} as RestEndpointMethodTypes['users']['getByUsername']['response']);
|
||||
|
||||
const result = await publisher.publish({
|
||||
const result = await publisher!.publish({
|
||||
values: {
|
||||
storePath: 'https://github.com/blam/test',
|
||||
owner: 'bob',
|
||||
@@ -103,6 +98,14 @@ describe('GitHub Publisher', () => {
|
||||
});
|
||||
|
||||
it('should use octokit to create a repo in the authed user if the organisation property is not set', async () => {
|
||||
const publisher = await GithubPublisher.fromConfig(
|
||||
{
|
||||
token: 'fake-token',
|
||||
host: 'github.com',
|
||||
},
|
||||
{ repoVisibility: 'public' },
|
||||
);
|
||||
|
||||
mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({
|
||||
data: {
|
||||
clone_url: 'https://github.com/backstage/backstage.git',
|
||||
@@ -114,7 +117,7 @@ describe('GitHub Publisher', () => {
|
||||
},
|
||||
} as RestEndpointMethodTypes['users']['getByUsername']['response']);
|
||||
|
||||
const result = await publisher.publish({
|
||||
const result = await publisher!.publish({
|
||||
values: {
|
||||
storePath: 'https://github.com/blam/test',
|
||||
owner: 'bob',
|
||||
@@ -147,6 +150,14 @@ describe('GitHub Publisher', () => {
|
||||
});
|
||||
|
||||
it('should invite other user in the authed user', async () => {
|
||||
const publisher = await GithubPublisher.fromConfig(
|
||||
{
|
||||
token: 'fake-token',
|
||||
host: 'github.com',
|
||||
},
|
||||
{ repoVisibility: 'public' },
|
||||
);
|
||||
|
||||
mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({
|
||||
data: {
|
||||
clone_url: 'https://github.com/backstage/backstage.git',
|
||||
@@ -158,7 +169,7 @@ describe('GitHub Publisher', () => {
|
||||
},
|
||||
} as RestEndpointMethodTypes['users']['getByUsername']['response']);
|
||||
|
||||
const result = await publisher.publish({
|
||||
const result = await publisher!.publish({
|
||||
values: {
|
||||
storePath: 'https://github.com/blam/test',
|
||||
owner: 'bob',
|
||||
@@ -197,20 +208,15 @@ describe('GitHub Publisher', () => {
|
||||
});
|
||||
|
||||
describe('with internal repo visibility', () => {
|
||||
const publisher = new GithubPublisher(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
github: [{ host: 'github.com', token: 'fake-token' }],
|
||||
},
|
||||
scaffolder: {
|
||||
github: {
|
||||
visibility: 'internal',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
it('creates a private repository in the organization with visibility set to internal', async () => {
|
||||
const publisher = await GithubPublisher.fromConfig(
|
||||
{
|
||||
token: 'fake-token',
|
||||
host: 'github.com',
|
||||
},
|
||||
{ repoVisibility: 'internal' },
|
||||
);
|
||||
|
||||
mockGithubClient.repos.createInOrg.mockResolvedValue({
|
||||
data: {
|
||||
clone_url: 'https://github.com/backstage/backstage.git',
|
||||
@@ -222,7 +228,7 @@ describe('GitHub Publisher', () => {
|
||||
},
|
||||
} as RestEndpointMethodTypes['users']['getByUsername']['response']);
|
||||
|
||||
const result = await publisher.publish({
|
||||
const result = await publisher!.publish({
|
||||
values: {
|
||||
isOrg: true,
|
||||
storePath: 'https://github.com/blam/test',
|
||||
@@ -253,25 +259,15 @@ describe('GitHub Publisher', () => {
|
||||
});
|
||||
|
||||
describe('private visibility in a user account', () => {
|
||||
const publisher = new GithubPublisher(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
github: [
|
||||
{
|
||||
token: 'fake-token',
|
||||
host: 'github.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
scaffolder: {
|
||||
github: {
|
||||
visibility: 'private',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
it('creates a private repository', async () => {
|
||||
const publisher = await GithubPublisher.fromConfig(
|
||||
{
|
||||
token: 'fake-token',
|
||||
host: 'github.com',
|
||||
},
|
||||
{ repoVisibility: 'private' },
|
||||
);
|
||||
|
||||
mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({
|
||||
data: {
|
||||
clone_url: 'https://github.com/backstage/backstage.git',
|
||||
@@ -283,7 +279,7 @@ describe('GitHub Publisher', () => {
|
||||
},
|
||||
} as RestEndpointMethodTypes['users']['getByUsername']['response']);
|
||||
|
||||
const result = await publisher.publish({
|
||||
const result = await publisher!.publish({
|
||||
values: {
|
||||
storePath: 'https://github.com/blam/test',
|
||||
owner: 'bob',
|
||||
|
||||
@@ -16,68 +16,54 @@
|
||||
|
||||
import { PublisherBase, PublisherOptions, PublisherResult } from './types';
|
||||
import { initRepoAndPush } from './helpers';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
GitHubIntegrationConfig,
|
||||
readGitHubIntegrationConfigs,
|
||||
} from '@backstage/integration';
|
||||
import { GitHubIntegrationConfig } from '@backstage/integration';
|
||||
import gitUrlParse from 'git-url-parse';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
|
||||
export type RepoVisibilityOptions = 'private' | 'internal' | 'public';
|
||||
export class GithubPublisher implements PublisherBase {
|
||||
private scaffolderToken: string | undefined;
|
||||
private readonly integrations: GitHubIntegrationConfig[];
|
||||
private readonly apiBaseUrl: string | undefined;
|
||||
private readonly repoVisibility: RepoVisibilityOptions;
|
||||
static async fromConfig(
|
||||
config: GitHubIntegrationConfig,
|
||||
{ repoVisibility }: { repoVisibility: RepoVisibilityOptions },
|
||||
) {
|
||||
if (!config.token) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static fromConfig(config: Config) {
|
||||
return new GithubPublisher(config);
|
||||
}
|
||||
constructor(config: Config) {
|
||||
this.integrations = readGitHubIntegrationConfigs(
|
||||
config.getOptionalConfigArray('integrations.github') ?? [],
|
||||
);
|
||||
|
||||
this.scaffolderToken = config.getOptionalString(
|
||||
'scaffolder.github.api.token',
|
||||
);
|
||||
|
||||
this.apiBaseUrl = config.getOptionalString('scaffolder.github.api.baseUrl');
|
||||
|
||||
this.repoVisibility = (config.getOptionalString(
|
||||
'scaffolder.github.visibility',
|
||||
) ?? 'public') as RepoVisibilityOptions;
|
||||
const githubClient = new Octokit({
|
||||
auth: config.token,
|
||||
baseUrl: config.apiBaseUrl,
|
||||
});
|
||||
|
||||
return new GithubPublisher(config.token, githubClient, repoVisibility);
|
||||
}
|
||||
constructor(
|
||||
private readonly token: string,
|
||||
private readonly client: Octokit,
|
||||
private readonly repoVisibility: RepoVisibilityOptions,
|
||||
) {}
|
||||
|
||||
async publish({
|
||||
values,
|
||||
directory,
|
||||
logger,
|
||||
}: PublisherOptions): Promise<PublisherResult> {
|
||||
const { resource: host, owner, name } = gitUrlParse(values.storePath);
|
||||
const token = this.getToken(host);
|
||||
|
||||
if (!token) {
|
||||
throw new Error('No token provided to create the remote repository');
|
||||
}
|
||||
const { owner, name } = gitUrlParse(values.storePath);
|
||||
|
||||
const description = values.description as string;
|
||||
const access = values.access as string;
|
||||
const remoteUrl = await this.createRemote({
|
||||
description,
|
||||
access,
|
||||
host,
|
||||
name,
|
||||
owner,
|
||||
token,
|
||||
});
|
||||
|
||||
await initRepoAndPush({
|
||||
dir: directory,
|
||||
remoteUrl,
|
||||
auth: {
|
||||
username: token ?? '',
|
||||
username: this.token,
|
||||
password: 'x-oauth-basic',
|
||||
},
|
||||
logger,
|
||||
@@ -95,30 +81,24 @@ export class GithubPublisher implements PublisherBase {
|
||||
access: string;
|
||||
name: string;
|
||||
owner: string;
|
||||
host: string;
|
||||
token: string;
|
||||
description: string;
|
||||
}) {
|
||||
const { access, description, host, owner, name, token } = opts;
|
||||
const { access, description, owner, name } = opts;
|
||||
|
||||
// create a github client with the config
|
||||
const githubClient = new Octokit({
|
||||
auth: token,
|
||||
baseUrl: this.getBaseUrl(host),
|
||||
const user = await this.client.users.getByUsername({
|
||||
username: owner,
|
||||
});
|
||||
|
||||
const user = await githubClient.users.getByUsername({ username: owner });
|
||||
|
||||
const repoCreationPromise =
|
||||
user.data.type === 'Organization'
|
||||
? githubClient.repos.createInOrg({
|
||||
? this.client.repos.createInOrg({
|
||||
name,
|
||||
org: owner,
|
||||
private: this.repoVisibility !== 'public',
|
||||
visibility: this.repoVisibility,
|
||||
description,
|
||||
})
|
||||
: githubClient.repos.createForAuthenticatedUser({
|
||||
: this.client.repos.createForAuthenticatedUser({
|
||||
name,
|
||||
private: this.repoVisibility === 'private',
|
||||
description,
|
||||
@@ -128,7 +108,7 @@ export class GithubPublisher implements PublisherBase {
|
||||
|
||||
if (access?.startsWith(`${owner}/`)) {
|
||||
const [, team] = access.split('/');
|
||||
await githubClient.teams.addOrUpdateRepoPermissionsInOrg({
|
||||
await this.client.teams.addOrUpdateRepoPermissionsInOrg({
|
||||
org: owner,
|
||||
team_slug: team,
|
||||
owner,
|
||||
@@ -137,7 +117,7 @@ export class GithubPublisher implements PublisherBase {
|
||||
});
|
||||
// no need to add access if it's the person who own's the personal account
|
||||
} else if (access && access !== owner) {
|
||||
await githubClient.repos.addCollaborator({
|
||||
await this.client.repos.addCollaborator({
|
||||
owner,
|
||||
repo: name,
|
||||
username: access,
|
||||
@@ -147,18 +127,4 @@ export class GithubPublisher implements PublisherBase {
|
||||
|
||||
return data?.clone_url;
|
||||
}
|
||||
|
||||
private getToken(host: string): string | undefined {
|
||||
return (
|
||||
this.scaffolderToken ||
|
||||
this.integrations.find(c => c.host === host)?.token
|
||||
);
|
||||
}
|
||||
|
||||
private getBaseUrl(host: string): string | undefined {
|
||||
return (
|
||||
this.apiBaseUrl ||
|
||||
this.integrations.find(c => c.host === host)?.apiBaseUrl
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,18 +50,10 @@ describe('GitLab Publisher', () => {
|
||||
|
||||
describe('publish: createRemoteInGitLab', () => {
|
||||
it('should use gitbeaker to create a repo in a namespace if the namespace property is set', async () => {
|
||||
const publisher = new GitlabPublisher(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
gitlab: [
|
||||
{
|
||||
host: 'gitlab.com',
|
||||
token: 'fake-token',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
const publisher = await GitlabPublisher.fromConfig({
|
||||
host: 'gitlab.com',
|
||||
token: 'fake-token',
|
||||
});
|
||||
|
||||
mockGitlabClient.Namespaces.show.mockResolvedValue({
|
||||
id: 42,
|
||||
@@ -70,7 +62,7 @@ describe('GitLab Publisher', () => {
|
||||
http_url_to_repo: 'mockclone',
|
||||
} as { http_url_to_repo: string });
|
||||
|
||||
const result = await publisher.publish({
|
||||
const result = await publisher!.publish({
|
||||
values: {
|
||||
isOrg: true,
|
||||
storePath: 'https://gitlab.com/blam/test',
|
||||
@@ -97,18 +89,10 @@ describe('GitLab Publisher', () => {
|
||||
});
|
||||
|
||||
it('should use gitbeaker to create a repo in the authed user if the namespace property is not set', async () => {
|
||||
const publisher = new GitlabPublisher(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
gitlab: [
|
||||
{
|
||||
host: 'gitlab.com',
|
||||
token: 'fake-token',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
const publisher = await GitlabPublisher.fromConfig({
|
||||
host: 'gitlab.com',
|
||||
token: 'fake-token',
|
||||
});
|
||||
|
||||
mockGitlabClient.Namespaces.show.mockResolvedValue({});
|
||||
mockGitlabClient.Users.current.mockResolvedValue({
|
||||
@@ -118,7 +102,7 @@ describe('GitLab Publisher', () => {
|
||||
http_url_to_repo: 'mockclone',
|
||||
} as { http_url_to_repo: string });
|
||||
|
||||
const result = await publisher.publish({
|
||||
const result = await publisher!.publish({
|
||||
values: {
|
||||
storePath: 'https://gitlab.com/blam/test',
|
||||
owner: 'bob',
|
||||
|
||||
@@ -16,61 +16,37 @@
|
||||
|
||||
import { PublisherBase, PublisherOptions, PublisherResult } from './types';
|
||||
import { Gitlab } from '@gitbeaker/node';
|
||||
import { Config } from '@backstage/config';
|
||||
import { Gitlab as GitlabClient } from '@gitbeaker/core';
|
||||
import { initRepoAndPush } from './helpers';
|
||||
import gitUrlParse from 'git-url-parse';
|
||||
|
||||
import {
|
||||
GitLabIntegrationConfig,
|
||||
readGitLabIntegrationConfigs,
|
||||
} from '@backstage/integration';
|
||||
import { GitLabIntegrationConfig } from '@backstage/integration';
|
||||
|
||||
export class GitlabPublisher implements PublisherBase {
|
||||
private readonly integrations: GitLabIntegrationConfig[];
|
||||
private readonly scaffolderToken: string | undefined;
|
||||
private readonly apiBaseUrl: string | undefined;
|
||||
static async fromConfig(config: GitLabIntegrationConfig) {
|
||||
if (!config.token) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
constructor(config: Config) {
|
||||
this.integrations = readGitLabIntegrationConfigs(
|
||||
config.getOptionalConfigArray('integrations.gitlab') ?? [],
|
||||
);
|
||||
this.scaffolderToken = config.getOptionalString(
|
||||
'scaffolder.gitlab.api.token',
|
||||
);
|
||||
|
||||
this.apiBaseUrl = config.getOptionalString('scaffolder.gitlab.api.baseUrl');
|
||||
const client = new Gitlab({ host: config.apiBaseUrl, token: config.token });
|
||||
return new GitlabPublisher(config.token, client);
|
||||
}
|
||||
|
||||
static fromConfig(config: Config) {
|
||||
return new GitlabPublisher(config);
|
||||
}
|
||||
constructor(
|
||||
private readonly token: string,
|
||||
private readonly client: GitlabClient,
|
||||
) {}
|
||||
|
||||
async publish({
|
||||
values,
|
||||
directory,
|
||||
logger,
|
||||
}: PublisherOptions): Promise<PublisherResult> {
|
||||
const { resource: host, owner, name } = gitUrlParse(values.storePath);
|
||||
|
||||
const token = this.getToken(host);
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
'No authentication set for Gitlab publisher. Creating the remote repository is not possible without a token',
|
||||
);
|
||||
}
|
||||
|
||||
const baseUrl = this.getBaseUrl(host);
|
||||
if (!baseUrl) {
|
||||
throw new Error(
|
||||
'No host set for Gitlab publisher. Creating the remote repository is not possible without a host',
|
||||
);
|
||||
}
|
||||
const { owner, name } = gitUrlParse(values.storePath);
|
||||
|
||||
const remoteUrl = await this.createRemote({
|
||||
host: baseUrl,
|
||||
owner,
|
||||
name,
|
||||
token,
|
||||
});
|
||||
|
||||
await initRepoAndPush({
|
||||
@@ -78,7 +54,7 @@ export class GitlabPublisher implements PublisherBase {
|
||||
remoteUrl,
|
||||
auth: {
|
||||
username: 'oauth2',
|
||||
password: token,
|
||||
password: this.token,
|
||||
},
|
||||
logger,
|
||||
});
|
||||
@@ -90,37 +66,21 @@ export class GitlabPublisher implements PublisherBase {
|
||||
return { remoteUrl, catalogInfoUrl };
|
||||
}
|
||||
|
||||
private getToken(host: string): string | undefined {
|
||||
return (
|
||||
this.scaffolderToken ||
|
||||
this.integrations.find(c => c.host === host)?.token
|
||||
);
|
||||
}
|
||||
private async createRemote(opts: { name: string; owner: string }) {
|
||||
const { owner, name } = opts;
|
||||
|
||||
private getBaseUrl(host: string): string | undefined {
|
||||
return (
|
||||
this.apiBaseUrl ||
|
||||
this.integrations.find(c => c.host === host)?.apiBaseUrl
|
||||
);
|
||||
}
|
||||
|
||||
private async createRemote(opts: {
|
||||
host: string;
|
||||
name: string;
|
||||
owner: string;
|
||||
token: string;
|
||||
}) {
|
||||
const { owner, name, host, token } = opts;
|
||||
|
||||
const client = new Gitlab({ host: host, token: token });
|
||||
let targetNamespace = ((await client.Namespaces.show(owner)) as {
|
||||
// TODO(blam): this needs cleaning up to be nicer. The amount of brackets is too damn high!
|
||||
// Shouldn't have to cast things now
|
||||
let targetNamespace = ((await this.client.Namespaces.show(owner)) as {
|
||||
id: number;
|
||||
}).id;
|
||||
|
||||
if (!targetNamespace) {
|
||||
targetNamespace = ((await client.Users.current()) as { id: number }).id;
|
||||
targetNamespace = ((await this.client.Users.current()) as { id: number })
|
||||
.id;
|
||||
}
|
||||
|
||||
const project = (await client.Projects.create({
|
||||
const project = (await this.client.Projects.create({
|
||||
namespace_id: targetNamespace,
|
||||
name: name,
|
||||
})) as { http_url_to_repo: string };
|
||||
|
||||
@@ -22,61 +22,86 @@ import { GitlabPublisher } from './gitlab';
|
||||
import { BitbucketPublisher } from './bitbucket';
|
||||
|
||||
jest.mock('@octokit/rest');
|
||||
jest.mock('azure-devops-node-api');
|
||||
|
||||
describe('Publishers', () => {
|
||||
const logger = getVoidLogger();
|
||||
|
||||
it('should throw an error when the publisher for the source location is not registered', () => {
|
||||
const publishers = new Publishers();
|
||||
|
||||
expect(() =>
|
||||
publishers.get('https://github.com/org/repo', {
|
||||
logger: getVoidLogger(),
|
||||
}),
|
||||
).toThrow(
|
||||
expect(() => publishers.get('https://github.com/org/repo')).toThrow(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
'No matching publisher detected for "https://github.com/org/repo". Please make sure this host is registered in the integration config',
|
||||
'Unable to find a publisher for URL: https://github.com/org/repo. Please make sure to register this host under an integration in app-config',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the correct preparer when the source matches for github', async () => {
|
||||
const publishers = await Publishers.fromConfig(new ConfigReader({}));
|
||||
|
||||
expect(
|
||||
publishers.get('https://github.com/org/repo', {
|
||||
logger: getVoidLogger(),
|
||||
const publishers = await Publishers.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
github: [{ host: 'github.com', token: 'blob' }],
|
||||
},
|
||||
}),
|
||||
).toBeInstanceOf(GithubPublisher);
|
||||
{
|
||||
logger,
|
||||
},
|
||||
);
|
||||
|
||||
expect(publishers.get('https://github.com/org/repo')).toBeInstanceOf(
|
||||
GithubPublisher,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the correct preparer when the source matches for azure', async () => {
|
||||
const publishers = await Publishers.fromConfig(new ConfigReader({}));
|
||||
const publishers = await Publishers.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
azure: [{ host: 'dev.azure.com', token: 'blob' }],
|
||||
},
|
||||
}),
|
||||
{
|
||||
logger,
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
publishers.get('https://dev.azure.com/org/project/_git/repo', {
|
||||
logger: getVoidLogger(),
|
||||
}),
|
||||
publishers.get('https://dev.azure.com/org/project/_git/repo'),
|
||||
).toBeInstanceOf(AzurePublisher);
|
||||
});
|
||||
|
||||
it('should return the correct preparer when the source matches for bitbucket', async () => {
|
||||
const publishers = await Publishers.fromConfig(new ConfigReader({}));
|
||||
|
||||
expect(
|
||||
publishers.get('https://bitbucket.org/owner/repo', {
|
||||
logger: getVoidLogger(),
|
||||
const publishers = await Publishers.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
bitbucket: [{ host: 'bitbucket.com', token: 'blob' }],
|
||||
},
|
||||
}),
|
||||
).toBeInstanceOf(BitbucketPublisher);
|
||||
{
|
||||
logger,
|
||||
},
|
||||
);
|
||||
expect(publishers.get('https://bitbucket.org/owner/repo')).toBeInstanceOf(
|
||||
BitbucketPublisher,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the correct preparer when the source matches for gitlab', async () => {
|
||||
const publishers = await Publishers.fromConfig(new ConfigReader({}));
|
||||
|
||||
expect(
|
||||
publishers.get('https://gitlab.com/owner/repo', {
|
||||
logger: getVoidLogger(),
|
||||
const publishers = await Publishers.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
gitlab: [{ host: 'gitlab.com', token: 'blob' }],
|
||||
},
|
||||
}),
|
||||
).toBeInstanceOf(GitlabPublisher);
|
||||
{
|
||||
logger,
|
||||
},
|
||||
);
|
||||
expect(publishers.get('https://gitlab.com/owner/repo')).toBeInstanceOf(
|
||||
GitlabPublisher,
|
||||
);
|
||||
});
|
||||
|
||||
it('should respect registrations for custom URLs for providers using the integrations config', async () => {
|
||||
@@ -88,12 +113,13 @@ describe('Publishers', () => {
|
||||
],
|
||||
},
|
||||
}),
|
||||
{
|
||||
logger,
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
publishers.get('https://my.special.github.enterprise.thing/org/repo', {
|
||||
logger: getVoidLogger(),
|
||||
}),
|
||||
publishers.get('https://my.special.github.enterprise.thing/org/repo'),
|
||||
).toBeInstanceOf(GithubPublisher);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,70 +14,125 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Logger } from 'winston';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
DeprecatedLocationTypeDetector,
|
||||
makeDeprecatedLocationTypeDetector,
|
||||
} from '../helpers';
|
||||
import { PublisherBase, PublisherBuilder } from './types';
|
||||
import { RemoteProtocol } from '../types';
|
||||
import { GithubPublisher } from './github';
|
||||
import { GithubPublisher, RepoVisibilityOptions } from './github';
|
||||
import { GitlabPublisher } from './gitlab';
|
||||
import { AzurePublisher } from './azure';
|
||||
import { BitbucketPublisher } from './bitbucket';
|
||||
import { Logger } from 'winston';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
|
||||
export class Publishers implements PublisherBuilder {
|
||||
private publisherMap = new Map<RemoteProtocol, PublisherBase>();
|
||||
private publisherMap = new Map<string, PublisherBase | undefined>();
|
||||
|
||||
constructor(private readonly typeDetector?: DeprecatedLocationTypeDetector) {}
|
||||
|
||||
register(protocol: RemoteProtocol, publisher: PublisherBase) {
|
||||
this.publisherMap.set(protocol, publisher);
|
||||
register(host: string, preparer: PublisherBase | undefined) {
|
||||
this.publisherMap.set(host, preparer);
|
||||
}
|
||||
|
||||
get(storePath: string, { logger }: { logger: Logger }): PublisherBase {
|
||||
const protocol = this.typeDetector?.(storePath);
|
||||
|
||||
if (!protocol) {
|
||||
get(url: string): PublisherBase {
|
||||
const preparer = this.publisherMap.get(new URL(url).host);
|
||||
if (!preparer) {
|
||||
throw new Error(
|
||||
`No matching publisher detected for "${storePath}". Please make sure this host is registered in the integration config`,
|
||||
`Unable to find a publisher for URL: ${url}. Please make sure to register this host under an integration in app-config`,
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Selected publisher ${protocol} for publishing to URL ${storePath}`,
|
||||
);
|
||||
|
||||
const publisher = this.publisherMap.get(protocol as RemoteProtocol);
|
||||
if (!publisher) {
|
||||
throw new Error(
|
||||
`Failed to detect publisher type. Unable to determine integration type for location "${protocol}". ` +
|
||||
"Please add appropriate configuration to the 'integrations' configuration section",
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(`Selected publisher for protocol ${protocol}`);
|
||||
|
||||
return publisher;
|
||||
return preparer;
|
||||
}
|
||||
|
||||
static async fromConfig(config: Config): Promise<PublisherBuilder> {
|
||||
const typeDetector = makeDeprecatedLocationTypeDetector(config);
|
||||
const publishers = new Publishers(typeDetector);
|
||||
static async fromConfig(
|
||||
config: Config,
|
||||
{ logger }: { logger: Logger },
|
||||
): Promise<PublisherBuilder> {
|
||||
const publishers = new Publishers();
|
||||
|
||||
const githubPublisher = GithubPublisher.fromConfig(config);
|
||||
publishers.register('file', githubPublisher);
|
||||
publishers.register('github', githubPublisher);
|
||||
const scm = ScmIntegrations.fromConfig(config);
|
||||
|
||||
const gitLabPublisher = GitlabPublisher.fromConfig(config);
|
||||
publishers.register('gitlab', gitLabPublisher);
|
||||
for (const integration of scm.azure.list()) {
|
||||
const publisher = await AzurePublisher.fromConfig(integration.config);
|
||||
|
||||
const azurePublisher = AzurePublisher.fromConfig(config);
|
||||
publishers.register('azure', azurePublisher);
|
||||
if (publisher) {
|
||||
publishers.register(integration.config.host, publisher);
|
||||
} else {
|
||||
logger.warn('DEPRECATED');
|
||||
|
||||
const bitbucketPublisher = BitbucketPublisher.fromConfig(config);
|
||||
publishers.register('bitbucket', bitbucketPublisher);
|
||||
publishers.register(
|
||||
integration.config.host,
|
||||
await AzurePublisher.fromConfig({
|
||||
token: config.getOptionalString('scaffolder.azure.token'),
|
||||
host: integration.config.host,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const integration of scm.github.list()) {
|
||||
const repoVisibility = (config.getOptionalString(
|
||||
'scaffolder.github.visibility',
|
||||
) ?? 'public') as RepoVisibilityOptions;
|
||||
|
||||
const publisher = await GithubPublisher.fromConfig(integration.config, {
|
||||
repoVisibility,
|
||||
});
|
||||
|
||||
if (publisher) {
|
||||
publishers.register(integration.config.host, publisher);
|
||||
} else {
|
||||
logger.warn('DEPRECATED');
|
||||
|
||||
publishers.register(
|
||||
integration.config.host,
|
||||
await GithubPublisher.fromConfig(
|
||||
{
|
||||
token: config.getOptionalString('scaffolder.github.token') ?? '',
|
||||
host: integration.config.host,
|
||||
},
|
||||
{ repoVisibility },
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const integration of scm.gitlab.list()) {
|
||||
const publisher = await GitlabPublisher.fromConfig(integration.config);
|
||||
|
||||
if (publisher) {
|
||||
publishers.register(integration.config.host, publisher);
|
||||
} else {
|
||||
logger.warn('DEPRECATED');
|
||||
|
||||
publishers.register(
|
||||
integration.config.host,
|
||||
await GitlabPublisher.fromConfig({
|
||||
token: config.getOptionalString('scaffolder.gitlab.token') ?? '',
|
||||
host: integration.config.host,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const integration of scm.bitbucket.list()) {
|
||||
const publisher = await BitbucketPublisher.fromConfig(integration.config);
|
||||
|
||||
if (publisher) {
|
||||
publishers.register(integration.config.host, publisher);
|
||||
} else {
|
||||
logger.warn('DEPRECATED');
|
||||
|
||||
publishers.register(
|
||||
integration.config.host,
|
||||
await BitbucketPublisher.fromConfig({
|
||||
token: config.getOptionalString('scaffolder.bitbucket.token') ?? '',
|
||||
username:
|
||||
config.getOptionalString('scaffolder.bitbucket.username') ?? '',
|
||||
appPassword:
|
||||
config.getOptionalString('scaffolder.bitbucket.appPassword') ??
|
||||
'',
|
||||
host: integration.config.host,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return publishers;
|
||||
}
|
||||
|
||||
@@ -45,5 +45,5 @@ export type PublisherResult = {
|
||||
|
||||
export type PublisherBuilder = {
|
||||
register(protocol: RemoteProtocol, publisher: PublisherBase): void;
|
||||
get(storePath: string, { logger }: { logger: Logger }): PublisherBase;
|
||||
get(storePath: string): PublisherBase;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user