diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts index ad83ad54e8..b07e77e49a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts @@ -16,19 +16,54 @@ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; import { initRepoAndPush } from './helpers'; -import { RequiredTemplateValues } from '../templater'; -import { JsonValue } from '../../../../../../packages/config/src'; import fetch from 'cross-fetch'; +import { Config } from '@backstage/config'; +import { + BitbucketIntegrationConfig, + readBitbucketIntegrationConfigs, +} from '@backstage/integration'; +import { Logger } from 'winston'; +import gitUrlParse from 'git-url-parse'; export class BitbucketPublisher implements PublisherBase { - private readonly host: string; - private readonly username: string; - private readonly token: string; + private readonly host?: string; + private readonly username?: string; + private readonly token?: string; + private readonly integrations: BitbucketIntegrationConfig[]; - constructor(host: string, username: string, token: string) { - this.host = host; - this.username = username; - this.token = token; + constructor(config: Config, { logger }: { logger: Logger }) { + this.integrations = readBitbucketIntegrationConfigs( + config.getOptionalConfigArray('integrations.bitbucket') ?? [], + ); + + if (!this.integrations.length) { + logger.warn( + 'Integrations for Bitbucket in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', + ); + } + + this.token = config.getOptionalString('scaffolder.bitbucket.api.token'); + if (this.token) { + logger.warn( + "DEPRECATION: Using the token format under 'scaffolder.bitbucket.api.token' will not be respected in future releases. Please consider using integrations config instead", + ); + } + + this.host = config.getOptionalString('scaffolder.bitbucket.api.host'); + if (this.host) { + logger.warn( + "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.bitbucket.api.host' will not be respected in future releases. Please consider using integrations config instead", + ); + } + + this.username = config.getOptionalString( + 'scaffolder.bitbucket.api.username', + ); + if (this.username) { + logger.warn( + "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.bitbucket.api.username' will not be respected in future releases. Please consider using integrations config instead", + ); + } } async publish({ @@ -36,33 +71,61 @@ export class BitbucketPublisher implements PublisherBase { directory, logger, }: PublisherOptions): Promise { - const result = await this.createRemote(values); + const { resource: host, owner: project, name } = gitUrlParse( + values.storePath, + ); + + const token = this.getToken(host); + if (!token) { + throw new Error('No token provided to create the remote repository'); + } + const username = this.getUsername(host); + if (!username) { + throw new Error('No username provided to create the remote repository'); + } + const apiUrl = this.getHost(host); + if (!apiUrl) { + throw new Error('No host provided to create the remote repository'); + } + + const description = values.description as string; + const result = await this.createRemote({ + project, + name, + description, + host, + }); await initRepoAndPush({ dir: directory, remoteUrl: result.remoteUrl, auth: { - username: this.username, - password: this.token, + username: username, + password: token, }, logger, }); return result; } - private async createRemote( - values: RequiredTemplateValues & Record, - ): Promise { - if (this.host === 'https://bitbucket.org') { - return this.createBitbucketCloudRepository(values); + private async createRemote(opts: { + project: string; + name: string; + description: string; + host: string; + }): Promise { + if (opts.host === 'https://bitbucket.org') { + return this.createBitbucketCloudRepository(opts); } - return this.createBitbucketServerRepository(values); + return this.createBitbucketServerRepository(opts); } - private async createBitbucketCloudRepository( - values: RequiredTemplateValues & Record, - ): Promise { - const [project, name] = values.storePath.split('/'); + private async createBitbucketCloudRepository(opts: { + project: string; + name: string; + description: string; + }): Promise { + const { project, name, description } = opts; let response: Response; const buffer = Buffer.from(`${this.username}:${this.token}`, 'utf8'); @@ -71,7 +134,7 @@ export class BitbucketPublisher implements PublisherBase { method: 'POST', body: JSON.stringify({ scm: 'git', - description: values.description, + description: description, }), headers: { Authorization: `Basic ${buffer.toString('base64')}`, @@ -102,17 +165,19 @@ export class BitbucketPublisher implements PublisherBase { throw new Error(`Not a valid response code ${await response.text()}`); } - private async createBitbucketServerRepository( - values: RequiredTemplateValues & Record, - ): Promise { - const [project, name] = values.storePath.split('/'); + private async createBitbucketServerRepository(opts: { + project: string; + name: string; + description: string; + }): Promise { + const { project, name, description } = opts; let response: Response; const options: RequestInit = { method: 'POST', body: JSON.stringify({ name: name, - description: values.description, + description: description, }), headers: { Authorization: `Bearer ${this.token}`, @@ -140,4 +205,18 @@ 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 getHost(host: string): string | undefined { + return this.host || this.integrations.find(c => c.host === host)?.host; + } }