diff --git a/.changeset/bitbucket-scaffolder.md b/.changeset/bitbucket-scaffolder.md new file mode 100644 index 0000000000..908bcd7e2b --- /dev/null +++ b/.changeset/bitbucket-scaffolder.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Add scaffolding support for Bitbucket Cloud and Server. diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index 587a742605..6537c8f63f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -26,10 +26,10 @@ import { Config } from '@backstage/config'; export class BitbucketPreparer implements PreparerBase { private readonly privateToken: string; - private readonly user: string; + private readonly username: string; constructor(config: Config) { - this.user = + this.username = config.getOptionalString('scaffolder.bitbucket.api.username') ?? ''; this.privateToken = config.getOptionalString('scaffolder.bitbucket.api.token') ?? ''; @@ -50,13 +50,7 @@ export class BitbucketPreparer implements PreparerBase { const templateId = template.metadata.name; const repo = GitUriParser(location); - let repositoryCheckoutUrl; - // could be refactor once https://github.com/IonicaBizau/git-url-parse/pull/117 has been published - if (repo.source === 'bitbucket.org') { - repositoryCheckoutUrl = `${repo.protocol}://${repo.resource}/${repo.owner}/${repo.name}`; - } else { - repositoryCheckoutUrl = `${repo.protocol}://${repo.resource}/scm/${repo.owner}/${repo.name}`; - } + const repositoryCheckoutUrl = repo.toString('https'); const tempDir = await fs.promises.mkdtemp( path.join(workingDirectory, templateId), @@ -72,7 +66,7 @@ export class BitbucketPreparer implements PreparerBase { fetchOpts: { callbacks: { credentials: () => - Cred.userpassPlaintextNew(this.user, this.privateToken), + Cred.userpassPlaintextNew(this.username, this.privateToken), }, }, } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts index 84e06440d8..f564f43ae5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts @@ -50,7 +50,7 @@ export class BitbucketPublisher implements PublisherBase { values: RequiredTemplateValues & Record, ): Promise { const [project, name] = values.storePath.split('/'); - if (this.host === 'bitbucket.org') { + if (this.host === 'https://bitbucket.org') { return this.createBitbucketCloudRepository(project, name); } return this.createBitbucketServerRepository(project, name); @@ -60,9 +60,39 @@ export class BitbucketPublisher implements PublisherBase { project: string, name: string, ): Promise { - throw new Error( - `Failed to create ${project}/${name} on bitbucket.org which is currently not supported`, - ); + let response: Response; + const buffer = Buffer.from(`${this.username}:${this.token}`, 'utf8'); + + const options: RequestInit = { + method: 'POST', + body: JSON.stringify({}), + headers: { + Authorization: `Basic ${buffer.toString('base64')}`, + 'Content-Type': 'application/json', + }, + }; + try { + response = await fetch( + `https://api.bitbucket.org/2.0/repositories/${project}/${name}`, + 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 defautl branch + const catalogInfoUrl = `${r.links.html.href}/src/master/catalog-info.yaml`; + return { remoteUrl, catalogInfoUrl }; + } + throw new Error(`Not a valid response code ${await response.text()}`); } private async createBitbucketServerRepository(