PageRenderTime 49ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts

https://github.com/backstage/backstage
TypeScript | 350 lines | 297 code | 35 blank | 18 comment | 44 complexity | feaa320f6c84c3b5f1b06b17cab38796 MD5 | raw file
Possible License(s): Apache-2.0, MIT, BSD-3-Clause, MPL-2.0-no-copyleft-exception
  1. /*
  2. * Copyright 2021 The Backstage Authors
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. import { InputError } from '@backstage/errors';
  17. import {
  18. BitbucketIntegrationConfig,
  19. ScmIntegrationRegistry,
  20. } from '@backstage/integration';
  21. import fetch from 'cross-fetch';
  22. import { initRepoAndPush } from '../helpers';
  23. import { createTemplateAction } from '../../createTemplateAction';
  24. import { getRepoSourceDirectory, parseRepoUrl } from './util';
  25. import { Config } from '@backstage/config';
  26. const createBitbucketCloudRepository = async (opts: {
  27. workspace: string;
  28. project: string;
  29. repo: string;
  30. description: string;
  31. repoVisibility: 'private' | 'public';
  32. authorization: string;
  33. }) => {
  34. const {
  35. workspace,
  36. project,
  37. repo,
  38. description,
  39. repoVisibility,
  40. authorization,
  41. } = opts;
  42. const options: RequestInit = {
  43. method: 'POST',
  44. body: JSON.stringify({
  45. scm: 'git',
  46. description: description,
  47. is_private: repoVisibility === 'private',
  48. project: { key: project },
  49. }),
  50. headers: {
  51. Authorization: authorization,
  52. 'Content-Type': 'application/json',
  53. },
  54. };
  55. let response: Response;
  56. try {
  57. response = await fetch(
  58. `https://api.bitbucket.org/2.0/repositories/${workspace}/${repo}`,
  59. options,
  60. );
  61. } catch (e) {
  62. throw new Error(`Unable to create repository, ${e}`);
  63. }
  64. if (response.status !== 200) {
  65. throw new Error(
  66. `Unable to create repository, ${response.status} ${
  67. response.statusText
  68. }, ${await response.text()}`,
  69. );
  70. }
  71. const r = await response.json();
  72. let remoteUrl = '';
  73. for (const link of r.links.clone) {
  74. if (link.name === 'https') {
  75. remoteUrl = link.href;
  76. }
  77. }
  78. // TODO use the urlReader to get the default branch
  79. const repoContentsUrl = `${r.links.html.href}/src/master`;
  80. return { remoteUrl, repoContentsUrl };
  81. };
  82. const createBitbucketServerRepository = async (opts: {
  83. host: string;
  84. project: string;
  85. repo: string;
  86. description: string;
  87. repoVisibility: 'private' | 'public';
  88. authorization: string;
  89. apiBaseUrl?: string;
  90. }) => {
  91. const {
  92. host,
  93. project,
  94. repo,
  95. description,
  96. authorization,
  97. repoVisibility,
  98. apiBaseUrl,
  99. } = opts;
  100. let response: Response;
  101. const options: RequestInit = {
  102. method: 'POST',
  103. body: JSON.stringify({
  104. name: repo,
  105. description: description,
  106. public: repoVisibility === 'public',
  107. }),
  108. headers: {
  109. Authorization: authorization,
  110. 'Content-Type': 'application/json',
  111. },
  112. };
  113. try {
  114. const baseUrl = apiBaseUrl ? apiBaseUrl : `https://${host}/rest/api/1.0`;
  115. response = await fetch(`${baseUrl}/projects/${project}/repos`, options);
  116. } catch (e) {
  117. throw new Error(`Unable to create repository, ${e}`);
  118. }
  119. if (response.status !== 201) {
  120. throw new Error(
  121. `Unable to create repository, ${response.status} ${
  122. response.statusText
  123. }, ${await response.text()}`,
  124. );
  125. }
  126. const r = await response.json();
  127. let remoteUrl = '';
  128. for (const link of r.links.clone) {
  129. if (link.name === 'http') {
  130. remoteUrl = link.href;
  131. }
  132. }
  133. const repoContentsUrl = `${r.links.self[0].href}`;
  134. return { remoteUrl, repoContentsUrl };
  135. };
  136. const getAuthorizationHeader = (config: BitbucketIntegrationConfig) => {
  137. if (config.username && config.appPassword) {
  138. const buffer = Buffer.from(
  139. `${config.username}:${config.appPassword}`,
  140. 'utf8',
  141. );
  142. return `Basic ${buffer.toString('base64')}`;
  143. }
  144. if (config.token) {
  145. return `Bearer ${config.token}`;
  146. }
  147. throw new Error(
  148. `Authorization has not been provided for Bitbucket. Please add either username + appPassword or token to the Integrations config`,
  149. );
  150. };
  151. const performEnableLFS = async (opts: {
  152. authorization: string;
  153. host: string;
  154. project: string;
  155. repo: string;
  156. }) => {
  157. const { authorization, host, project, repo } = opts;
  158. const options: RequestInit = {
  159. method: 'PUT',
  160. headers: {
  161. Authorization: authorization,
  162. },
  163. };
  164. const { ok, status, statusText } = await fetch(
  165. `https://${host}/rest/git-lfs/admin/projects/${project}/repos/${repo}/enabled`,
  166. options,
  167. );
  168. if (!ok)
  169. throw new Error(
  170. `Failed to enable LFS in the repository, ${status}: ${statusText}`,
  171. );
  172. };
  173. export function createPublishBitbucketAction(options: {
  174. integrations: ScmIntegrationRegistry;
  175. config: Config;
  176. }) {
  177. const { integrations, config } = options;
  178. return createTemplateAction<{
  179. repoUrl: string;
  180. description: string;
  181. defaultBranch?: string;
  182. repoVisibility: 'private' | 'public';
  183. sourcePath?: string;
  184. enableLFS: boolean;
  185. }>({
  186. id: 'publish:bitbucket',
  187. description:
  188. 'Initializes a git repository of the content in the workspace, and publishes it to Bitbucket.',
  189. schema: {
  190. input: {
  191. type: 'object',
  192. required: ['repoUrl'],
  193. properties: {
  194. repoUrl: {
  195. title: 'Repository Location',
  196. type: 'string',
  197. },
  198. description: {
  199. title: 'Repository Description',
  200. type: 'string',
  201. },
  202. repoVisibility: {
  203. title: 'Repository Visibility',
  204. type: 'string',
  205. enum: ['private', 'public'],
  206. },
  207. defaultBranch: {
  208. title: 'Default Branch',
  209. type: 'string',
  210. description: `Sets the default branch on the repository. The default value is 'master'`,
  211. },
  212. sourcePath: {
  213. title:
  214. 'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.',
  215. type: 'string',
  216. },
  217. enableLFS: {
  218. title:
  219. 'Enable LFS for the repository. Only available for hosted Bitbucket.',
  220. type: 'boolean',
  221. },
  222. },
  223. },
  224. output: {
  225. type: 'object',
  226. properties: {
  227. remoteUrl: {
  228. title: 'A URL to the repository with the provider',
  229. type: 'string',
  230. },
  231. repoContentsUrl: {
  232. title: 'A URL to the root of the repository',
  233. type: 'string',
  234. },
  235. },
  236. },
  237. },
  238. async handler(ctx) {
  239. const {
  240. repoUrl,
  241. description,
  242. defaultBranch = 'master',
  243. repoVisibility = 'private',
  244. enableLFS = false,
  245. } = ctx.input;
  246. const { workspace, project, repo, host } = parseRepoUrl(
  247. repoUrl,
  248. integrations,
  249. );
  250. // Workspace is only required for bitbucket cloud
  251. if (host === 'bitbucket.org') {
  252. if (!workspace) {
  253. throw new InputError(
  254. `Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing workspace`,
  255. );
  256. }
  257. }
  258. // Project is required for both bitbucket cloud and bitbucket server
  259. if (!project) {
  260. throw new InputError(
  261. `Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing project`,
  262. );
  263. }
  264. const integrationConfig = integrations.bitbucket.byHost(host);
  265. if (!integrationConfig) {
  266. throw new InputError(
  267. `No matching integration configuration for host ${host}, please check your integrations config`,
  268. );
  269. }
  270. const authorization = getAuthorizationHeader(integrationConfig.config);
  271. const apiBaseUrl = integrationConfig.config.apiBaseUrl;
  272. const createMethod =
  273. host === 'bitbucket.org'
  274. ? createBitbucketCloudRepository
  275. : createBitbucketServerRepository;
  276. const { remoteUrl, repoContentsUrl } = await createMethod({
  277. authorization,
  278. host,
  279. workspace: workspace || '',
  280. project,
  281. repo,
  282. repoVisibility,
  283. description,
  284. apiBaseUrl,
  285. });
  286. const gitAuthorInfo = {
  287. name: config.getOptionalString('scaffolder.defaultAuthor.name'),
  288. email: config.getOptionalString('scaffolder.defaultAuthor.email'),
  289. };
  290. await initRepoAndPush({
  291. dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath),
  292. remoteUrl,
  293. auth: {
  294. username: integrationConfig.config.username
  295. ? integrationConfig.config.username
  296. : 'x-token-auth',
  297. password: integrationConfig.config.appPassword
  298. ? integrationConfig.config.appPassword
  299. : integrationConfig.config.token ?? '',
  300. },
  301. defaultBranch,
  302. logger: ctx.logger,
  303. commitMessage: config.getOptionalString(
  304. 'scaffolder.defaultCommitMessage',
  305. ),
  306. gitAuthorInfo,
  307. });
  308. if (enableLFS && host !== 'bitbucket.org') {
  309. await performEnableLFS({ authorization, host, project, repo });
  310. }
  311. ctx.output('remoteUrl', remoteUrl);
  312. ctx.output('repoContentsUrl', repoContentsUrl);
  313. },
  314. });
  315. }