Refactor publishers

Co-authored-by: blam<ben@blam.sh>
This commit is contained in:
Johan Haals
2021-01-19 16:50:38 +01:00
parent ff59698e4e
commit 8b66c474e3
9 changed files with 300 additions and 400 deletions
@@ -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;
};