Integration - Removed long deprecated code
Signed-off-by: Andre Wanlin <awanlin@spotify.com> Fixed lock file Signed-off-by: Andre Wanlin <awanlin@spotify.com> Improve changesets Signed-off-by: Andre Wanlin <awanlin@spotify.com> Removed link Signed-off-by: Andre Wanlin <awanlin@spotify.com> Update .changeset/sharp-ravens-shop.md Co-authored-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Update .changeset/six-trees-carry.md Co-authored-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Update .changeset/tiny-zoos-smash.md Co-authored-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Corrected gerrit changes based on feedback Signed-off-by: Andre Wanlin <awanlin@spotify.com> Updated API report Signed-off-by: Andre Wanlin <awanlin@spotify.com> Fixed some tests Signed-off-by: Andre Wanlin <awanlin@spotify.com> Fixed test Signed-off-by: Andre Wanlin <awanlin@spotify.com> Fixed another test Signed-off-by: Andre Wanlin <awanlin@spotify.com> Removed parseGerritGitilesUrl Signed-off-by: Andre Wanlin <awanlin@spotify.com> Table clean up Signed-off-by: Andre Wanlin <awanlin@spotify.com> Remove from changeset Signed-off-by: Andre Wanlin <awanlin@spotify.com> Changes based on feedback Signed-off-by: Andre Wanlin <awanlin@spotify.com>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
---
|
||||
'@backstage/integration': major
|
||||
---
|
||||
|
||||
**BREAKING** Removed deprecated Azure DevOps, Bitbucket, Gerrit and GitHub code:
|
||||
|
||||
- For Azure Devops, the long deprecated `token` string and `credential` object have been removed from the `config.d.ts`. Use the `credentials` array object instead.
|
||||
- For Bitbucket, the long deprecated `bitbucket` object has been removed from the `config.d.ts`. Use the `bitbucketCloud` or `bitbucketServer` objects instead.
|
||||
- For Gerrit, the `parseGerritGitilesUrl` function has been removed, use `parseGitilesUrlRef` instead. The `buildGerritGitilesArchiveUrl` function has also been removed, use `buildGerritGitilesArchiveUrlFromLocation` instead.
|
||||
- For GitHub, the `getGitHubRequestOptions` function has been removed.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-defaults': minor
|
||||
---
|
||||
|
||||
**BREAKING** Removed deprecated `BitbucketUrlReader`. Use the `BitbucketCloudUrlReader` or the `BitbucketServerUrlReader` instead.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-common': major
|
||||
---
|
||||
|
||||
**BREAKING** Removed deprecated `bitbucket` integration from being registered in the `ScaffolderClient`. Use the `bitbucketCloud` or `bitbucketServer` integrations instead.
|
||||
@@ -1340,105 +1340,3 @@ const backend = createBackend();
|
||||
backend.add(import('@backstage/plugin-kubernetes-backend'));
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
### The Plugins in Backstage Repo
|
||||
|
||||
The vast majority of the backend plugins that currently live in the Backstage Repo have been migrated and their respective `README`s have details on how they should be installed using the New Backend System.
|
||||
|
||||
| Package | Role | Migrated | Uses Alpha Export | Link to `README` |
|
||||
| ------------------------------------------------------------------ | --------------------- | -------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| @backstage-community/plugin-adr-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/adr/plugins/adr-backend/README.md) |
|
||||
| @backstage-community/plugin-airbrake-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/airbrake/plugins/airbrake-backend/README.md) |
|
||||
| @backstage/plugin-app-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/app-backend/README.md) |
|
||||
| @backstage/plugin-auth-backend | backend-plugin | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend/README.md) |
|
||||
| @backstage/plugin-auth-backend-module-atlassian-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-atlassian-provider/README.md) |
|
||||
| @backstage/plugin-auth-backend-module-aws-alb-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-aws-alb-provider/README.md) |
|
||||
| @backstage/plugin-auth-backend-module-gcp-iap-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-gcp-iap-provider/README.md) |
|
||||
| @backstage/plugin-auth-backend-module-github-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-github-provider/README.md) |
|
||||
| @backstage/plugin-auth-backend-module-gitlab-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-gitlab-provider/README.md) |
|
||||
| @backstage/plugin-auth-backend-module-google-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-google-provider/README.md) |
|
||||
| @backstage/plugin-auth-backend-module-guest-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-guest-provider/README.md) |
|
||||
| @backstage/plugin-auth-backend-module-microsoft-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-microsoft-provider/README.md) |
|
||||
| @backstage/plugin-auth-backend-module-oauth2-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-oauth2-provider/README.md) |
|
||||
| @backstage/plugin-auth-backend-module-oauth2-proxy-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-oauth2-proxy-provider/README.md) |
|
||||
| @backstage/plugin-auth-backend-module-oidc-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-oidc-provider/README.md) |
|
||||
| @backstage/plugin-auth-backend-module-okta-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-okta-provider/README.md) |
|
||||
| @backstage/plugin-auth-backend-module-pinniped-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-pinniped-provider/README.md) |
|
||||
| @backstage/plugin-auth-backend-module-vmware-cloud-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-vmware-cloud-provider/README.md) |
|
||||
| @backstage-community/plugin-azure-devops-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/azure-devops/plugins/azure-devops-backend/README.md) |
|
||||
| @backstage-community/plugin-azure-sites-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/azure-sites/plugins/azure-sites-backend/README.md) |
|
||||
| @backstage-community/plugin-badges-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/badges/plugins/badges-backend/README.md) |
|
||||
| @backstage-community/plugin-bazaar-backend | backend-plugin | true | true | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/bazaar/plugins/bazaar-backend/README.md) |
|
||||
| @backstage/plugin-catalog-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-aws | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-aws/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-azure | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-azure/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-backstage-openapi | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-backstage-openapi/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-bitbucket-cloud | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-bitbucket-cloud/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-bitbucket-server | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-bitbucket-server/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-gcp | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-gcp/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-gerrit | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-gerrit/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-github | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-github/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-github-org | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-github-org/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-gitlab | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-gitlab/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-incremental-ingestion | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-incremental-ingestion/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-ldap | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-ldap/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-msgraph | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-msgraph/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-openapi | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-openapi/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-puppetdb | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-puppetdb/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-scaffolder-entity-model | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-scaffolder-entity-model/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-unprocessed | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-unprocessed/README.md) |
|
||||
| @backstage-community/plugin-code-coverage-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/code-coverage/plugins/code-coverage-backend/README.md) |
|
||||
| @backstage/plugin-devtools-backend | backend-plugin | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/devtools-backend/README.md) |
|
||||
| @backstage-community/plugin-entity-feedback-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/entity-feedback/plugins/entity-feedback-backend/README.md) |
|
||||
| @backstage/plugin-events-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/events-backend/README.md) |
|
||||
| @backstage/plugin-events-backend-module-aws-sqs | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-aws-sqs/README.md) |
|
||||
| @backstage/plugin-events-backend-module-azure | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-azure/README.md) |
|
||||
| @backstage/plugin-events-backend-module-bitbucket-cloud | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-bitbucket-cloud/README.md) |
|
||||
| @backstage/plugin-events-backend-module-gerrit | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-gerrit/README.md) |
|
||||
| @backstage/plugin-events-backend-module-github | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-github/README.md) |
|
||||
| @backstage/plugin-events-backend-module-gitlab | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-gitlab/README.md) |
|
||||
| @internal/plugin-todo-list-backend | backend-plugin | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/example-todo-list-backend/README.md) |
|
||||
| @backstage-community/plugin-explore-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/explore/plugins/explore-backend/README.md) |
|
||||
| @backstage-community/plugin-jenkins-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/jenkins/plugins/jenkins-backend/README.md) |
|
||||
| @backstage-community/plugin-kafka-backend | backend-plugin | true | true | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/kafka/plugins/kafka-backend/README.md) |
|
||||
| @backstage/plugin-kubernetes-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/kubernetes-backend/README.md) |
|
||||
| @backstage-community/plugin-lighthouse-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/lighthouse/plugins/lighthouse-backend/README.md) |
|
||||
| @backstage-community/plugin-linguist-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/linguist/plugins/linguist-backend/README.md) |
|
||||
| @backstage-community/plugin-nomad-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/nomad/plugins/nomad-backend/README.md) |
|
||||
| @backstage/plugin-notifications-backend | backend-plugin | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/notifications-backend/README.md) |
|
||||
| @backstage-community/plugin-periskop-backend | backend-plugin | true | true | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/periskop/plugins/periskop-backend/README.md) |
|
||||
| @backstage/plugin-permission-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/permission-backend/README.md) |
|
||||
| @backstage/plugin-permission-backend-module-allow-all-policy | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/permission-backend-module-policy-allow-all/README.md) |
|
||||
| @backstage-community/plugin-playlist-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/playlist/plugins/playlist-backend/README.md) |
|
||||
| @backstage/plugin-proxy-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/proxy-backend/README.md) |
|
||||
| @backstage-community/plugin-rollbar-backend | backend-plugin | | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/rollbar/plugins/rollbar-backend/README.md) |
|
||||
| @backstage/plugin-scaffolder-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/README.md) |
|
||||
| @backstage/plugin-scaffolder-backend-module-azure | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-azure/README.md) |
|
||||
| @backstage/plugin-scaffolder-backend-module-bitbucket | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-bitbucket/README.md) |
|
||||
| @backstage/plugin-scaffolder-backend-module-bitbucket-cloud | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-bitbucket-cloud/README.md) |
|
||||
| @backstage/plugin-scaffolder-backend-module-bitbucket-server | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-bitbucket-server/README.md) |
|
||||
| @backstage/plugin-scaffolder-backend-module-confluence-to-markdown | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-confluence-to-markdown/README.md) |
|
||||
| @backstage/plugin-scaffolder-backend-module-cookiecutter | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-cookiecutter/README.md) |
|
||||
| @backstage/plugin-scaffolder-backend-module-gerrit | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-gerrit/README.md) |
|
||||
| @backstage/plugin-scaffolder-backend-module-gitea | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-gitea/README.md) |
|
||||
| @backstage/plugin-scaffolder-backend-module-github | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-github/README.md) |
|
||||
| @backstage/plugin-scaffolder-backend-module-gitlab | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-gitlab/README.md) |
|
||||
| @backstage/plugin-scaffolder-backend-module-rails | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-rails/README.md) |
|
||||
| @backstage/plugin-scaffolder-backend-module-sentry | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-sentry/README.md) |
|
||||
| @backstage/plugin-scaffolder-backend-module-yeoman | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-yeoman/README.md) |
|
||||
| @backstage/plugin-search-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/search-backend/README.md) |
|
||||
| @backstage/plugin-search-backend-module-catalog | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-catalog/README.md) |
|
||||
| @backstage/plugin-search-backend-module-elasticsearch | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-elasticsearch/README.md) |
|
||||
| @backstage/plugin-search-backend-module-explore | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-explore/README.md) |
|
||||
| @backstage/plugin-search-backend-module-pg | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-pg/README.md) |
|
||||
| @backstage/plugin-search-backend-module-stack-overflow-collator | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow-collator/README.md) |
|
||||
| @backstage/plugin-search-backend-module-techdocs | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-techdocs/README.md) |
|
||||
| @backstage/plugin-signals-backend | backend-plugin | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/signals-backend/README.md) |
|
||||
| @backstage-community/plugin-sonarqube-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/sonarqube/plugins/sonarqube-backend/README.md) |
|
||||
| @backstage-community/plugin-stack-overflow-backend | backend-plugin | | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/stack-overflow/plugins/stack-overflow-backend/README.md) |
|
||||
| @backstage-community/plugin-tech-insights-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/tech-insights/plugins/tech-insights-backend/README.md) |
|
||||
| @backstage-community/plugin-tech-insights-backend-module-jsonfc | backend-plugin-module | true | | [README](https://github.com/backstage/community-plugins/blob/main/workspaces/tech-insights/plugins/tech-insights-backend-module-jsonfc/README.md) |
|
||||
| @backstage/plugin-techdocs-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend/README.md) |
|
||||
| @backstage-community/plugin-todo-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/todo/plugins/todo-backend/README.md) |
|
||||
| @backstage/plugin-user-settings-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/user-settings-backend/README.md) |
|
||||
| @backstage-community/plugin-vault-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/vault/plugins/vault-backend/README.md) |
|
||||
|
||||
@@ -10,7 +10,6 @@ import { AzureCredentialsManager } from '@backstage/integration';
|
||||
import { AzureDevOpsCredentialsProvider } from '@backstage/integration';
|
||||
import { AzureIntegration } from '@backstage/integration';
|
||||
import { BitbucketCloudIntegration } from '@backstage/integration';
|
||||
import { BitbucketIntegration } from '@backstage/integration';
|
||||
import { BitbucketServerIntegration } from '@backstage/integration';
|
||||
import { Config } from '@backstage/config';
|
||||
import { GerritIntegration } from '@backstage/integration';
|
||||
@@ -190,38 +189,6 @@ export class BitbucketServerUrlReader implements UrlReaderService {
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
// @public @deprecated
|
||||
export class BitbucketUrlReader implements UrlReaderService {
|
||||
constructor(
|
||||
integration: BitbucketIntegration,
|
||||
logger: LoggerService,
|
||||
deps: {
|
||||
treeResponseFactory: ReadTreeResponseFactory;
|
||||
},
|
||||
);
|
||||
// (undocumented)
|
||||
static factory: ReaderFactory;
|
||||
// (undocumented)
|
||||
read(url: string): Promise<Buffer>;
|
||||
// (undocumented)
|
||||
readTree(
|
||||
url: string,
|
||||
options?: UrlReaderServiceReadTreeOptions,
|
||||
): Promise<UrlReaderServiceReadTreeResponse>;
|
||||
// (undocumented)
|
||||
readUrl(
|
||||
url: string,
|
||||
options?: UrlReaderServiceReadUrlOptions,
|
||||
): Promise<UrlReaderServiceReadUrlResponse>;
|
||||
// (undocumented)
|
||||
search(
|
||||
url: string,
|
||||
options?: UrlReaderServiceSearchOptions,
|
||||
): Promise<UrlReaderServiceSearchResponse>;
|
||||
// (undocumented)
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class FetchUrlReader implements UrlReaderService {
|
||||
static factory: ReaderFactory;
|
||||
|
||||
@@ -1,656 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
BitbucketIntegration,
|
||||
readBitbucketIntegrationConfig,
|
||||
} from '@backstage/integration';
|
||||
import {
|
||||
createMockDirectory,
|
||||
mockServices,
|
||||
registerMswTestHooks,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import fs from 'fs-extra';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import path from 'node:path';
|
||||
import { NotModifiedError } from '@backstage/errors';
|
||||
import { BitbucketUrlReader } from './BitbucketUrlReader';
|
||||
import { DefaultReadTreeResponseFactory } from './tree';
|
||||
import getRawBody from 'raw-body';
|
||||
import { UrlReaderServiceReadUrlResponse } from '@backstage/backend-plugin-api';
|
||||
|
||||
const logger = mockServices.logger.mock();
|
||||
|
||||
describe('BitbucketUrlReader.factory', () => {
|
||||
it('only apply integration configs not inherited from bitbucketCloud or bitbucketServer', () => {
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
bitbucket: [],
|
||||
bitbucketCloud: [
|
||||
{
|
||||
username: 'username',
|
||||
appPassword: 'password',
|
||||
},
|
||||
],
|
||||
bitbucketServer: [
|
||||
{
|
||||
host: 'bitbucket-server.local',
|
||||
token: 'test-token',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
|
||||
config: config,
|
||||
});
|
||||
|
||||
const tuples = BitbucketUrlReader.factory({
|
||||
config,
|
||||
logger,
|
||||
treeResponseFactory,
|
||||
});
|
||||
|
||||
expect(tuples).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BitbucketUrlReader', () => {
|
||||
const mockDir = createMockDirectory({ mockOsTmpDir: true });
|
||||
|
||||
beforeEach(mockDir.clear);
|
||||
|
||||
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
|
||||
config: new ConfigReader({}),
|
||||
});
|
||||
|
||||
const bitbucketProcessor = new BitbucketUrlReader(
|
||||
new BitbucketIntegration(
|
||||
readBitbucketIntegrationConfig(
|
||||
new ConfigReader({
|
||||
host: 'bitbucket.org',
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
}),
|
||||
),
|
||||
),
|
||||
logger,
|
||||
{ treeResponseFactory },
|
||||
);
|
||||
|
||||
const hostedBitbucketProcessor = new BitbucketUrlReader(
|
||||
new BitbucketIntegration(
|
||||
readBitbucketIntegrationConfig(
|
||||
new ConfigReader({
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
|
||||
}),
|
||||
),
|
||||
),
|
||||
logger,
|
||||
{ treeResponseFactory },
|
||||
);
|
||||
|
||||
const worker = setupServer();
|
||||
registerMswTestHooks(worker);
|
||||
|
||||
describe('readUrl', () => {
|
||||
it('should be able to readUrl via buffer without ETag', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('If-None-Match')).toBeNull();
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.body('foo'),
|
||||
ctx.set('ETag', 'etag-value'),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const result = await bitbucketProcessor.readUrl(
|
||||
'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml',
|
||||
);
|
||||
const buffer = await result.buffer();
|
||||
expect(buffer.toString()).toBe('foo');
|
||||
});
|
||||
|
||||
it('should be able to readUrl using provided token', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('authorization')).toBe(
|
||||
'Bearer manual-token',
|
||||
);
|
||||
return res(ctx.status(200), ctx.body('foo'));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const result = await bitbucketProcessor.readUrl(
|
||||
'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml',
|
||||
{ token: 'manual-token' },
|
||||
);
|
||||
const buffer = await result.buffer();
|
||||
expect(buffer.toString()).toBe('foo');
|
||||
});
|
||||
|
||||
it('should be able to readUrl via stream without ETag', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('If-None-Match')).toBeNull();
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.body('foo'),
|
||||
ctx.set('ETag', 'etag-value'),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const result = await bitbucketProcessor.readUrl(
|
||||
'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml',
|
||||
);
|
||||
const fromStream = await getRawBody(result.stream!());
|
||||
expect(fromStream.toString()).toBe('foo');
|
||||
});
|
||||
|
||||
it('should be able to readUrl with matching ETag', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('If-None-Match')).toBe(
|
||||
'matching-etag-value',
|
||||
);
|
||||
return res(ctx.status(304));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
bitbucketProcessor.readUrl(
|
||||
'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml',
|
||||
{ etag: 'matching-etag-value' },
|
||||
),
|
||||
).rejects.toThrow(NotModifiedError);
|
||||
});
|
||||
|
||||
it('should be able to readUrl without matching ETag', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('If-None-Match')).toBe(
|
||||
'previous-etag-value',
|
||||
);
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.body('foo'),
|
||||
ctx.set('ETag', 'new-etag-value'),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const result = await bitbucketProcessor.readUrl(
|
||||
'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml',
|
||||
{ etag: 'previous-etag-value' },
|
||||
);
|
||||
const buffer = await result.buffer();
|
||||
expect(buffer.toString()).toBe('foo');
|
||||
expect(result.etag).toBe('new-etag-value');
|
||||
});
|
||||
|
||||
it('should be able to readUrl via buffer without If-Modified-Since', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('If-None-Match')).toBeNull();
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.body('foo'),
|
||||
ctx.set('ETag', 'etag-value'),
|
||||
ctx.set(
|
||||
'Last-Modified',
|
||||
new Date('2020-01-01T00:00:00Z').toUTCString(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const result = await bitbucketProcessor.readUrl(
|
||||
'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml',
|
||||
);
|
||||
const buffer = await result.buffer();
|
||||
expect(result.lastModifiedAt).toEqual(new Date('2020-01-01T00:00:00Z'));
|
||||
expect(buffer.toString()).toBe('foo');
|
||||
});
|
||||
|
||||
it('should be throw not modified when If-Modified-Since returns a 304', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('If-Modified-Since')).toBe(
|
||||
new Date('1999 12 31 23:59:59 GMT').toUTCString(),
|
||||
);
|
||||
return res(ctx.status(304));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
bitbucketProcessor.readUrl(
|
||||
'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml',
|
||||
{ lastModifiedAfter: new Date('1999 12 31 23:59:59 GMT') },
|
||||
),
|
||||
).rejects.toThrow(NotModifiedError);
|
||||
});
|
||||
|
||||
it('should be able to readUrl when If-Modified-Since is before Last-Modified', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('If-Modified-Since')).toBe(
|
||||
new Date('1999 12 31 23:59:59 GMT').toUTCString(),
|
||||
);
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.set(
|
||||
'Last-Modified',
|
||||
new Date('2020-01-01T00:00:00Z').toUTCString(),
|
||||
),
|
||||
ctx.body('foo'),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const result = await bitbucketProcessor.readUrl(
|
||||
'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml',
|
||||
{ lastModifiedAfter: new Date('1999 12 31 23:59:59 GMT') },
|
||||
);
|
||||
const buffer = await result.buffer();
|
||||
expect(buffer.toString()).toBe('foo');
|
||||
expect(result.lastModifiedAt).toEqual(new Date('2020-01-01T00:00:00Z'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('read', () => {
|
||||
it('rejects unknown targets', async () => {
|
||||
await expect(
|
||||
bitbucketProcessor.read('https://not.bitbucket.com/apa'),
|
||||
).rejects.toThrow(
|
||||
'Incorrect URL: https://not.bitbucket.com/apa, Error: Invalid Bitbucket URL or file path',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readTree', () => {
|
||||
const repoBuffer = fs.readFileSync(
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'__fixtures__/bitbucket-repo-with-commit-hash.tar.gz',
|
||||
),
|
||||
);
|
||||
|
||||
const privateBitbucketRepoBuffer = fs.readFileSync(
|
||||
path.resolve(__dirname, '__fixtures__/bitbucket-server-repo.tar.gz'),
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage/mock',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
mainbranch: {
|
||||
type: 'branch',
|
||||
name: 'master',
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://bitbucket.org/backstage/mock/get/master.tar.gz',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/zip'),
|
||||
ctx.set(
|
||||
'content-disposition',
|
||||
'attachment; filename=backstage-mock-12ab34cd56ef.tar.gz',
|
||||
),
|
||||
ctx.body(new Uint8Array(repoBuffer)),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage/mock/commits/master',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
values: [{ hash: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }],
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/zip'),
|
||||
ctx.set(
|
||||
'content-disposition',
|
||||
'attachment; filename=backstage-mock.tgz',
|
||||
),
|
||||
ctx.body(new Uint8Array(privateBitbucketRepoBuffer)),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/commits',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
values: [{ id: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }],
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the wanted files from an archive', async () => {
|
||||
const response = await bitbucketProcessor.readTree(
|
||||
'https://bitbucket.org/backstage/mock/src/master',
|
||||
);
|
||||
|
||||
expect(response.etag).toBe('12ab34cd56ef');
|
||||
|
||||
const files = await response.files();
|
||||
|
||||
expect(files.length).toBe(2);
|
||||
const mkDocsFile = await files[0].content();
|
||||
const indexMarkdownFile = await files[1].content();
|
||||
|
||||
expect(indexMarkdownFile.toString()).toBe('# Test\n');
|
||||
expect(mkDocsFile.toString()).toBe('site_name: Test\n');
|
||||
});
|
||||
|
||||
it('creates a directory with the wanted files', async () => {
|
||||
const response = await bitbucketProcessor.readTree(
|
||||
'https://bitbucket.org/backstage/mock',
|
||||
);
|
||||
|
||||
const dir = await response.dir({ targetDir: mockDir.path });
|
||||
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'),
|
||||
).resolves.toBe('site_name: Test\n');
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'docs', 'index.md'), 'utf8'),
|
||||
).resolves.toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('uses private bitbucket host', async () => {
|
||||
const response = await hostedBitbucketProcessor.readTree(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs?at=some-branch',
|
||||
);
|
||||
|
||||
expect(response.etag).toBe('12ab34cd56ef');
|
||||
|
||||
const files = await response.files();
|
||||
|
||||
expect(files.length).toBe(1);
|
||||
const indexMarkdownFile = await files[0].content();
|
||||
|
||||
expect(indexMarkdownFile.toString()).toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('returns the wanted files from an archive with a subpath', async () => {
|
||||
const response = await bitbucketProcessor.readTree(
|
||||
'https://bitbucket.org/backstage/mock/src/master/docs',
|
||||
);
|
||||
|
||||
expect(response.etag).toBe('12ab34cd56ef');
|
||||
|
||||
const files = await response.files();
|
||||
|
||||
expect(files.length).toBe(1);
|
||||
const indexMarkdownFile = await files[0].content();
|
||||
|
||||
expect(indexMarkdownFile.toString()).toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('creates a directory with the wanted files with a subpath', async () => {
|
||||
const response = await bitbucketProcessor.readTree(
|
||||
'https://bitbucket.org/backstage/mock/src/master/docs',
|
||||
);
|
||||
|
||||
const dir = await response.dir({ targetDir: mockDir.path });
|
||||
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'index.md'), 'utf8'),
|
||||
).resolves.toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('throws a NotModifiedError when given a etag in options', async () => {
|
||||
const fnBitbucket = async () => {
|
||||
await bitbucketProcessor.readTree(
|
||||
'https://bitbucket.org/backstage/mock',
|
||||
{ etag: '12ab34cd56ef' },
|
||||
);
|
||||
};
|
||||
|
||||
await expect(fnBitbucket).rejects.toThrow(NotModifiedError);
|
||||
});
|
||||
|
||||
it('should not throw a NotModifiedError when given an outdated etag in options', async () => {
|
||||
const response = await bitbucketProcessor.readTree(
|
||||
'https://bitbucket.org/backstage/mock',
|
||||
{ etag: 'outdatedetag123abc' },
|
||||
);
|
||||
|
||||
expect(response.etag).toBe('12ab34cd56ef');
|
||||
});
|
||||
});
|
||||
|
||||
describe('search hosted', () => {
|
||||
const repoBuffer = fs.readFileSync(
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'__fixtures__/bitbucket-repo-with-commit-hash.tar.gz',
|
||||
),
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage/mock',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
mainbranch: {
|
||||
type: 'branch',
|
||||
name: 'master',
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://bitbucket.org/backstage/mock/get/master.tar.gz',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/zip'),
|
||||
ctx.set(
|
||||
'content-disposition',
|
||||
'attachment; filename=backstage-mock-12ab34cd56ef.tar.gz',
|
||||
),
|
||||
ctx.body(new Uint8Array(repoBuffer)),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage/mock/commits/master',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
values: [{ hash: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }],
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('works for the naive case', async () => {
|
||||
const result = await bitbucketProcessor.search(
|
||||
'https://bitbucket.org/backstage/mock/src/master/**/index.*',
|
||||
);
|
||||
expect(result.etag).toBe('12ab34cd56ef');
|
||||
expect(result.files.length).toBe(1);
|
||||
expect(result.files[0].url).toBe(
|
||||
'https://bitbucket.org/backstage/mock/src/master/docs/index.md',
|
||||
);
|
||||
await expect(result.files[0].content()).resolves.toEqual(
|
||||
Buffer.from('# Test\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('works in nested folders', async () => {
|
||||
const result = await bitbucketProcessor.search(
|
||||
'https://bitbucket.org/backstage/mock/src/master/docs/index.*',
|
||||
);
|
||||
expect(result.etag).toBe('12ab34cd56ef');
|
||||
expect(result.files.length).toBe(1);
|
||||
expect(result.files[0].url).toBe(
|
||||
'https://bitbucket.org/backstage/mock/src/master/docs/index.md',
|
||||
);
|
||||
await expect(result.files[0].content()).resolves.toEqual(
|
||||
Buffer.from('# Test\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws NotModifiedError when same etag', async () => {
|
||||
await expect(
|
||||
bitbucketProcessor.search(
|
||||
'https://bitbucket.org/backstage/mock/src/master/**/index.*',
|
||||
{ etag: '12ab34cd56ef' },
|
||||
),
|
||||
).rejects.toThrow(NotModifiedError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('search private', () => {
|
||||
const privateBitbucketRepoBuffer = fs.readFileSync(
|
||||
path.resolve(__dirname, '__fixtures__/bitbucket-server-repo.tar.gz'),
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/zip'),
|
||||
ctx.set(
|
||||
'content-disposition',
|
||||
'attachment; filename=backstage-mock.tgz',
|
||||
),
|
||||
ctx.body(new Uint8Array(privateBitbucketRepoBuffer)),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/commits',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
values: [{ id: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }],
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('works for the naive case', async () => {
|
||||
const result = await hostedBitbucketProcessor.search(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/**/index.*?at=master',
|
||||
);
|
||||
expect(result.etag).toBe('12ab34cd56ef');
|
||||
expect(result.files.length).toBe(1);
|
||||
expect(result.files[0].url).toBe(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.md?at=master',
|
||||
);
|
||||
await expect(result.files[0].content()).resolves.toEqual(
|
||||
Buffer.from('# Test\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('works in nested folders', async () => {
|
||||
const result = await hostedBitbucketProcessor.search(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.*?at=master',
|
||||
);
|
||||
expect(result.etag).toBe('12ab34cd56ef');
|
||||
expect(result.files.length).toBe(1);
|
||||
expect(result.files[0].url).toBe(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.md?at=master',
|
||||
);
|
||||
await expect(result.files[0].content()).resolves.toEqual(
|
||||
Buffer.from('# Test\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws NotModifiedError when same etag', async () => {
|
||||
await expect(
|
||||
hostedBitbucketProcessor.search(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/**/index.*?at=master',
|
||||
{ etag: '12ab34cd56ef' },
|
||||
),
|
||||
).rejects.toThrow(NotModifiedError);
|
||||
});
|
||||
|
||||
it('should work for exact URLs', async () => {
|
||||
hostedBitbucketProcessor.readUrl = jest.fn().mockResolvedValue({
|
||||
buffer: async () => Buffer.from('content'),
|
||||
etag: 'etag',
|
||||
} as UrlReaderServiceReadUrlResponse);
|
||||
|
||||
const result = await hostedBitbucketProcessor.search(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.md?at=master',
|
||||
);
|
||||
expect(result.etag).toBe('etag');
|
||||
expect(result.files.length).toBe(1);
|
||||
expect(result.files[0].url).toBe(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.md?at=master',
|
||||
);
|
||||
expect((await result.files[0].content()).toString()).toEqual('content');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,313 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
UrlReaderService,
|
||||
UrlReaderServiceReadTreeOptions,
|
||||
UrlReaderServiceReadTreeResponse,
|
||||
UrlReaderServiceReadUrlOptions,
|
||||
UrlReaderServiceReadUrlResponse,
|
||||
UrlReaderServiceSearchOptions,
|
||||
UrlReaderServiceSearchResponse,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
assertError,
|
||||
NotFoundError,
|
||||
NotModifiedError,
|
||||
} from '@backstage/errors';
|
||||
import {
|
||||
BitbucketIntegration,
|
||||
getBitbucketDefaultBranch,
|
||||
getBitbucketDownloadUrl,
|
||||
getBitbucketFileFetchUrl,
|
||||
getBitbucketRequestOptions,
|
||||
ScmIntegrations,
|
||||
} from '@backstage/integration';
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import { trimEnd } from 'lodash';
|
||||
import { Minimatch } from 'minimatch';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { ReaderFactory, ReadTreeResponseFactory } from './types';
|
||||
import { ReadUrlResponseFactory } from './ReadUrlResponseFactory';
|
||||
|
||||
/**
|
||||
* Implements a {@link @backstage/backend-plugin-api#UrlReaderService} for files from Bitbucket v1 and v2 APIs, such
|
||||
* as the one exposed by Bitbucket Cloud itself.
|
||||
*
|
||||
* @public
|
||||
* @deprecated in favor of BitbucketCloudUrlReader and BitbucketServerUrlReader
|
||||
*/
|
||||
export class BitbucketUrlReader implements UrlReaderService {
|
||||
static factory: ReaderFactory = ({ config, logger, treeResponseFactory }) => {
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
return integrations.bitbucket
|
||||
.list()
|
||||
.filter(
|
||||
item =>
|
||||
!integrations.bitbucketCloud.byHost(item.config.host) &&
|
||||
!integrations.bitbucketServer.byHost(item.config.host),
|
||||
)
|
||||
.map(integration => {
|
||||
const reader = new BitbucketUrlReader(integration, logger, {
|
||||
treeResponseFactory,
|
||||
});
|
||||
const predicate = (url: URL) => url.host === integration.config.host;
|
||||
return { reader, predicate };
|
||||
});
|
||||
};
|
||||
|
||||
private readonly integration: BitbucketIntegration;
|
||||
private readonly deps: { treeResponseFactory: ReadTreeResponseFactory };
|
||||
|
||||
constructor(
|
||||
integration: BitbucketIntegration,
|
||||
logger: LoggerService,
|
||||
deps: { treeResponseFactory: ReadTreeResponseFactory },
|
||||
) {
|
||||
this.integration = integration;
|
||||
this.deps = deps;
|
||||
const { host, token, username, appPassword } = integration.config;
|
||||
const replacement =
|
||||
host === 'bitbucket.org' ? 'bitbucketCloud' : 'bitbucketServer';
|
||||
logger.warn(
|
||||
`[Deprecated] Please migrate from "integrations.bitbucket" to "integrations.${replacement}".`,
|
||||
);
|
||||
|
||||
if (!token && username && !appPassword) {
|
||||
throw new Error(
|
||||
`Bitbucket integration for '${host}' has configured a username but is missing a required appPassword.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async read(url: string): Promise<Buffer> {
|
||||
const response = await this.readUrl(url);
|
||||
return response.buffer();
|
||||
}
|
||||
|
||||
private getCredentials = async (options?: {
|
||||
token?: string;
|
||||
}): Promise<{ headers: Record<string, string> }> => {
|
||||
if (options?.token) {
|
||||
return {
|
||||
headers: {
|
||||
Authorization: `Bearer ${options.token}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return await getBitbucketRequestOptions(this.integration.config);
|
||||
};
|
||||
|
||||
async readUrl(
|
||||
url: string,
|
||||
options?: UrlReaderServiceReadUrlOptions,
|
||||
): Promise<UrlReaderServiceReadUrlResponse> {
|
||||
const { etag, lastModifiedAfter, signal } = options ?? {};
|
||||
const bitbucketUrl = getBitbucketFileFetchUrl(url, this.integration.config);
|
||||
const requestOptions = await this.getCredentials(options);
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(bitbucketUrl.toString(), {
|
||||
headers: {
|
||||
...requestOptions.headers,
|
||||
...(etag && { 'If-None-Match': etag }),
|
||||
...(lastModifiedAfter && {
|
||||
'If-Modified-Since': lastModifiedAfter.toUTCString(),
|
||||
}),
|
||||
},
|
||||
// TODO(freben): The signal cast is there because pre-3.x versions of
|
||||
// node-fetch have a very slightly deviating AbortSignal type signature.
|
||||
// The difference does not affect us in practice however. The cast can be
|
||||
// removed after we support ESM for CLI dependencies and migrate to
|
||||
// version 3 of node-fetch.
|
||||
// https://github.com/backstage/backstage/issues/8242
|
||||
...(signal && { signal: signal as any }),
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(`Unable to read ${url}, ${e}`);
|
||||
}
|
||||
|
||||
if (response.status === 304) {
|
||||
throw new NotModifiedError();
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
return ReadUrlResponseFactory.fromResponse(response);
|
||||
}
|
||||
|
||||
const message = `${url} could not be read as ${bitbucketUrl}, ${response.status} ${response.statusText}`;
|
||||
if (response.status === 404) {
|
||||
throw new NotFoundError(message);
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
async readTree(
|
||||
url: string,
|
||||
options?: UrlReaderServiceReadTreeOptions,
|
||||
): Promise<UrlReaderServiceReadTreeResponse> {
|
||||
const { filepath } = parseGitUrl(url);
|
||||
|
||||
const lastCommitShortHash = await this.getLastCommitShortHash(url);
|
||||
if (options?.etag && options.etag === lastCommitShortHash) {
|
||||
throw new NotModifiedError();
|
||||
}
|
||||
|
||||
const downloadUrl = await getBitbucketDownloadUrl(
|
||||
url,
|
||||
this.integration.config,
|
||||
);
|
||||
const archiveBitbucketResponse = await fetch(
|
||||
downloadUrl,
|
||||
getBitbucketRequestOptions(this.integration.config),
|
||||
);
|
||||
if (!archiveBitbucketResponse.ok) {
|
||||
const message = `Failed to read tree from ${url}, ${archiveBitbucketResponse.status} ${archiveBitbucketResponse.statusText}`;
|
||||
if (archiveBitbucketResponse.status === 404) {
|
||||
throw new NotFoundError(message);
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return await this.deps.treeResponseFactory.fromTarArchive({
|
||||
response: archiveBitbucketResponse,
|
||||
subpath: filepath,
|
||||
etag: lastCommitShortHash,
|
||||
filter: options?.filter,
|
||||
});
|
||||
}
|
||||
|
||||
async search(
|
||||
url: string,
|
||||
options?: UrlReaderServiceSearchOptions,
|
||||
): Promise<UrlReaderServiceSearchResponse> {
|
||||
const { filepath } = parseGitUrl(url);
|
||||
|
||||
// If it's a direct URL we use readUrl instead
|
||||
if (!filepath?.match(/[*?]/)) {
|
||||
try {
|
||||
const data = await this.readUrl(url, options);
|
||||
|
||||
return {
|
||||
files: [
|
||||
{
|
||||
url: url,
|
||||
content: data.buffer,
|
||||
lastModifiedAt: data.lastModifiedAt,
|
||||
},
|
||||
],
|
||||
etag: data.etag ?? '',
|
||||
};
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
if (error.name === 'NotFoundError') {
|
||||
return {
|
||||
files: [],
|
||||
etag: '',
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const matcher = new Minimatch(filepath);
|
||||
|
||||
// TODO(freben): For now, read the entire repo and filter through that. In
|
||||
// a future improvement, we could be smart and try to deduce that non-glob
|
||||
// prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used
|
||||
// to get just that part of the repo.
|
||||
const treeUrl = trimEnd(url.replace(filepath, ''), '/');
|
||||
|
||||
const tree = await this.readTree(treeUrl, {
|
||||
etag: options?.etag,
|
||||
filter: path => matcher.match(path),
|
||||
});
|
||||
const files = await tree.files();
|
||||
|
||||
return {
|
||||
etag: tree.etag,
|
||||
files: files.map(file => ({
|
||||
url: this.integration.resolveUrl({
|
||||
url: `/${file.path}`,
|
||||
base: url,
|
||||
}),
|
||||
content: file.content,
|
||||
lastModifiedAt: file.lastModifiedAt,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
toString() {
|
||||
const { host, token, username, appPassword } = this.integration.config;
|
||||
let authed = Boolean(token);
|
||||
if (!authed) {
|
||||
authed = Boolean(username && appPassword);
|
||||
}
|
||||
return `bitbucket{host=${host},authed=${authed}}`;
|
||||
}
|
||||
|
||||
private async getLastCommitShortHash(url: string): Promise<string> {
|
||||
const { resource, name: repoName, owner: project, ref } = parseGitUrl(url);
|
||||
|
||||
let branch = ref;
|
||||
if (!branch) {
|
||||
branch = await getBitbucketDefaultBranch(url, this.integration.config);
|
||||
}
|
||||
|
||||
const isHosted = resource === 'bitbucket.org';
|
||||
// Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp222
|
||||
const commitsApiUrl = isHosted
|
||||
? `${this.integration.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}`
|
||||
: `${this.integration.config.apiBaseUrl}/projects/${project}/repos/${repoName}/commits`;
|
||||
|
||||
const commitsResponse = await fetch(
|
||||
commitsApiUrl,
|
||||
getBitbucketRequestOptions(this.integration.config),
|
||||
);
|
||||
if (!commitsResponse.ok) {
|
||||
const message = `Failed to retrieve commits from ${commitsApiUrl}, ${commitsResponse.status} ${commitsResponse.statusText}`;
|
||||
if (commitsResponse.status === 404) {
|
||||
throw new NotFoundError(message);
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const commits = await commitsResponse.json();
|
||||
if (isHosted) {
|
||||
if (
|
||||
commits &&
|
||||
commits.values &&
|
||||
commits.values.length > 0 &&
|
||||
commits.values[0].hash
|
||||
) {
|
||||
return commits.values[0].hash.substring(0, 12);
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
commits &&
|
||||
commits.values &&
|
||||
commits.values.length > 0 &&
|
||||
commits.values[0].id
|
||||
) {
|
||||
return commits.values[0].id.substring(0, 12);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Failed to read response from ${commitsApiUrl}`);
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@ import { UrlReaderPredicateMux } from './UrlReaderPredicateMux';
|
||||
import { AzureUrlReader } from './AzureUrlReader';
|
||||
import { BitbucketCloudUrlReader } from './BitbucketCloudUrlReader';
|
||||
import { BitbucketServerUrlReader } from './BitbucketServerUrlReader';
|
||||
import { BitbucketUrlReader } from './BitbucketUrlReader';
|
||||
import { GerritUrlReader } from './GerritUrlReader';
|
||||
import { GithubUrlReader } from './GithubUrlReader';
|
||||
import { GitlabUrlReader } from './GitlabUrlReader';
|
||||
@@ -92,7 +91,6 @@ export class UrlReaders {
|
||||
AzureUrlReader.factory,
|
||||
BitbucketCloudUrlReader.factory,
|
||||
BitbucketServerUrlReader.factory,
|
||||
BitbucketUrlReader.factory,
|
||||
GerritUrlReader.factory,
|
||||
GithubUrlReader.factory,
|
||||
GiteaUrlReader.factory,
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
export { AzureUrlReader } from './AzureUrlReader';
|
||||
export { BitbucketCloudUrlReader } from './BitbucketCloudUrlReader';
|
||||
export { BitbucketUrlReader } from './BitbucketUrlReader';
|
||||
export { BitbucketServerUrlReader } from './BitbucketServerUrlReader';
|
||||
export { GerritUrlReader } from './GerritUrlReader';
|
||||
export { GithubUrlReader } from './GithubUrlReader';
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import { ScmIntegration, ScmIntegrationsGroup } from '@backstage/integration';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { scmIntegrationsApiRef } from '../src/ScmIntegrationsApi';
|
||||
import { scmIntegrationsApiRef } from '../src/api/ScmIntegrationsApi';
|
||||
|
||||
import { Content } from '@backstage/core-components';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
@@ -48,10 +48,6 @@ export const DevPage = () => {
|
||||
Azure
|
||||
</Typography>
|
||||
<Integrations group={integrations.azure} />
|
||||
<Typography paragraph variant="h2">
|
||||
Bitbucket
|
||||
</Typography>
|
||||
<Integrations group={integrations.bitbucket} />
|
||||
<Typography paragraph variant="h2">
|
||||
Bitbucket Cloud
|
||||
</Typography>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
import { createDevApp } from '@backstage/dev-utils';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { scmIntegrationsApiRef } from '../src/ScmIntegrationsApi';
|
||||
import { scmIntegrationsApiRef } from '../src/api/ScmIntegrationsApi';
|
||||
import { DevPage } from './DevPage';
|
||||
import { configApiRef, createApiFactory } from '@backstage/core-plugin-api';
|
||||
|
||||
|
||||
@@ -26,6 +26,6 @@ describe('scmIntegrationsApiRef', () => {
|
||||
|
||||
it('should be instantiated', () => {
|
||||
const i = ScmIntegrationsApi.fromConfig(new ConfigReader({}));
|
||||
expect(i.list().length).toBe(8); // The default ones
|
||||
expect(i.list().length).toBe(7); // The default ones
|
||||
});
|
||||
});
|
||||
|
||||
Vendored
-58
@@ -27,27 +27,6 @@ export interface Config {
|
||||
* @visibility frontend
|
||||
*/
|
||||
host: string;
|
||||
/**
|
||||
* Token used to authenticate requests.
|
||||
* @visibility secret
|
||||
* @deprecated Use `credentials` instead.
|
||||
*/
|
||||
token?: string;
|
||||
|
||||
/**
|
||||
* The credential to use for requests.
|
||||
*
|
||||
* If no credential is specified anonymous access is used.
|
||||
*
|
||||
* @deepVisibility secret
|
||||
* @deprecated Use `credentials` instead.
|
||||
*/
|
||||
credential?: {
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
tenantId?: string;
|
||||
personalAccessToken?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The credentials to use for requests. If multiple credentials are specified the first one that matches the organization is used.
|
||||
@@ -131,43 +110,6 @@ export interface Config {
|
||||
};
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Integration configuration for Bitbucket
|
||||
* @deprecated replaced by bitbucketCloud and bitbucketServer
|
||||
*/
|
||||
bitbucket?: Array<{
|
||||
/**
|
||||
* The hostname of the given Bitbucket instance
|
||||
* @visibility frontend
|
||||
*/
|
||||
host: string;
|
||||
/**
|
||||
* Token used to authenticate requests.
|
||||
* @visibility secret
|
||||
*/
|
||||
token?: string;
|
||||
/**
|
||||
* The base url for the Bitbucket API, for example https://api.bitbucket.org/2.0
|
||||
* @visibility frontend
|
||||
*/
|
||||
apiBaseUrl?: string;
|
||||
/**
|
||||
* The username to use for authenticated requests.
|
||||
* @visibility secret
|
||||
*/
|
||||
username?: string;
|
||||
/**
|
||||
* Bitbucket app password used to authenticate requests.
|
||||
* @visibility secret
|
||||
*/
|
||||
appPassword?: string;
|
||||
/**
|
||||
* PGP signing key for signing commits.
|
||||
* @visibility secret
|
||||
*/
|
||||
commitSigningKey?: string;
|
||||
}>;
|
||||
|
||||
/** Integration configuration for Bitbucket Cloud */
|
||||
bitbucketCloud?: Array<{
|
||||
/**
|
||||
|
||||
@@ -201,8 +201,6 @@ export class AzureIntegration implements ScmIntegration {
|
||||
// @public
|
||||
export type AzureIntegrationConfig = {
|
||||
host: string;
|
||||
token?: string;
|
||||
credential?: AzureDevOpsCredential;
|
||||
credentials?: AzureDevOpsCredential[];
|
||||
commitSigningKey?: string;
|
||||
};
|
||||
@@ -255,37 +253,6 @@ export type BitbucketCloudIntegrationConfig = {
|
||||
commitSigningKey?: string;
|
||||
};
|
||||
|
||||
// @public @deprecated
|
||||
export class BitbucketIntegration implements ScmIntegration {
|
||||
constructor(integrationConfig: BitbucketIntegrationConfig);
|
||||
// (undocumented)
|
||||
get config(): BitbucketIntegrationConfig;
|
||||
// (undocumented)
|
||||
static factory: ScmIntegrationsFactory<BitbucketIntegration>;
|
||||
// (undocumented)
|
||||
resolveEditUrl(url: string): string;
|
||||
// (undocumented)
|
||||
resolveUrl(options: {
|
||||
url: string;
|
||||
base: string;
|
||||
lineNumber?: number;
|
||||
}): string;
|
||||
// (undocumented)
|
||||
get title(): string;
|
||||
// (undocumented)
|
||||
get type(): string;
|
||||
}
|
||||
|
||||
// @public @deprecated
|
||||
export type BitbucketIntegrationConfig = {
|
||||
host: string;
|
||||
apiBaseUrl: string;
|
||||
token?: string;
|
||||
username?: string;
|
||||
appPassword?: string;
|
||||
commitSigningKey?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export class BitbucketServerIntegration implements ScmIntegration {
|
||||
constructor(integrationConfig: BitbucketServerIntegrationConfig);
|
||||
@@ -317,14 +284,6 @@ export type BitbucketServerIntegrationConfig = {
|
||||
commitSigningKey?: string;
|
||||
};
|
||||
|
||||
// @public @deprecated
|
||||
export function buildGerritGitilesArchiveUrl(
|
||||
config: GerritIntegrationConfig,
|
||||
project: string,
|
||||
branch: string,
|
||||
filePath: string,
|
||||
): string;
|
||||
|
||||
// @public
|
||||
export function buildGerritGitilesArchiveUrlFromLocation(
|
||||
config: GerritIntegrationConfig,
|
||||
@@ -426,14 +385,6 @@ export function getAzureDownloadUrl(url: string): string;
|
||||
// @public
|
||||
export function getAzureFileFetchUrl(url: string): string;
|
||||
|
||||
// @public @deprecated
|
||||
export function getAzureRequestOptions(
|
||||
config: AzureIntegrationConfig,
|
||||
additionalHeaders?: Record<string, string>,
|
||||
): Promise<{
|
||||
headers: Record<string, string>;
|
||||
}>;
|
||||
|
||||
// @public
|
||||
export function getBitbucketCloudDefaultBranch(
|
||||
url: string,
|
||||
@@ -465,31 +416,6 @@ export function getBitbucketCloudRequestOptions(
|
||||
headers: Record<string, string>;
|
||||
}>;
|
||||
|
||||
// @public @deprecated
|
||||
export function getBitbucketDefaultBranch(
|
||||
url: string,
|
||||
config: BitbucketIntegrationConfig,
|
||||
): Promise<string>;
|
||||
|
||||
// @public @deprecated
|
||||
export function getBitbucketDownloadUrl(
|
||||
url: string,
|
||||
config: BitbucketIntegrationConfig,
|
||||
): Promise<string>;
|
||||
|
||||
// @public @deprecated
|
||||
export function getBitbucketFileFetchUrl(
|
||||
url: string,
|
||||
config: BitbucketIntegrationConfig,
|
||||
): string;
|
||||
|
||||
// @public @deprecated
|
||||
export function getBitbucketRequestOptions(
|
||||
config: BitbucketIntegrationConfig,
|
||||
): {
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function getBitbucketServerDefaultBranch(
|
||||
url: string,
|
||||
@@ -579,14 +505,6 @@ export function getGithubFileFetchUrl(
|
||||
credentials: GithubCredentials,
|
||||
): string;
|
||||
|
||||
// @public @deprecated
|
||||
export function getGitHubRequestOptions(
|
||||
config: GithubIntegrationConfig,
|
||||
credentials: GithubCredentials,
|
||||
): {
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function getGitilesAuthenticationUrl(
|
||||
config: GerritIntegrationConfig,
|
||||
@@ -854,8 +772,6 @@ export interface IntegrationsByType {
|
||||
azure: ScmIntegrationsGroup<AzureIntegration>;
|
||||
// (undocumented)
|
||||
azureBlobStorage: ScmIntegrationsGroup<AzureBlobStorageIntergation>;
|
||||
// @deprecated (undocumented)
|
||||
bitbucket: ScmIntegrationsGroup<BitbucketIntegration>;
|
||||
// (undocumented)
|
||||
bitbucketCloud: ScmIntegrationsGroup<BitbucketCloudIntegration>;
|
||||
// (undocumented)
|
||||
@@ -874,16 +790,6 @@ export interface IntegrationsByType {
|
||||
harness: ScmIntegrationsGroup<HarnessIntegration>;
|
||||
}
|
||||
|
||||
// @public @deprecated
|
||||
export function parseGerritGitilesUrl(
|
||||
config: GerritIntegrationConfig,
|
||||
url: string,
|
||||
): {
|
||||
branch: string;
|
||||
filePath: string;
|
||||
project: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function parseGerritJsonResponse(response: Response): Promise<unknown>;
|
||||
|
||||
@@ -989,16 +895,6 @@ export function readBitbucketCloudIntegrationConfigs(
|
||||
configs: Config[],
|
||||
): BitbucketCloudIntegrationConfig[];
|
||||
|
||||
// @public @deprecated
|
||||
export function readBitbucketIntegrationConfig(
|
||||
config: Config,
|
||||
): BitbucketIntegrationConfig;
|
||||
|
||||
// @public @deprecated
|
||||
export function readBitbucketIntegrationConfigs(
|
||||
configs: Config[],
|
||||
): BitbucketIntegrationConfig[];
|
||||
|
||||
// @public
|
||||
export function readBitbucketServerIntegrationConfig(
|
||||
config: Config,
|
||||
@@ -1085,8 +981,6 @@ export interface ScmIntegrationRegistry
|
||||
azure: ScmIntegrationsGroup<AzureIntegration>;
|
||||
// (undocumented)
|
||||
azureBlobStorage: ScmIntegrationsGroup<AzureBlobStorageIntergation>;
|
||||
// @deprecated (undocumented)
|
||||
bitbucket: ScmIntegrationsGroup<BitbucketIntegration>;
|
||||
// (undocumented)
|
||||
bitbucketCloud: ScmIntegrationsGroup<BitbucketCloudIntegration>;
|
||||
// (undocumented)
|
||||
@@ -1120,8 +1014,6 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
|
||||
get azure(): ScmIntegrationsGroup<AzureIntegration>;
|
||||
// (undocumented)
|
||||
get azureBlobStorage(): ScmIntegrationsGroup<AzureBlobStorageIntergation>;
|
||||
// @deprecated (undocumented)
|
||||
get bitbucket(): ScmIntegrationsGroup<BitbucketIntegration>;
|
||||
// (undocumented)
|
||||
get bitbucketCloud(): ScmIntegrationsGroup<BitbucketCloudIntegration>;
|
||||
// (undocumented)
|
||||
|
||||
@@ -21,8 +21,6 @@ import {
|
||||
BitbucketCloudIntegration,
|
||||
BitbucketCloudIntegrationConfig,
|
||||
} from './bitbucketCloud';
|
||||
import { BitbucketIntegrationConfig } from './bitbucket';
|
||||
import { BitbucketIntegration } from './bitbucket/BitbucketIntegration';
|
||||
import {
|
||||
BitbucketServerIntegration,
|
||||
BitbucketServerIntegrationConfig,
|
||||
@@ -62,10 +60,6 @@ describe('ScmIntegrations', () => {
|
||||
host: 'azureblobstorage.local',
|
||||
} as AzureBlobStorageIntegrationConfig);
|
||||
|
||||
const bitbucket = new BitbucketIntegration({
|
||||
host: 'bitbucket.local',
|
||||
} as BitbucketIntegrationConfig);
|
||||
|
||||
const bitbucketCloud = new BitbucketCloudIntegration({
|
||||
host: 'bitbucket.org',
|
||||
} as BitbucketCloudIntegrationConfig);
|
||||
@@ -103,7 +97,6 @@ describe('ScmIntegrations', () => {
|
||||
awsCodeCommit: basicIntegrations([awsCodeCommit], item => item.config.host),
|
||||
azure: basicIntegrations([azure], item => item.config.host),
|
||||
azureBlobStorage: basicIntegrations([azureBlob], item => item.config.host),
|
||||
bitbucket: basicIntegrations([bitbucket], item => item.config.host),
|
||||
bitbucketCloud: basicIntegrations([bitbucketCloud], item => item.title),
|
||||
bitbucketServer: basicIntegrations(
|
||||
[bitbucketServer],
|
||||
@@ -126,7 +119,6 @@ describe('ScmIntegrations', () => {
|
||||
expect(i.azureBlobStorage.byUrl('https://azureblobstorage.local')).toBe(
|
||||
azureBlob,
|
||||
);
|
||||
expect(i.bitbucket.byUrl('https://bitbucket.local')).toBe(bitbucket);
|
||||
expect(i.bitbucketCloud.byUrl('https://bitbucket.org')).toBe(
|
||||
bitbucketCloud,
|
||||
);
|
||||
@@ -147,7 +139,6 @@ describe('ScmIntegrations', () => {
|
||||
awsCodeCommit,
|
||||
azure,
|
||||
azureBlob,
|
||||
bitbucket,
|
||||
bitbucketCloud,
|
||||
bitbucketServer,
|
||||
gerrit,
|
||||
@@ -166,7 +157,6 @@ describe('ScmIntegrations', () => {
|
||||
expect(i.azureBlobStorage.byUrl('https://azureblobstorage.local')).toBe(
|
||||
azureBlob,
|
||||
);
|
||||
expect(i.byUrl('https://bitbucket.local')).toBe(bitbucket);
|
||||
expect(i.byUrl('https://bitbucket.org')).toBe(bitbucketCloud);
|
||||
expect(i.byUrl('https://bitbucket-server.local')).toBe(bitbucketServer);
|
||||
expect(i.byUrl('https://gerrit.local')).toBe(gerrit);
|
||||
@@ -179,7 +169,6 @@ describe('ScmIntegrations', () => {
|
||||
expect(i.byHost('awscodecommit.local')).toBe(awsCodeCommit);
|
||||
expect(i.byHost('azure.local')).toBe(azure);
|
||||
expect(i.byHost('azureblobstorage.local')).toBe(azureBlob);
|
||||
expect(i.byHost('bitbucket.local')).toBe(bitbucket);
|
||||
expect(i.byHost('bitbucket.org')).toBe(bitbucketCloud);
|
||||
expect(i.byHost('bitbucket-server.local')).toBe(bitbucketServer);
|
||||
expect(i.byHost('gerrit.local')).toBe(gerrit);
|
||||
|
||||
@@ -19,7 +19,6 @@ import { AwsS3Integration } from './awsS3/AwsS3Integration';
|
||||
import { AwsCodeCommitIntegration } from './awsCodeCommit/AwsCodeCommitIntegration';
|
||||
import { AzureIntegration } from './azure/AzureIntegration';
|
||||
import { BitbucketCloudIntegration } from './bitbucketCloud/BitbucketCloudIntegration';
|
||||
import { BitbucketIntegration } from './bitbucket/BitbucketIntegration';
|
||||
import { BitbucketServerIntegration } from './bitbucketServer/BitbucketServerIntegration';
|
||||
import { GerritIntegration } from './gerrit/GerritIntegration';
|
||||
import { GithubIntegration } from './github/GithubIntegration';
|
||||
@@ -42,10 +41,6 @@ export interface IntegrationsByType {
|
||||
awsCodeCommit: ScmIntegrationsGroup<AwsCodeCommitIntegration>;
|
||||
azureBlobStorage: ScmIntegrationsGroup<AzureBlobStorageIntergation>;
|
||||
azure: ScmIntegrationsGroup<AzureIntegration>;
|
||||
/**
|
||||
* @deprecated in favor of `bitbucketCloud` and `bitbucketServer`
|
||||
*/
|
||||
bitbucket: ScmIntegrationsGroup<BitbucketIntegration>;
|
||||
bitbucketCloud: ScmIntegrationsGroup<BitbucketCloudIntegration>;
|
||||
bitbucketServer: ScmIntegrationsGroup<BitbucketServerIntegration>;
|
||||
gerrit: ScmIntegrationsGroup<GerritIntegration>;
|
||||
@@ -70,7 +65,6 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
|
||||
awsCodeCommit: AwsCodeCommitIntegration.factory({ config }),
|
||||
azureBlobStorage: AzureBlobStorageIntergation.factory({ config }),
|
||||
azure: AzureIntegration.factory({ config }),
|
||||
bitbucket: BitbucketIntegration.factory({ config }),
|
||||
bitbucketCloud: BitbucketCloudIntegration.factory({ config }),
|
||||
bitbucketServer: BitbucketServerIntegration.factory({ config }),
|
||||
gerrit: GerritIntegration.factory({ config }),
|
||||
@@ -102,13 +96,6 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
|
||||
return this.byType.azure;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated in favor of `bitbucketCloud()` and `bitbucketServer()`
|
||||
*/
|
||||
get bitbucket(): ScmIntegrationsGroup<BitbucketIntegration> {
|
||||
return this.byType.bitbucket;
|
||||
}
|
||||
|
||||
get bitbucketCloud(): ScmIntegrationsGroup<BitbucketCloudIntegration> {
|
||||
return this.byType.bitbucketCloud;
|
||||
}
|
||||
@@ -148,20 +135,10 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
|
||||
}
|
||||
|
||||
byUrl(url: string | URL): ScmIntegration | undefined {
|
||||
let candidates = Object.values(this.byType)
|
||||
const candidates = Object.values(this.byType)
|
||||
.map(i => i.byUrl(url))
|
||||
.filter(Boolean);
|
||||
|
||||
// Do not return deprecated integrations if there are other options
|
||||
if (candidates.length > 1) {
|
||||
const filteredCandidates = candidates.filter(
|
||||
x => !(x instanceof BitbucketIntegration),
|
||||
);
|
||||
if (filteredCandidates.length !== 0) {
|
||||
candidates = filteredCandidates;
|
||||
}
|
||||
}
|
||||
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
|
||||
@@ -276,50 +276,6 @@ describe('readAzureIntegrationConfig', () => {
|
||||
expect(output).toEqual({ host: 'dev.azure.com' });
|
||||
});
|
||||
|
||||
it('maps deprecated token to credentials', () => {
|
||||
const output = readAzureIntegrationConfig(
|
||||
buildConfig({
|
||||
host: 'dev.azure.com',
|
||||
token: 't',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(output).toEqual({
|
||||
host: 'dev.azure.com',
|
||||
credentials: [
|
||||
{
|
||||
kind: 'PersonalAccessToken',
|
||||
personalAccessToken: 't',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('maps deprecated credential to credentials', () => {
|
||||
const output = readAzureIntegrationConfig(
|
||||
buildConfig({
|
||||
host: 'dev.azure.com',
|
||||
credential: {
|
||||
clientId: 'id',
|
||||
clientSecret: 'secret',
|
||||
tenantId: 'tenantId',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(output).toEqual({
|
||||
host: 'dev.azure.com',
|
||||
credentials: [
|
||||
{
|
||||
kind: 'ClientSecret',
|
||||
clientId: 'id',
|
||||
clientSecret: 'secret',
|
||||
tenantId: 'tenantId',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects config when host is not valid', () => {
|
||||
expect(() =>
|
||||
readAzureIntegrationConfig(buildConfig({ ...valid, host: 7 })),
|
||||
|
||||
@@ -32,24 +32,6 @@ export type AzureIntegrationConfig = {
|
||||
*/
|
||||
host: string;
|
||||
|
||||
/**
|
||||
* The authorization token to use for requests.
|
||||
*
|
||||
* If no token is specified, anonymous access is used.
|
||||
*
|
||||
* @deprecated Use `credentials` instead.
|
||||
*/
|
||||
token?: string;
|
||||
|
||||
/**
|
||||
* The credential to use for requests.
|
||||
*
|
||||
* If no credential is specified anonymous access is used.
|
||||
*
|
||||
* @deprecated Use `credentials` instead.
|
||||
*/
|
||||
credential?: AzureDevOpsCredential;
|
||||
|
||||
/**
|
||||
* The credentials to use for requests. If multiple credentials are specified the first one that matches the organization is used.
|
||||
* If not organization matches the first credential without an organization is used.
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { getAzureRequestOptions } from './deprecated';
|
||||
import { DateTime } from 'luxon';
|
||||
import {
|
||||
AccessToken,
|
||||
ClientSecretCredential,
|
||||
ManagedIdentityCredential,
|
||||
} from '@azure/identity';
|
||||
|
||||
jest.mock('@azure/identity');
|
||||
|
||||
const MockedClientSecretCredential = ClientSecretCredential as jest.MockedClass<
|
||||
typeof ClientSecretCredential
|
||||
>;
|
||||
|
||||
const MockedManagedIdentityCredential =
|
||||
ManagedIdentityCredential as jest.MockedClass<
|
||||
typeof ManagedIdentityCredential
|
||||
>;
|
||||
|
||||
describe('azure core', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
MockedClientSecretCredential.prototype.getToken.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(),
|
||||
token: 'fake-client-secret-token',
|
||||
} as AccessToken),
|
||||
);
|
||||
MockedManagedIdentityCredential.prototype.getToken.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(),
|
||||
token: 'fake-managed-identity-token',
|
||||
} as AccessToken),
|
||||
);
|
||||
});
|
||||
|
||||
describe('getAzureRequestOptions', () => {
|
||||
it('should not add authorization header when not using token or credential', async () => {
|
||||
expect(await getAzureRequestOptions({ host: '' })).toEqual(
|
||||
expect.objectContaining({
|
||||
headers: expect.not.objectContaining({
|
||||
Authorization: expect.anything(),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should add authorization header when using a personal access token', async () => {
|
||||
const pat = '0123456789';
|
||||
const encoded = Buffer.from(`:${pat}`).toString('base64');
|
||||
expect(
|
||||
await getAzureRequestOptions({
|
||||
host: '',
|
||||
credentials: [
|
||||
{
|
||||
kind: 'PersonalAccessToken',
|
||||
personalAccessToken: pat,
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: `Basic ${encoded}`,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should add authorization header when using a client secret', async () => {
|
||||
expect(
|
||||
await getAzureRequestOptions({
|
||||
host: '',
|
||||
credentials: [
|
||||
{
|
||||
kind: 'ClientSecret',
|
||||
clientId: 'fake-id',
|
||||
clientSecret: 'fake-secret',
|
||||
tenantId: 'fake-tenant',
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer fake-client-secret-token',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should add authorization header when using a managed identity', async () => {
|
||||
expect(
|
||||
await getAzureRequestOptions({
|
||||
host: '',
|
||||
credentials: [
|
||||
{
|
||||
kind: 'ManagedIdentity',
|
||||
clientId: 'fake-id',
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer fake-managed-identity-token',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { AzureIntegrationConfig } from './config';
|
||||
import { CachedAzureDevOpsCredentialsProvider } from './CachedAzureDevOpsCredentialsProvider';
|
||||
|
||||
/**
|
||||
* Gets the request options necessary to make requests to a given provider.
|
||||
*
|
||||
* @param config - The relevant provider config
|
||||
* @param additionalHeaders - Additional headers for the request
|
||||
* @public
|
||||
* @deprecated Use {@link AzureDevOpsCredentialsProvider} instead.
|
||||
*/
|
||||
export async function getAzureRequestOptions(
|
||||
config: AzureIntegrationConfig,
|
||||
additionalHeaders?: Record<string, string>,
|
||||
): Promise<{ headers: Record<string, string> }> {
|
||||
const headers: Record<string, string> = additionalHeaders
|
||||
? { ...additionalHeaders }
|
||||
: {};
|
||||
|
||||
/*
|
||||
* Since we do not have a way to determine which organization the request is for,
|
||||
* we will use the first credential that does not have an organization specified.
|
||||
*/
|
||||
const credentialConfig = config.credentials?.filter(
|
||||
credential =>
|
||||
credential.organizations === undefined ||
|
||||
credential.organizations.length === 0,
|
||||
)[0];
|
||||
|
||||
if (credentialConfig) {
|
||||
const credentialsProvider =
|
||||
CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential(
|
||||
credentialConfig,
|
||||
);
|
||||
const credentials = await credentialsProvider.getCredentials();
|
||||
|
||||
return {
|
||||
headers: {
|
||||
...credentials?.headers,
|
||||
...headers,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { headers };
|
||||
}
|
||||
@@ -38,5 +38,3 @@ export {
|
||||
|
||||
export * from './types';
|
||||
export { DefaultAzureDevOpsCredentialsProvider } from './DefaultAzureDevOpsCredentialsProvider';
|
||||
|
||||
export * from './deprecated';
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { BitbucketIntegration } from './BitbucketIntegration';
|
||||
|
||||
describe('BitbucketIntegration', () => {
|
||||
describe('factory', () => {
|
||||
it('works', () => {
|
||||
const integrations = BitbucketIntegration.factory({
|
||||
config: new ConfigReader({
|
||||
integrations: {
|
||||
bitbucket: [
|
||||
{
|
||||
host: 'h.com',
|
||||
apiBaseUrl: 'a',
|
||||
token: 't',
|
||||
username: 'u',
|
||||
appPassword: 'p',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(integrations.list().length).toBe(2); // including default
|
||||
expect(integrations.list()[0].config.host).toBe('h.com');
|
||||
expect(integrations.list()[1].config.host).toBe('bitbucket.org');
|
||||
});
|
||||
|
||||
it('falls back to bitbucketCloud+bitbucketServer', () => {
|
||||
const integrations = BitbucketIntegration.factory({
|
||||
config: new ConfigReader({
|
||||
integrations: {
|
||||
bitbucketCloud: [
|
||||
{
|
||||
username: 'u',
|
||||
appPassword: 'p',
|
||||
},
|
||||
],
|
||||
bitbucketServer: [
|
||||
{
|
||||
host: 'h.com',
|
||||
apiBaseUrl: 'a',
|
||||
token: 't',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(integrations.list().length).toBe(2); // including default
|
||||
expect(integrations.list()[0].config.host).toBe('bitbucket.org');
|
||||
expect(integrations.list()[1].config.host).toBe('h.com');
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the basics', () => {
|
||||
const integration = new BitbucketIntegration({ host: 'h.com' } as any);
|
||||
expect(integration.type).toBe('bitbucket');
|
||||
expect(integration.title).toBe('h.com');
|
||||
});
|
||||
|
||||
it('resolves url line number correctly for Bitbucket Cloud', () => {
|
||||
const integration = new BitbucketIntegration({
|
||||
host: 'bitbucket.org',
|
||||
} as any);
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: './a.yaml',
|
||||
base: 'https://bitbucket.org/my-owner/my-project/src/master/README.md',
|
||||
lineNumber: 14,
|
||||
}),
|
||||
).toBe(
|
||||
'https://bitbucket.org/my-owner/my-project/src/master/a.yaml#lines-14',
|
||||
);
|
||||
});
|
||||
|
||||
it('resolves url line number correctly for Bitbucket Server', () => {
|
||||
const integration = new BitbucketIntegration({ host: 'h.com' } as any);
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: './a.yaml',
|
||||
base: 'https://bitbucket.org/my-owner/my-project/src/master/README.md',
|
||||
lineNumber: 14,
|
||||
}),
|
||||
).toBe('https://bitbucket.org/my-owner/my-project/src/master/a.yaml#14');
|
||||
});
|
||||
|
||||
it('resolve edit URL', () => {
|
||||
const integration = new BitbucketIntegration({ host: 'h.com' } as any);
|
||||
|
||||
expect(
|
||||
integration.resolveEditUrl(
|
||||
'https://bitbucket.org/my-owner/my-project/src/master/README.md',
|
||||
),
|
||||
).toBe(
|
||||
'https://bitbucket.org/my-owner/my-project/src/master/README.md?mode=edit&spa=0&at=master',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,99 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import { basicIntegrations, defaultScmResolveUrl } from '../helpers';
|
||||
import { ScmIntegration, ScmIntegrationsFactory } from '../types';
|
||||
import {
|
||||
BitbucketIntegrationConfig,
|
||||
readBitbucketIntegrationConfigs,
|
||||
} from './config';
|
||||
|
||||
/**
|
||||
* A Bitbucket based integration.
|
||||
*
|
||||
* @public
|
||||
* @deprecated replaced by the integrations bitbucketCloud and bitbucketServer.
|
||||
*/
|
||||
export class BitbucketIntegration implements ScmIntegration {
|
||||
static factory: ScmIntegrationsFactory<BitbucketIntegration> = ({
|
||||
config,
|
||||
}) => {
|
||||
const configs = readBitbucketIntegrationConfigs(
|
||||
config.getOptionalConfigArray('integrations.bitbucket') ?? [
|
||||
// if integrations.bitbucket was not used assume the use was migrated to the new configs
|
||||
// and backport for the deprecated integration to be usable for other parts of the system
|
||||
// until these got migrated
|
||||
...(config.getOptionalConfigArray('integrations.bitbucketCloud') ?? []),
|
||||
...(config.getOptionalConfigArray('integrations.bitbucketServer') ??
|
||||
[]),
|
||||
],
|
||||
);
|
||||
return basicIntegrations(
|
||||
configs.map(c => new BitbucketIntegration(c)),
|
||||
i => i.config.host,
|
||||
);
|
||||
};
|
||||
|
||||
constructor(private readonly integrationConfig: BitbucketIntegrationConfig) {}
|
||||
|
||||
get type(): string {
|
||||
return 'bitbucket';
|
||||
}
|
||||
|
||||
get title(): string {
|
||||
return this.integrationConfig.host;
|
||||
}
|
||||
|
||||
get config(): BitbucketIntegrationConfig {
|
||||
return this.integrationConfig;
|
||||
}
|
||||
|
||||
resolveUrl(options: {
|
||||
url: string;
|
||||
base: string;
|
||||
lineNumber?: number;
|
||||
}): string {
|
||||
const resolved = defaultScmResolveUrl(options);
|
||||
if (!options.lineNumber) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
const url = new URL(resolved);
|
||||
|
||||
if (this.integrationConfig.host === 'bitbucket.org') {
|
||||
// Bitbucket Cloud uses the syntax #lines-{start}[:{end}][,...]
|
||||
url.hash = `lines-${options.lineNumber}`;
|
||||
} else {
|
||||
// Bitbucket Server uses the syntax #{start}[-{end}][,...]
|
||||
url.hash = `${options.lineNumber}`;
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
resolveEditUrl(url: string): string {
|
||||
const urlData = parseGitUrl(url);
|
||||
const editUrl = new URL(url);
|
||||
|
||||
editUrl.searchParams.set('mode', 'edit');
|
||||
// TODO: Not sure what spa=0 does, at least bitbucket.org doesn't support it
|
||||
// but this is taken over from the initial implementation.
|
||||
editUrl.searchParams.set('spa', '0');
|
||||
editUrl.searchParams.set('at', urlData.ref);
|
||||
return editUrl.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config, ConfigReader } from '@backstage/config';
|
||||
import { loadConfigSchema } from '@backstage/config-loader';
|
||||
import {
|
||||
BitbucketIntegrationConfig,
|
||||
readBitbucketIntegrationConfig,
|
||||
readBitbucketIntegrationConfigs,
|
||||
} from './config';
|
||||
|
||||
describe('readBitbucketIntegrationConfig', () => {
|
||||
function buildConfig(data: Partial<BitbucketIntegrationConfig>): Config {
|
||||
return new ConfigReader(data);
|
||||
}
|
||||
|
||||
async function buildFrontendConfig(
|
||||
data: Partial<BitbucketIntegrationConfig>,
|
||||
): Promise<Config> {
|
||||
const fullSchema = await loadConfigSchema({
|
||||
dependencies: ['@backstage/integration'],
|
||||
});
|
||||
const serializedSchema = fullSchema.serialize() as {
|
||||
schemas: { value: { properties?: { integrations?: object } } }[];
|
||||
};
|
||||
const schema = await loadConfigSchema({
|
||||
serialized: {
|
||||
...serializedSchema, // only include schemas that apply to integrations
|
||||
schemas: serializedSchema.schemas.filter(
|
||||
s => s.value?.properties?.integrations,
|
||||
),
|
||||
},
|
||||
});
|
||||
const processed = schema.process(
|
||||
[{ data: { integrations: { bitbucket: [data] } }, context: 'app' }],
|
||||
{ visibility: ['frontend'] },
|
||||
);
|
||||
return new ConfigReader(
|
||||
(processed[0].data as any).integrations.bitbucket[0],
|
||||
);
|
||||
}
|
||||
|
||||
it('reads all values', () => {
|
||||
const output = readBitbucketIntegrationConfig(
|
||||
buildConfig({
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
token: 't\n\n\n',
|
||||
username: 'u',
|
||||
appPassword: '\n\n\np',
|
||||
}),
|
||||
);
|
||||
expect(output).toEqual({
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
token: 't',
|
||||
username: 'u',
|
||||
appPassword: 'p',
|
||||
});
|
||||
});
|
||||
|
||||
it('inserts the defaults if missing', () => {
|
||||
const output = readBitbucketIntegrationConfig(buildConfig({}));
|
||||
expect(output).toEqual(
|
||||
expect.objectContaining({
|
||||
host: 'bitbucket.org',
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects funky configs', () => {
|
||||
const valid: any = {
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
token: 't',
|
||||
username: 'u',
|
||||
appPassword: 'p',
|
||||
};
|
||||
expect(() =>
|
||||
readBitbucketIntegrationConfig(buildConfig({ ...valid, host: 7 })),
|
||||
).toThrow(/host/);
|
||||
expect(() =>
|
||||
readBitbucketIntegrationConfig(buildConfig({ ...valid, apiBaseUrl: 7 })),
|
||||
).toThrow(/apiBaseUrl/);
|
||||
expect(() =>
|
||||
readBitbucketIntegrationConfig(buildConfig({ ...valid, token: 7 })),
|
||||
).toThrow(/token/);
|
||||
expect(() =>
|
||||
readBitbucketIntegrationConfig(buildConfig({ ...valid, username: 7 })),
|
||||
).toThrow(/username/);
|
||||
expect(() =>
|
||||
readBitbucketIntegrationConfig(buildConfig({ ...valid, appPassword: 7 })),
|
||||
).toThrow(/appPassword/);
|
||||
});
|
||||
|
||||
it('works on the frontend', async () => {
|
||||
expect(
|
||||
readBitbucketIntegrationConfig(
|
||||
await buildFrontendConfig({
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
token: 't',
|
||||
username: 'u',
|
||||
appPassword: 'p',
|
||||
}),
|
||||
),
|
||||
).toEqual({
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('readBitbucketIntegrationConfigs', () => {
|
||||
function buildConfig(data: Partial<BitbucketIntegrationConfig>[]): Config[] {
|
||||
return data.map(item => new ConfigReader(item));
|
||||
}
|
||||
|
||||
it('reads all values', () => {
|
||||
const output = readBitbucketIntegrationConfigs(
|
||||
buildConfig([
|
||||
{
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
token: 't',
|
||||
username: 'u',
|
||||
appPassword: 'p',
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(output).toContainEqual({
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
token: 't',
|
||||
username: 'u',
|
||||
appPassword: 'p',
|
||||
});
|
||||
});
|
||||
|
||||
it('adds a default Bitbucket Cloud entry when missing', () => {
|
||||
const output = readBitbucketIntegrationConfigs(buildConfig([]));
|
||||
expect(output).toEqual([
|
||||
{
|
||||
host: 'bitbucket.org',
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('injects the correct Bitbucket Cloud API base URL when missing', () => {
|
||||
const output = readBitbucketIntegrationConfigs(
|
||||
buildConfig([{ host: 'bitbucket.org' }]),
|
||||
);
|
||||
expect(output).toEqual([
|
||||
{
|
||||
host: 'bitbucket.org',
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,136 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { trimEnd } from 'lodash';
|
||||
import { isValidHost } from '../helpers';
|
||||
|
||||
const BITBUCKET_HOST = 'bitbucket.org';
|
||||
const BITBUCKET_API_BASE_URL = 'https://api.bitbucket.org/2.0';
|
||||
|
||||
/**
|
||||
* The configuration parameters for a single Bitbucket API provider.
|
||||
*
|
||||
* @public
|
||||
* @deprecated bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer.
|
||||
*/
|
||||
export type BitbucketIntegrationConfig = {
|
||||
/**
|
||||
* The host of the target that this matches on, e.g. "bitbucket.org"
|
||||
*/
|
||||
host: string;
|
||||
|
||||
/**
|
||||
* The base URL of the API of this provider, e.g. "https://api.bitbucket.org/2.0",
|
||||
* with no trailing slash.
|
||||
*
|
||||
* Values omitted at the optional property at the app-config will be deduced
|
||||
* from the "host" value.
|
||||
*/
|
||||
apiBaseUrl: string;
|
||||
|
||||
/**
|
||||
* The authorization token to use for requests to a Bitbucket Server provider.
|
||||
*
|
||||
* See https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html
|
||||
*
|
||||
* If no token is specified, anonymous access is used.
|
||||
*/
|
||||
token?: string;
|
||||
|
||||
/**
|
||||
* The username to use for requests to Bitbucket Cloud (bitbucket.org).
|
||||
*/
|
||||
username?: string;
|
||||
|
||||
/**
|
||||
* Authentication with Bitbucket Cloud (bitbucket.org) is done using app passwords.
|
||||
*
|
||||
* See https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/
|
||||
*/
|
||||
appPassword?: string;
|
||||
|
||||
/**
|
||||
* Signing key for commits
|
||||
*/
|
||||
commitSigningKey?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads a single Bitbucket integration config.
|
||||
*
|
||||
* @param config - The config object of a single integration
|
||||
* @public
|
||||
* @deprecated bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer.
|
||||
*/
|
||||
export function readBitbucketIntegrationConfig(
|
||||
config: Config,
|
||||
): BitbucketIntegrationConfig {
|
||||
const host = config.getOptionalString('host') ?? BITBUCKET_HOST;
|
||||
let apiBaseUrl = config.getOptionalString('apiBaseUrl');
|
||||
const token = config.getOptionalString('token')?.trim();
|
||||
const username = config.getOptionalString('username');
|
||||
const appPassword = config.getOptionalString('appPassword')?.trim();
|
||||
|
||||
if (!isValidHost(host)) {
|
||||
throw new Error(
|
||||
`Invalid Bitbucket integration config, '${host}' is not a valid host`,
|
||||
);
|
||||
}
|
||||
|
||||
if (apiBaseUrl) {
|
||||
apiBaseUrl = trimEnd(apiBaseUrl, '/');
|
||||
} else if (host === BITBUCKET_HOST) {
|
||||
apiBaseUrl = BITBUCKET_API_BASE_URL;
|
||||
} else {
|
||||
apiBaseUrl = `https://${host}/rest/api/1.0`;
|
||||
}
|
||||
|
||||
return {
|
||||
host,
|
||||
apiBaseUrl,
|
||||
token,
|
||||
username,
|
||||
appPassword,
|
||||
commitSigningKey: config.getOptionalString('commitSigningKey'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a set of Bitbucket integration configs, and inserts some defaults for
|
||||
* public Bitbucket if not specified.
|
||||
*
|
||||
* @param configs - All of the integration config objects
|
||||
* @public
|
||||
* @deprecated bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer.
|
||||
*/
|
||||
export function readBitbucketIntegrationConfigs(
|
||||
configs: Config[],
|
||||
): BitbucketIntegrationConfig[] {
|
||||
// First read all the explicit integrations
|
||||
const result = configs.map(readBitbucketIntegrationConfig);
|
||||
|
||||
// If no explicit bitbucket.org integration was added, put one in the list as
|
||||
// a convenience
|
||||
if (!result.some(c => c.host === BITBUCKET_HOST)) {
|
||||
result.push({
|
||||
host: BITBUCKET_HOST,
|
||||
apiBaseUrl: BITBUCKET_API_BASE_URL,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,304 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { registerMswTestHooks } from '../helpers';
|
||||
import { BitbucketIntegrationConfig } from './config';
|
||||
import {
|
||||
getBitbucketDefaultBranch,
|
||||
getBitbucketDownloadUrl,
|
||||
getBitbucketFileFetchUrl,
|
||||
getBitbucketRequestOptions,
|
||||
} from './core';
|
||||
|
||||
describe('bitbucket core', () => {
|
||||
const worker = setupServer();
|
||||
registerMswTestHooks(worker);
|
||||
|
||||
describe('getBitbucketRequestOptions', () => {
|
||||
it('inserts a token when needed', () => {
|
||||
const withToken: BitbucketIntegrationConfig = {
|
||||
host: '',
|
||||
apiBaseUrl: '',
|
||||
token: 'A',
|
||||
};
|
||||
const withoutToken: BitbucketIntegrationConfig = {
|
||||
host: '',
|
||||
apiBaseUrl: '',
|
||||
};
|
||||
expect(
|
||||
(getBitbucketRequestOptions(withToken).headers as any).Authorization,
|
||||
).toEqual('Bearer A');
|
||||
expect(
|
||||
(getBitbucketRequestOptions(withoutToken).headers as any).Authorization,
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('insert basic auth when needed', () => {
|
||||
const withUsernameAndPassword: BitbucketIntegrationConfig = {
|
||||
host: '',
|
||||
apiBaseUrl: '',
|
||||
username: 'some-user',
|
||||
appPassword: 'my-secret',
|
||||
};
|
||||
const withoutUsernameAndPassword: BitbucketIntegrationConfig = {
|
||||
host: '',
|
||||
apiBaseUrl: '',
|
||||
};
|
||||
expect(
|
||||
(getBitbucketRequestOptions(withUsernameAndPassword).headers as any)
|
||||
.Authorization,
|
||||
).toEqual('Basic c29tZS11c2VyOm15LXNlY3JldA==');
|
||||
expect(
|
||||
(getBitbucketRequestOptions(withoutUsernameAndPassword).headers as any)
|
||||
.Authorization,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBitbucketFileFetchUrl', () => {
|
||||
it('rejects targets that do not look like URLs', () => {
|
||||
const config: BitbucketIntegrationConfig = { host: '', apiBaseUrl: '' };
|
||||
expect(() => getBitbucketFileFetchUrl('a/b', config)).toThrow(
|
||||
/Incorrect URL: a\/b/,
|
||||
);
|
||||
});
|
||||
|
||||
it('happy path for Bitbucket Cloud', () => {
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.org',
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
};
|
||||
expect(
|
||||
getBitbucketFileFetchUrl(
|
||||
'https://bitbucket.org/org-name/repo-name/src/master/templates/my-template.yaml',
|
||||
config,
|
||||
),
|
||||
).toEqual(
|
||||
'https://api.bitbucket.org/2.0/repositories/org-name/repo-name/src/master/templates/my-template.yaml',
|
||||
);
|
||||
});
|
||||
|
||||
it('happy path for Bitbucket Server', () => {
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://bitbucket.mycompany.net/rest/api/1.0',
|
||||
};
|
||||
expect(
|
||||
getBitbucketFileFetchUrl(
|
||||
'https://bitbucket.mycompany.net/projects/a/repos/b/browse/path/to/c.yaml',
|
||||
config,
|
||||
),
|
||||
).toEqual(
|
||||
'https://bitbucket.mycompany.net/rest/api/1.0/projects/a/repos/b/raw/path/to/c.yaml?at=',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBitbucketDownloadUrl', () => {
|
||||
it('add path param if a path is specified for Bitbucket Server', async () => {
|
||||
const defaultBranchResponse = {
|
||||
displayId: 'main',
|
||||
};
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json(defaultBranchResponse),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
|
||||
};
|
||||
const result = await getBitbucketDownloadUrl(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs',
|
||||
config,
|
||||
);
|
||||
expect(result).toEqual(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=tgz&at=main&prefix=backstage-mock&path=docs',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not double encode the filepath', async () => {
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
|
||||
};
|
||||
const result = await getBitbucketDownloadUrl(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/%2Fdocs?at=some-branch',
|
||||
config,
|
||||
);
|
||||
expect(result).toEqual(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=tgz&at=some-branch&prefix=backstage-mock&path=%2Fdocs',
|
||||
);
|
||||
});
|
||||
|
||||
it('do not add path param if no path is specified for Bitbucket Server', async () => {
|
||||
const defaultBranchResponse = {
|
||||
displayId: 'main',
|
||||
};
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json(defaultBranchResponse),
|
||||
),
|
||||
),
|
||||
);
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
|
||||
};
|
||||
const result = await getBitbucketDownloadUrl(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse',
|
||||
config,
|
||||
);
|
||||
|
||||
expect(result).toEqual(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=tgz&at=main&prefix=backstage-mock',
|
||||
);
|
||||
});
|
||||
|
||||
it('get by branch for Bitbucket Server', async () => {
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
|
||||
};
|
||||
const result = await getBitbucketDownloadUrl(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs?at=some-branch',
|
||||
config,
|
||||
);
|
||||
expect(result).toEqual(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=tgz&at=some-branch&prefix=backstage-mock&path=docs',
|
||||
);
|
||||
});
|
||||
|
||||
it('do not add path param for Bitbucket Cloud', async () => {
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.org',
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
};
|
||||
const result = await getBitbucketDownloadUrl(
|
||||
'https://bitbucket.org/backstage/mock/src/master',
|
||||
config,
|
||||
);
|
||||
expect(result).toEqual(
|
||||
'https://bitbucket.org/backstage/mock/get/master.tar.gz',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBitbucketDefaultBranch', () => {
|
||||
it('return default branch for Bitbucket Cloud', async () => {
|
||||
const repoInfoResponse = {
|
||||
mainbranch: {
|
||||
name: 'main',
|
||||
},
|
||||
};
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage/mock',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json(repoInfoResponse),
|
||||
),
|
||||
),
|
||||
);
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.org',
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
};
|
||||
const defaultBranch = await getBitbucketDefaultBranch(
|
||||
'https://bitbucket.org/backstage/mock/src/main',
|
||||
config,
|
||||
);
|
||||
expect(defaultBranch).toEqual('main');
|
||||
});
|
||||
|
||||
it('return default branch for Bitbucket Server', async () => {
|
||||
const defaultBranchResponse = {
|
||||
displayId: 'main',
|
||||
};
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json(defaultBranchResponse),
|
||||
),
|
||||
),
|
||||
);
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
|
||||
};
|
||||
const defaultBranch = await getBitbucketDefaultBranch(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/README.md',
|
||||
config,
|
||||
);
|
||||
expect(defaultBranch).toEqual('main');
|
||||
});
|
||||
|
||||
it('return default branch for Bitbucket Server for bitbucket version 5.11', async () => {
|
||||
const defaultBranchResponse = {
|
||||
displayId: 'main',
|
||||
};
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(404),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json(defaultBranchResponse),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/branches/default',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json(defaultBranchResponse),
|
||||
),
|
||||
),
|
||||
);
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
|
||||
};
|
||||
const defaultBranch = await getBitbucketDefaultBranch(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/README.md',
|
||||
config,
|
||||
);
|
||||
expect(defaultBranch).toEqual('main');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,183 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fetch from 'cross-fetch';
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import { BitbucketIntegrationConfig } from './config';
|
||||
|
||||
/**
|
||||
* Given a URL pointing to a path on a provider, returns the default branch.
|
||||
*
|
||||
* @param url - A URL pointing to a path
|
||||
* @param config - The relevant provider config
|
||||
* @public
|
||||
* @deprecated no longer in use, bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer.
|
||||
*/
|
||||
export async function getBitbucketDefaultBranch(
|
||||
url: string,
|
||||
config: BitbucketIntegrationConfig,
|
||||
): Promise<string> {
|
||||
const { name: repoName, owner: project, resource } = parseGitUrl(url);
|
||||
|
||||
const isHosted = resource === 'bitbucket.org';
|
||||
// Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp184
|
||||
let branchUrl = isHosted
|
||||
? `${config.apiBaseUrl}/repositories/${project}/${repoName}`
|
||||
: `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/default-branch`;
|
||||
|
||||
let response = await fetch(branchUrl, getBitbucketRequestOptions(config));
|
||||
|
||||
if (response.status === 404 && !isHosted) {
|
||||
// First try the new format, and then if it gets specifically a 404 it should try the old format
|
||||
// (to support old Atlassian Bitbucket v5.11.1 format )
|
||||
branchUrl = `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/branches/default`;
|
||||
response = await fetch(branchUrl, getBitbucketRequestOptions(config));
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const message = `Failed to retrieve default branch from ${branchUrl}, ${response.status} ${response.statusText}`;
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
let defaultBranch;
|
||||
if (isHosted) {
|
||||
const repoInfo = await response.json();
|
||||
defaultBranch = repoInfo.mainbranch.name;
|
||||
} else {
|
||||
const { displayId } = await response.json();
|
||||
defaultBranch = displayId;
|
||||
}
|
||||
if (!defaultBranch) {
|
||||
throw new Error(
|
||||
`Failed to read default branch from ${branchUrl}. ` +
|
||||
`Response ${response.status} ${response.json()}`,
|
||||
);
|
||||
}
|
||||
return defaultBranch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a URL pointing to a path on a provider, returns a URL that is suitable
|
||||
* for downloading the subtree.
|
||||
*
|
||||
* @param url - A URL pointing to a path
|
||||
* @param config - The relevant provider config
|
||||
* @public
|
||||
* @deprecated no longer in use, bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer.
|
||||
*/
|
||||
export async function getBitbucketDownloadUrl(
|
||||
url: string,
|
||||
config: BitbucketIntegrationConfig,
|
||||
): Promise<string> {
|
||||
const {
|
||||
name: repoName,
|
||||
owner: project,
|
||||
ref,
|
||||
protocol,
|
||||
resource,
|
||||
filepath,
|
||||
} = parseGitUrl(url);
|
||||
|
||||
const isHosted = resource === 'bitbucket.org';
|
||||
|
||||
let branch = ref;
|
||||
if (!branch) {
|
||||
branch = await getBitbucketDefaultBranch(url, config);
|
||||
}
|
||||
// path will limit the downloaded content
|
||||
// /docs will only download the docs folder and everything below it
|
||||
// /docs/index.md will download the docs folder and everything below it
|
||||
const path = filepath
|
||||
? `&path=${encodeURIComponent(decodeURIComponent(filepath))}`
|
||||
: '';
|
||||
const archiveUrl = isHosted
|
||||
? `${protocol}://${resource}/${project}/${repoName}/get/${branch}.tar.gz`
|
||||
: `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/archive?format=tgz&at=${branch}&prefix=${project}-${repoName}${path}`;
|
||||
|
||||
return archiveUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a URL pointing to a file on a provider, returns a URL that is suitable
|
||||
* for fetching the contents of the data.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Converts
|
||||
* from: https://bitbucket.org/orgname/reponame/src/master/file.yaml
|
||||
* to: https://api.bitbucket.org/2.0/repositories/orgname/reponame/src/master/file.yaml
|
||||
*
|
||||
* @param url - A URL pointing to a file
|
||||
* @param config - The relevant provider config
|
||||
* @public
|
||||
* @deprecated no longer in use, bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer.
|
||||
*/
|
||||
export function getBitbucketFileFetchUrl(
|
||||
url: string,
|
||||
config: BitbucketIntegrationConfig,
|
||||
): string {
|
||||
try {
|
||||
const { owner, name, ref, filepathtype, filepath } = parseGitUrl(url);
|
||||
if (
|
||||
!owner ||
|
||||
!name ||
|
||||
(filepathtype !== 'browse' &&
|
||||
filepathtype !== 'raw' &&
|
||||
filepathtype !== 'src')
|
||||
) {
|
||||
throw new Error('Invalid Bitbucket URL or file path');
|
||||
}
|
||||
|
||||
const pathWithoutSlash = filepath.replace(/^\//, '');
|
||||
|
||||
if (config.host === 'bitbucket.org') {
|
||||
if (!ref) {
|
||||
throw new Error('Invalid Bitbucket URL or file path');
|
||||
}
|
||||
return `${config.apiBaseUrl}/repositories/${owner}/${name}/src/${ref}/${pathWithoutSlash}`;
|
||||
}
|
||||
return `${config.apiBaseUrl}/projects/${owner}/repos/${name}/raw/${pathWithoutSlash}?at=${ref}`;
|
||||
} catch (e) {
|
||||
throw new Error(`Incorrect URL: ${url}, ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request options necessary to make requests to a given provider.
|
||||
*
|
||||
* @param config - The relevant provider config
|
||||
* @public
|
||||
* @deprecated no longer in use, bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer.
|
||||
*/
|
||||
export function getBitbucketRequestOptions(
|
||||
config: BitbucketIntegrationConfig,
|
||||
): { headers: Record<string, string> } {
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
if (config.token) {
|
||||
headers.Authorization = `Bearer ${config.token}`;
|
||||
} else if (config.username && config.appPassword) {
|
||||
const buffer = Buffer.from(
|
||||
`${config.username}:${config.appPassword}`,
|
||||
'utf8',
|
||||
);
|
||||
headers.Authorization = `Basic ${buffer.toString('base64')}`;
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
};
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { BitbucketIntegration } from './BitbucketIntegration';
|
||||
export {
|
||||
readBitbucketIntegrationConfig,
|
||||
readBitbucketIntegrationConfigs,
|
||||
} from './config';
|
||||
export type { BitbucketIntegrationConfig } from './config';
|
||||
export {
|
||||
getBitbucketDefaultBranch,
|
||||
getBitbucketDownloadUrl,
|
||||
getBitbucketFileFetchUrl,
|
||||
getBitbucketRequestOptions,
|
||||
} from './core';
|
||||
@@ -20,7 +20,6 @@ import fetch from 'cross-fetch';
|
||||
import { registerMswTestHooks } from '../helpers';
|
||||
import { GerritIntegrationConfig } from './config';
|
||||
import {
|
||||
buildGerritGitilesArchiveUrl,
|
||||
buildGerritGitilesArchiveUrlFromLocation,
|
||||
buildGerritGitilesUrl,
|
||||
getGerritBranchApiUrl,
|
||||
@@ -28,7 +27,6 @@ import {
|
||||
getGerritRequestOptions,
|
||||
parseGerritJsonResponse,
|
||||
parseGitilesUrlRef,
|
||||
parseGerritGitilesUrl,
|
||||
getGerritFileContentsApiUrl,
|
||||
} from './core';
|
||||
|
||||
@@ -36,86 +34,6 @@ describe('gerrit core', () => {
|
||||
const worker = setupServer();
|
||||
registerMswTestHooks(worker);
|
||||
|
||||
describe('buildGerritGitilesArchiveUrl', () => {
|
||||
const config: GerritIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
baseUrl: 'https://gerrit.com',
|
||||
gitilesBaseUrl: 'https://gerrit.com/gitiles',
|
||||
};
|
||||
const configWithPath: GerritIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
baseUrl: 'https://gerrit.com/gerrit',
|
||||
gitilesBaseUrl: 'https://gerrit.com/gerrit/plugins/gitiles',
|
||||
};
|
||||
const configWithDedicatedGitiles: GerritIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
baseUrl: 'https://gerrit.com/gerrit',
|
||||
gitilesBaseUrl: 'https://dedicated-gitiles-server.com/gerrit/gitiles',
|
||||
};
|
||||
it('can create an archive url for a branch', () => {
|
||||
expect(buildGerritGitilesArchiveUrl(config, 'repo', 'dev', '')).toEqual(
|
||||
'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev.tar.gz',
|
||||
);
|
||||
|
||||
expect(buildGerritGitilesArchiveUrl(config, 'repo', 'dev', '/')).toEqual(
|
||||
'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev.tar.gz',
|
||||
);
|
||||
});
|
||||
it('can create an archive url for a specific directory', () => {
|
||||
expect(
|
||||
buildGerritGitilesArchiveUrl(config, 'repo', 'dev', 'docs'),
|
||||
).toEqual(
|
||||
'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz',
|
||||
);
|
||||
});
|
||||
it('can create an authenticated url when auth is enabled', () => {
|
||||
const authConfig = {
|
||||
...config,
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
};
|
||||
expect(
|
||||
buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'),
|
||||
).toEqual(
|
||||
'https://gerrit.com/a/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz',
|
||||
);
|
||||
});
|
||||
it('can create an authenticated url when auth is enabled and an url-path is used', () => {
|
||||
const authConfig = {
|
||||
...configWithPath,
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
};
|
||||
expect(
|
||||
buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'),
|
||||
).toEqual(
|
||||
'https://gerrit.com/gerrit/a/plugins/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz',
|
||||
);
|
||||
});
|
||||
it('Cannot build an authenticated url when a dedicated Gitiles server is used', () => {
|
||||
const authConfig = {
|
||||
...configWithDedicatedGitiles,
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
};
|
||||
expect(() =>
|
||||
buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'),
|
||||
).toThrow(
|
||||
'Since the baseUrl (Gerrit) is not part of the gitilesBaseUrl, an authentication URL could not be constructed.',
|
||||
);
|
||||
});
|
||||
it('Build a non-authenticated url when a dedicated Gitiles server is used', () => {
|
||||
const authConfig = {
|
||||
...configWithDedicatedGitiles,
|
||||
};
|
||||
expect(
|
||||
buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'),
|
||||
).toEqual(
|
||||
'https://dedicated-gitiles-server.com/gerrit/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildGerritGitilesArchiveUrlFromLocation', () => {
|
||||
const config: GerritIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
@@ -238,6 +156,7 @@ describe('gerrit core', () => {
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseGitilesUrlRef', () => {
|
||||
const config: GerritIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
@@ -360,64 +279,6 @@ describe('gerrit core', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('parseGerritGitilesUrl', () => {
|
||||
it('can parse a valid gitiles urls.', () => {
|
||||
const config: GerritIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
gitilesBaseUrl: 'https://gerrit.com/gitiles',
|
||||
};
|
||||
const { branch, filePath, project } = parseGerritGitilesUrl(
|
||||
config,
|
||||
'https://gerrit.com/gitiles/web/project/+/refs/heads/master/README.md',
|
||||
);
|
||||
expect(project).toEqual('web/project');
|
||||
expect(branch).toEqual('master');
|
||||
expect(filePath).toEqual('README.md');
|
||||
|
||||
const { filePath: rootPath } = parseGerritGitilesUrl(
|
||||
config,
|
||||
'https://gerrit.com/gitiles/web/project/+/refs/heads/master',
|
||||
);
|
||||
expect(rootPath).toEqual('/');
|
||||
});
|
||||
it('can parse a valid authenticated gitiles url.', () => {
|
||||
const config: GerritIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
gitilesBaseUrl: 'https://gerrit.com/gitiles',
|
||||
};
|
||||
const { branch, filePath, project } = parseGerritGitilesUrl(
|
||||
config,
|
||||
'https://gerrit.com/a/gitiles/web/project/+/refs/heads/master/README.md',
|
||||
);
|
||||
expect(project).toEqual('web/project');
|
||||
expect(branch).toEqual('master');
|
||||
expect(filePath).toEqual('README.md');
|
||||
|
||||
const { filePath: rootPath } = parseGerritGitilesUrl(
|
||||
config,
|
||||
'https://gerrit.com/gitiles/web/project/+/refs/heads/master',
|
||||
);
|
||||
expect(rootPath).toEqual('/');
|
||||
});
|
||||
it('throws on incorrect gitiles urls.', () => {
|
||||
const config: GerritIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
gitilesBaseUrl: 'https://gerrit.com',
|
||||
};
|
||||
expect(() =>
|
||||
parseGerritGitilesUrl(
|
||||
config,
|
||||
'https://gerrit.com/+/refs/heads/master/README.md',
|
||||
),
|
||||
).toThrow(/project/);
|
||||
expect(() =>
|
||||
parseGerritGitilesUrl(
|
||||
config,
|
||||
'https://gerrit.com/web/project/+/refs/changes/1/11/master/README.md',
|
||||
),
|
||||
).toThrow(/branch/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGerritBranchApiUrl', () => {
|
||||
it('can create an url for anonymous access.', () => {
|
||||
|
||||
@@ -18,70 +18,6 @@ import { GerritIntegrationConfig } from './config';
|
||||
|
||||
const GERRIT_BODY_PREFIX = ")]}'";
|
||||
|
||||
/**
|
||||
* Parse a Gitiles URL and return branch, file path and project.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Gerrit only handles code reviews so it does not have a native way to browse
|
||||
* or showing the content of gits. Image if Github only had the "pull requests"
|
||||
* tab.
|
||||
*
|
||||
* Any source code browsing is instead handled by optional services outside
|
||||
* Gerrit. The url format chosen for the Gerrit url reader is the one used by
|
||||
* the Gitiles project. Gerrit will work perfectly with Backstage without
|
||||
* having Gitiles installed but there are some places in the Backstage GUI
|
||||
* with links to the url used by the url reader. These will not work unless
|
||||
* the urls point to an actual Gitiles installation.
|
||||
*
|
||||
* Gitiles url:
|
||||
* https://g.com/optional_path/\{project\}/+/refs/heads/\{branch\}/\{filePath\}
|
||||
* https://g.com/a/optional_path/\{project\}/+/refs/heads/\{branch\}/\{filePath\}
|
||||
*
|
||||
*
|
||||
* @param url - An URL pointing to a file stored in git.
|
||||
* @public
|
||||
* @deprecated `parseGerritGitilesUrl` is deprecated. Use
|
||||
* {@link parseGitilesUrlRef} instead.
|
||||
*/
|
||||
export function parseGerritGitilesUrl(
|
||||
config: GerritIntegrationConfig,
|
||||
url: string,
|
||||
): { branch: string; filePath: string; project: string } {
|
||||
const baseUrlParse = new URL(config.gitilesBaseUrl!);
|
||||
const urlParse = new URL(url);
|
||||
|
||||
// Remove the gerrit authentication prefix '/a/' from the url
|
||||
// In case of the gitilesBaseUrl is https://review.gerrit.com/plugins/gitiles
|
||||
// and the url provided is https://review.gerrit.com/a/plugins/gitiles/...
|
||||
// remove the prefix only if the pathname start with '/a/'
|
||||
const urlPath = urlParse.pathname
|
||||
.substring(urlParse.pathname.startsWith('/a/') ? 2 : 0)
|
||||
.replace(baseUrlParse.pathname, '');
|
||||
|
||||
const parts = urlPath.split('/').filter(p => !!p);
|
||||
|
||||
const projectEndIndex = parts.indexOf('+');
|
||||
|
||||
if (projectEndIndex <= 0) {
|
||||
throw new Error(`Unable to parse project from url: ${url}`);
|
||||
}
|
||||
const project = trimStart(parts.slice(0, projectEndIndex).join('/'), '/');
|
||||
|
||||
const branchIndex = parts.indexOf('heads');
|
||||
if (branchIndex <= 0) {
|
||||
throw new Error(`Unable to parse branch from url: ${url}`);
|
||||
}
|
||||
const branch = parts[branchIndex + 1];
|
||||
const filePath = parts.slice(branchIndex + 2).join('/');
|
||||
|
||||
return {
|
||||
branch,
|
||||
filePath: filePath === '' ? '/' : filePath,
|
||||
project,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses Gitiles urls and returns the following:
|
||||
*
|
||||
@@ -231,30 +167,6 @@ export function buildGerritEditUrl(
|
||||
)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a Gerrit Gitiles archive url that targets a specific branch and path
|
||||
*
|
||||
* @param config - A Gerrit provider config.
|
||||
* @param project - The name of the git project
|
||||
* @param branch - The branch we will target.
|
||||
* @param filePath - The absolute file path.
|
||||
* @public
|
||||
* @deprecated `buildGerritGitilesArchiveUrl` is deprecated. Use
|
||||
* {@link buildGerritGitilesArchiveUrlFromLocation} instead.
|
||||
*/
|
||||
export function buildGerritGitilesArchiveUrl(
|
||||
config: GerritIntegrationConfig,
|
||||
project: string,
|
||||
branch: string,
|
||||
filePath: string,
|
||||
): string {
|
||||
const archiveName =
|
||||
filePath === '/' || filePath === '' ? '.tar.gz' : `/${filePath}.tar.gz`;
|
||||
return `${getGitilesAuthenticationUrl(
|
||||
config,
|
||||
)}/${project}/+archive/refs/heads/${branch}${archiveName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a Gerrit Gitiles archive url from a Gitiles url.
|
||||
*
|
||||
@@ -350,11 +262,15 @@ export function getGerritBranchApiUrl(
|
||||
config: GerritIntegrationConfig,
|
||||
url: string,
|
||||
) {
|
||||
const { branch, project } = parseGerritGitilesUrl(config, url);
|
||||
const { ref, refType, project } = parseGitilesUrlRef(config, url);
|
||||
|
||||
if (refType !== 'branch') {
|
||||
throw new Error(`Unsupported gitiles ref type: ${refType}`);
|
||||
}
|
||||
|
||||
return `${config.baseUrl}${getAuthenticationPrefix(
|
||||
config,
|
||||
)}projects/${encodeURIComponent(project)}/branches/${branch}`;
|
||||
)}projects/${encodeURIComponent(project)}/branches/${ref}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -367,7 +283,7 @@ export function getGerritCloneRepoUrl(
|
||||
config: GerritIntegrationConfig,
|
||||
url: string,
|
||||
) {
|
||||
const { project } = parseGerritGitilesUrl(config, url);
|
||||
const { project } = parseGitilesUrlRef(config, url);
|
||||
|
||||
return `${config.cloneUrl}${getAuthenticationPrefix(config)}${project}`;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ export {
|
||||
readGerritIntegrationConfigs,
|
||||
} from './config';
|
||||
export {
|
||||
buildGerritGitilesArchiveUrl,
|
||||
buildGerritGitilesArchiveUrlFromLocation,
|
||||
getGitilesAuthenticationUrl,
|
||||
getGerritBranchApiUrl,
|
||||
@@ -28,7 +27,6 @@ export {
|
||||
getGerritProjectsApiUrl,
|
||||
getGerritRequestOptions,
|
||||
parseGerritJsonResponse,
|
||||
parseGerritGitilesUrl,
|
||||
parseGitilesUrlRef,
|
||||
} from './core';
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { GithubIntegrationConfig } from './config';
|
||||
import { getGithubFileFetchUrl, getGitHubRequestOptions } from './core';
|
||||
import { getGithubFileFetchUrl } from './core';
|
||||
import { GithubCredentials } from './types';
|
||||
|
||||
describe('github core', () => {
|
||||
@@ -35,28 +35,6 @@ describe('github core', () => {
|
||||
type: 'token',
|
||||
};
|
||||
|
||||
describe('getGitHubRequestOptions', () => {
|
||||
it('inserts a token when needed', () => {
|
||||
const withToken: GithubIntegrationConfig = {
|
||||
host: '',
|
||||
rawBaseUrl: '',
|
||||
token: 'A',
|
||||
};
|
||||
const withoutToken: GithubIntegrationConfig = {
|
||||
host: '',
|
||||
rawBaseUrl: '',
|
||||
};
|
||||
expect(
|
||||
(getGitHubRequestOptions(withToken, appCredentials).headers as any)
|
||||
.Authorization,
|
||||
).toEqual('token A');
|
||||
expect(
|
||||
(getGitHubRequestOptions(withoutToken, noCredentials).headers as any)
|
||||
.Authorization,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGithubFileFetchUrl', () => {
|
||||
it('rejects targets that do not look like URLs', () => {
|
||||
const config: GithubIntegrationConfig = { host: '', apiBaseUrl: '' };
|
||||
|
||||
@@ -63,30 +63,6 @@ export function getGithubFileFetchUrl(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request options necessary to make requests to a given provider.
|
||||
*
|
||||
* @deprecated This function is no longer used internally
|
||||
* @param config - The relevant provider config
|
||||
* @public
|
||||
*/
|
||||
export function getGitHubRequestOptions(
|
||||
config: GithubIntegrationConfig,
|
||||
credentials: GithubCredentials,
|
||||
): { headers: Record<string, string> } {
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
if (chooseEndpoint(config, credentials) === 'api') {
|
||||
headers.Accept = 'application/vnd.github.v3.raw';
|
||||
}
|
||||
|
||||
if (credentials.token) {
|
||||
headers.Authorization = `token ${credentials.token}`;
|
||||
}
|
||||
|
||||
return { headers };
|
||||
}
|
||||
|
||||
export function chooseEndpoint(
|
||||
config: GithubIntegrationConfig,
|
||||
credentials: GithubCredentials,
|
||||
|
||||
@@ -19,7 +19,7 @@ export {
|
||||
readGithubIntegrationConfigs,
|
||||
} from './config';
|
||||
export type { GithubAppConfig, GithubIntegrationConfig } from './config';
|
||||
export { getGithubFileFetchUrl, getGitHubRequestOptions } from './core';
|
||||
export { getGithubFileFetchUrl } from './core';
|
||||
export { DefaultGithubCredentialsProvider } from './DefaultGithubCredentialsProvider';
|
||||
export {
|
||||
GithubAppCredentialsMux,
|
||||
|
||||
@@ -24,7 +24,6 @@ export * from './awsS3';
|
||||
export * from './awsCodeCommit';
|
||||
export * from './azureBlobStorage';
|
||||
export * from './azure';
|
||||
export * from './bitbucket';
|
||||
export * from './bitbucketCloud';
|
||||
export * from './bitbucketServer';
|
||||
export * from './gerrit';
|
||||
|
||||
@@ -19,7 +19,6 @@ import { AwsS3Integration } from './awsS3/AwsS3Integration';
|
||||
import { AwsCodeCommitIntegration } from './awsCodeCommit';
|
||||
import { AzureIntegration } from './azure/AzureIntegration';
|
||||
import { BitbucketCloudIntegration } from './bitbucketCloud/BitbucketCloudIntegration';
|
||||
import { BitbucketIntegration } from './bitbucket/BitbucketIntegration';
|
||||
import { BitbucketServerIntegration } from './bitbucketServer/BitbucketServerIntegration';
|
||||
import { GerritIntegration } from './gerrit/GerritIntegration';
|
||||
import { GithubIntegration } from './github/GithubIntegration';
|
||||
@@ -39,10 +38,6 @@ export interface ScmIntegrationRegistry
|
||||
awsCodeCommit: ScmIntegrationsGroup<AwsCodeCommitIntegration>;
|
||||
azureBlobStorage: ScmIntegrationsGroup<AzureBlobStorageIntergation>;
|
||||
azure: ScmIntegrationsGroup<AzureIntegration>;
|
||||
/**
|
||||
* @deprecated in favor of `bitbucketCloud` and `bitbucketServer`
|
||||
*/
|
||||
bitbucket: ScmIntegrationsGroup<BitbucketIntegration>;
|
||||
bitbucketCloud: ScmIntegrationsGroup<BitbucketCloudIntegration>;
|
||||
bitbucketServer: ScmIntegrationsGroup<BitbucketServerIntegration>;
|
||||
gerrit: ScmIntegrationsGroup<GerritIntegration>;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
# @backstage/plugin-scaffolder-backend-module-bitbucket
|
||||
|
||||
**Deprecated!**
|
||||
|
||||
Please use one of the following modules instead:
|
||||
|
||||
- [@backstage/plugin-scaffolder-backend-module-bitbucket-cloud](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-bitbucket-cloud)
|
||||
- [@backstage/plugin-scaffolder-backend-module-bitbucket-server](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-bitbucket-server).
|
||||
@@ -1,10 +0,0 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: backstage-plugin-scaffolder-backend-module-bitbucket
|
||||
title: '@backstage/plugin-scaffolder-backend-module-bitbucket'
|
||||
description: The bitbucket module for @backstage/plugin-scaffolder-backend
|
||||
spec:
|
||||
lifecycle: experimental
|
||||
type: backstage-backend-plugin-module
|
||||
owner: maintainers
|
||||
@@ -1,8 +0,0 @@
|
||||
# Knip report
|
||||
|
||||
## Unused dependencies (1)
|
||||
|
||||
| Name | Location | Severity |
|
||||
| :------- | :----------- | :------- |
|
||||
| fs-extra | plugins/scaffolder-backend-module-bitbucket/package.json | error |
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"name": "@backstage/plugin-scaffolder-backend-module-bitbucket",
|
||||
"version": "0.3.20-next.0",
|
||||
"description": "The bitbucket module for @backstage/plugin-scaffolder-backend",
|
||||
"backstage": {
|
||||
"role": "backend-plugin-module",
|
||||
"pluginId": "scaffolder",
|
||||
"pluginPackage": "@backstage/plugin-scaffolder-backend"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "plugins/scaffolder-backend-module-bitbucket"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"package.json": [
|
||||
"package.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"clean": "backstage-cli package clean",
|
||||
"lint": "backstage-cli package lint",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"start": "backstage-cli package start",
|
||||
"test": "backstage-cli package test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/integration": "workspace:^",
|
||||
"@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "workspace:^",
|
||||
"@backstage/plugin-scaffolder-backend-module-bitbucket-server": "workspace:^",
|
||||
"@backstage/plugin-scaffolder-node": "workspace:^",
|
||||
"fs-extra": "^11.2.0",
|
||||
"yaml": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/plugin-scaffolder-node-test-utils": "workspace:^",
|
||||
"msw": "^1.0.0"
|
||||
},
|
||||
"deprecated": true
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
## API Report File for "@backstage/plugin-scaffolder-backend-module-bitbucket"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { BackendFeature } from '@backstage/backend-plugin-api';
|
||||
import * as bitbucketCloud from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud';
|
||||
import * as bitbucketServer from '@backstage/plugin-scaffolder-backend-module-bitbucket-server';
|
||||
import { Config } from '@backstage/config';
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
|
||||
|
||||
// @public @deprecated
|
||||
const bitbucketModule: BackendFeature;
|
||||
export default bitbucketModule;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export const createBitbucketPipelinesRunAction: (options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
}) => TemplateAction<
|
||||
{
|
||||
workspace: string;
|
||||
repo_slug: string;
|
||||
body?:
|
||||
| {
|
||||
target?:
|
||||
| {
|
||||
type?: string | undefined;
|
||||
source?: string | undefined;
|
||||
selector?:
|
||||
| {
|
||||
type: string;
|
||||
pattern: string;
|
||||
}
|
||||
| undefined;
|
||||
pull_request?:
|
||||
| {
|
||||
id: string;
|
||||
}
|
||||
| undefined;
|
||||
commit?:
|
||||
| {
|
||||
type: string;
|
||||
hash: string;
|
||||
}
|
||||
| undefined;
|
||||
destination?: string | undefined;
|
||||
ref_name?: string | undefined;
|
||||
ref_type?: string | undefined;
|
||||
destination_commit?:
|
||||
| {
|
||||
hash: string;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
| undefined;
|
||||
variables?:
|
||||
| {
|
||||
key: string;
|
||||
value: string;
|
||||
secured: boolean;
|
||||
}[]
|
||||
| undefined;
|
||||
}
|
||||
| undefined;
|
||||
token?: string | undefined;
|
||||
},
|
||||
{
|
||||
buildNumber?: number | undefined;
|
||||
repoUrl?: string | undefined;
|
||||
pipelinesUrl?: string | undefined;
|
||||
},
|
||||
'v2'
|
||||
>;
|
||||
|
||||
// @public @deprecated
|
||||
export function createPublishBitbucketAction(options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
config: Config;
|
||||
}): TemplateAction<
|
||||
{
|
||||
repoUrl: string;
|
||||
description?: string | undefined;
|
||||
repoVisibility?: 'private' | 'public' | undefined;
|
||||
defaultBranch?: string | undefined;
|
||||
sourcePath?: string | undefined;
|
||||
enableLFS?: boolean | undefined;
|
||||
token?: string | undefined;
|
||||
gitCommitMessage?: string | undefined;
|
||||
gitAuthorName?: string | undefined;
|
||||
gitAuthorEmail?: string | undefined;
|
||||
signCommit?: boolean | undefined;
|
||||
},
|
||||
{
|
||||
remoteUrl?: string | undefined;
|
||||
repoContentsUrl?: string | undefined;
|
||||
commitHash?: string | undefined;
|
||||
},
|
||||
'v2'
|
||||
>;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export const createPublishBitbucketCloudAction: typeof bitbucketCloud.createPublishBitbucketCloudAction;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export const createPublishBitbucketServerAction: typeof bitbucketServer.createPublishBitbucketServerAction;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export const createPublishBitbucketServerPullRequestAction: typeof bitbucketServer.createPublishBitbucketServerPullRequestAction;
|
||||
```
|
||||
@@ -1,368 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
jest.mock('@backstage/plugin-scaffolder-node', () => {
|
||||
return {
|
||||
...jest.requireActual('@backstage/plugin-scaffolder-node'),
|
||||
initRepoAndPush: jest.fn().mockResolvedValue({
|
||||
commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6',
|
||||
}),
|
||||
commitAndPushRepo: jest.fn().mockResolvedValue({
|
||||
commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6',
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
import { createPublishBitbucketAction } from './bitbucket';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { registerMswTestHooks } from '@backstage/backend-test-utils';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { initRepoAndPush } from '@backstage/plugin-scaffolder-node';
|
||||
import yaml from 'yaml';
|
||||
import { sep } from 'node:path';
|
||||
import { examples } from './bitbucket.examples';
|
||||
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
|
||||
|
||||
describe('publish:bitbucket', () => {
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
bitbucket: [
|
||||
{
|
||||
host: 'bitbucket.org',
|
||||
token: 'tokenlols',
|
||||
},
|
||||
{
|
||||
host: 'hosted.bitbucket.com',
|
||||
token: 'thing',
|
||||
apiBaseUrl: 'https://hosted.bitbucket.com/rest/api/1.0',
|
||||
},
|
||||
{
|
||||
host: 'notoken.bitbucket.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
const action = createPublishBitbucketAction({ integrations, config });
|
||||
const mockContext = createMockActionContext({
|
||||
input: {
|
||||
repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo',
|
||||
repoVisibility: 'private' as const,
|
||||
},
|
||||
});
|
||||
const server = setupServer();
|
||||
registerMswTestHooks(server);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should call initAndPush with the correct values', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/workspace/cloneurl',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
input: yaml.parse(examples[0].example).steps[0].input,
|
||||
});
|
||||
|
||||
expect(initRepoAndPush).toHaveBeenCalledWith({
|
||||
dir: mockContext.workspacePath,
|
||||
remoteUrl: 'https://bitbucket.org/workspace/cloneurl',
|
||||
defaultBranch: 'master',
|
||||
auth: { username: 'x-token-auth', password: 'tokenlols' },
|
||||
logger: mockContext.logger,
|
||||
commitMessage: 'initial commit',
|
||||
gitAuthorInfo: { email: undefined, name: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
it('should call initAndPush with the correct default branch', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/workspace/cloneurl',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
input: yaml.parse(examples[3].example).steps[0].input,
|
||||
});
|
||||
|
||||
expect(initRepoAndPush).toHaveBeenCalledWith({
|
||||
dir: mockContext.workspacePath,
|
||||
remoteUrl: 'https://bitbucket.org/workspace/cloneurl',
|
||||
defaultBranch: 'main',
|
||||
auth: { username: 'x-token-auth', password: 'tokenlols' },
|
||||
logger: mockContext.logger,
|
||||
commitMessage: 'initial commit',
|
||||
gitAuthorInfo: { email: undefined, name: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
it('should call initAndPush with the specified source path', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/workspace/cloneurl',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
input: yaml.parse(examples[4].example).steps[0].input,
|
||||
});
|
||||
expect(initRepoAndPush).toHaveBeenCalledWith({
|
||||
dir: `${mockContext.workspacePath}${sep}repoRoot`,
|
||||
remoteUrl: 'https://bitbucket.org/workspace/cloneurl',
|
||||
auth: { username: 'x-token-auth', password: 'tokenlols' },
|
||||
logger: mockContext.logger,
|
||||
defaultBranch: 'master',
|
||||
commitMessage: 'initial commit',
|
||||
gitAuthorInfo: { email: undefined, name: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
it('should call initAndPush with the authentication token', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/workspace/cloneurl',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
input: yaml.parse(examples[6].example).steps[0].input,
|
||||
});
|
||||
expect(initRepoAndPush).toHaveBeenCalledWith({
|
||||
dir: mockContext.workspacePath,
|
||||
remoteUrl: 'https://bitbucket.org/workspace/cloneurl',
|
||||
auth: { username: 'x-token-auth', password: 'your-auth-token' },
|
||||
logger: mockContext.logger,
|
||||
defaultBranch: 'master',
|
||||
commitMessage: 'initial commit',
|
||||
gitAuthorInfo: {
|
||||
email: undefined,
|
||||
name: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should call initAndPush with the custom commit message', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/workspace/cloneurl',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
input: yaml.parse(examples[7].example).steps[0].input,
|
||||
});
|
||||
|
||||
expect(initRepoAndPush).toHaveBeenCalledWith({
|
||||
dir: mockContext.workspacePath,
|
||||
remoteUrl: 'https://bitbucket.org/workspace/cloneurl',
|
||||
auth: { username: 'x-token-auth', password: 'tokenlols' },
|
||||
logger: mockContext.logger,
|
||||
defaultBranch: 'master',
|
||||
commitMessage: 'Initial commit with custom message',
|
||||
gitAuthorInfo: { email: undefined, name: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
it('should call initAndPush with the custom author name and email for the commit.', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/workspace/cloneurl',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
input: yaml.parse(examples[8].example).steps[0].input,
|
||||
});
|
||||
|
||||
expect(initRepoAndPush).toHaveBeenCalledWith({
|
||||
dir: mockContext.workspacePath,
|
||||
remoteUrl: 'https://bitbucket.org/workspace/cloneurl',
|
||||
auth: { username: 'x-token-auth', password: 'tokenlols' },
|
||||
logger: mockContext.logger,
|
||||
defaultBranch: 'master',
|
||||
commitMessage: 'initial commit',
|
||||
gitAuthorInfo: { email: 'your.email@example.com', name: 'Your Name' },
|
||||
});
|
||||
});
|
||||
|
||||
describe('LFS for hosted bitbucket', () => {
|
||||
const repoCreationResponse = {
|
||||
links: {
|
||||
self: [
|
||||
{
|
||||
href: 'https://bitbucket.mycompany.com/projects/project/repos/repo',
|
||||
},
|
||||
],
|
||||
clone: [
|
||||
{
|
||||
name: 'http',
|
||||
href: 'https://bitbucket.mycompany.com/scm/project/repo',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
it('should call the correct APIs to enable LFS if requested and the host is hosted bitbucket', async () => {
|
||||
expect.assertions(1);
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos',
|
||||
(_, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(201),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json(repoCreationResponse),
|
||||
);
|
||||
},
|
||||
),
|
||||
rest.put(
|
||||
'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('Authorization')).toBe('Bearer thing');
|
||||
return res(ctx.status(204));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
input: yaml.parse(examples[5].example).steps[0].input,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,203 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TemplateExample } from '@backstage/plugin-scaffolder-node';
|
||||
import yaml from 'yaml';
|
||||
|
||||
export const examples: TemplateExample[] = [
|
||||
{
|
||||
description:
|
||||
'Initializes a git repository with the content in the workspace, and publishes it to Bitbucket with the default configuration.',
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
id: 'publish',
|
||||
action: 'publish:bitbucket',
|
||||
name: 'Publish to Bitbucket',
|
||||
input: {
|
||||
repoUrl:
|
||||
'bitbucket.org?repo=repo&workspace=workspace&project=project',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
description: 'Initializes a Bitbucket repository with a description.',
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
id: 'publish',
|
||||
action: 'publish:bitbucket',
|
||||
name: 'Publish to Bitbucket',
|
||||
input: {
|
||||
repoUrl:
|
||||
'bitbucket.org?repo=repo&workspace=workspace&project=project',
|
||||
description: 'Initialize a git repository',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Initializes a Bitbucket repository with public repo visibility, if not set defaults to private',
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
id: 'publish',
|
||||
action: 'publish:bitbucket',
|
||||
name: 'Publish to Bitbucket',
|
||||
input: {
|
||||
repoUrl:
|
||||
'bitbucket.org?repo=repo&workspace=workspace&project=project',
|
||||
repoVisibility: 'public',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Initializes a Bitbucket repository with a default branch, if not set defaults to master',
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
id: 'publish',
|
||||
action: 'publish:bitbucket',
|
||||
name: 'Publish to Bitbucket',
|
||||
input: {
|
||||
repoUrl:
|
||||
'bitbucket.org?repo=repo&workspace=workspace&project=project',
|
||||
defaultBranch: 'main',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository',
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
id: 'publish',
|
||||
action: 'publish:bitbucket',
|
||||
name: 'Publish to Bitbucket',
|
||||
input: {
|
||||
repoUrl:
|
||||
'bitbucket.org?repo=repo&workspace=workspace&project=project',
|
||||
sourcePath: './repoRoot',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
description: 'Initializes a Bitbucket repository with LFS enabled',
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
id: 'publish',
|
||||
action: 'publish:bitbucket',
|
||||
name: 'Publish to Bitbucket',
|
||||
input: {
|
||||
repoUrl: 'hosted.bitbucket.com?project=project&repo=repo',
|
||||
enableLFS: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Initializes a Bitbucket repository with a custom authentication token',
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
id: 'publish',
|
||||
action: 'publish:bitbucket',
|
||||
name: 'Publish to Bitbucket',
|
||||
input: {
|
||||
repoUrl:
|
||||
'bitbucket.org?repo=repo&workspace=workspace&project=project',
|
||||
token: 'your-auth-token',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Initializes a Bitbucket repository with an initial commit message, if not set defaults to `initial commit`',
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
id: 'publish',
|
||||
action: 'publish:bitbucket',
|
||||
name: 'Publish to Bitbucket',
|
||||
input: {
|
||||
repoUrl:
|
||||
'bitbucket.org?repo=repo&workspace=workspace&project=project',
|
||||
gitCommitMessage: 'Initial commit with custom message',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
description: 'Initializes a Bitbucket repository with a custom author',
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
id: 'publish',
|
||||
action: 'publish:bitbucket',
|
||||
name: 'Publish to Bitbucket',
|
||||
input: {
|
||||
repoUrl:
|
||||
'bitbucket.org?repo=repo&workspace=workspace&project=project',
|
||||
gitAuthorName: 'Your Name',
|
||||
gitAuthorEmail: 'your.email@example.com',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Initializes a Bitbucket repository with all properties being set',
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
id: 'publish',
|
||||
action: 'publish:bitbucket',
|
||||
name: 'Publish to Bitbucket',
|
||||
input: {
|
||||
repoUrl:
|
||||
'bitbucket.org?repo=repo&workspace=workspace&project=project',
|
||||
description: 'Initialize a git repository',
|
||||
repoVisibility: 'public',
|
||||
defaultBranch: 'main',
|
||||
token: 'your-auth-token',
|
||||
gitCommitMessage: 'Initial commit with custom message',
|
||||
gitAuthorName: 'Your Name',
|
||||
gitAuthorEmail: 'your.email@example.com',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
];
|
||||
@@ -1,619 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
jest.mock('@backstage/plugin-scaffolder-node', () => {
|
||||
return {
|
||||
...jest.requireActual('@backstage/plugin-scaffolder-node'),
|
||||
initRepoAndPush: jest.fn().mockResolvedValue({
|
||||
commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6',
|
||||
}),
|
||||
commitAndPushRepo: jest.fn().mockResolvedValue({
|
||||
commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6',
|
||||
}),
|
||||
};
|
||||
});
|
||||
import { createPublishBitbucketAction } from './bitbucket';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { registerMswTestHooks } from '@backstage/backend-test-utils';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { initRepoAndPush } from '@backstage/plugin-scaffolder-node';
|
||||
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
|
||||
|
||||
describe('publish:bitbucket', () => {
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
bitbucket: [
|
||||
{
|
||||
host: 'bitbucket.org',
|
||||
token: 'tokenlols',
|
||||
},
|
||||
{
|
||||
host: 'hosted.bitbucket.com',
|
||||
token: 'thing',
|
||||
apiBaseUrl: 'https://hosted.bitbucket.com/rest/api/1.0',
|
||||
},
|
||||
{
|
||||
host: 'notoken.bitbucket.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
const action = createPublishBitbucketAction({ integrations, config });
|
||||
const mockContext = createMockActionContext({
|
||||
input: {
|
||||
repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo',
|
||||
repoVisibility: 'private' as const,
|
||||
},
|
||||
});
|
||||
const server = setupServer();
|
||||
registerMswTestHooks(server);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should throw an error when the repoUrl is not well formed', async () => {
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: { repoUrl: 'bitbucket.org?project=project&repo=repo' },
|
||||
}),
|
||||
).rejects.toThrow(/missing workspace/);
|
||||
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: { repoUrl: 'bitbucket.org?workspace=workspace&repo=repo' },
|
||||
}),
|
||||
).rejects.toThrow(/missing project/);
|
||||
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: { repoUrl: 'bitbucket.org?workspace=workspace&project=project' },
|
||||
}),
|
||||
).rejects.toThrow(/missing repo/);
|
||||
});
|
||||
|
||||
it('should throw if there is no integration config provided', async () => {
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: {
|
||||
repoUrl: 'missing.com?workspace=workspace&project=project&repo=repo',
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(/No matching integration configuration/);
|
||||
});
|
||||
|
||||
it('should throw if there is no token in the integration config that is returned', async () => {
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: {
|
||||
repoUrl:
|
||||
'notoken.bitbucket.com?workspace=workspace&project=project&repo=repo',
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(/Authorization has not been provided for Bitbucket/);
|
||||
});
|
||||
|
||||
it('should call the correct APIs when the host is bitbucket cloud', async () => {
|
||||
expect.assertions(2);
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('Authorization')).toBe('Bearer tokenlols');
|
||||
expect(req.body).toEqual({
|
||||
is_private: true,
|
||||
scm: 'git',
|
||||
project: { key: 'project' },
|
||||
});
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
input: {
|
||||
...mockContext.input,
|
||||
repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should call the correct APIs when the host is hosted bitbucket', async () => {
|
||||
expect.assertions(2);
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('Authorization')).toBe('Bearer thing');
|
||||
expect(req.body).toEqual({ public: false, name: 'repo' });
|
||||
return res(
|
||||
ctx.status(201),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json({
|
||||
links: {
|
||||
self: [
|
||||
{
|
||||
href: 'https://bitbucket.mycompany.com/projects/project/repos/repo',
|
||||
},
|
||||
],
|
||||
clone: [
|
||||
{
|
||||
name: 'http',
|
||||
href: 'https://bitbucket.mycompany.com/scm/project/repo',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
input: {
|
||||
...mockContext.input,
|
||||
repoUrl: 'hosted.bitbucket.com?project=project&repo=repo',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should work if the token is provided through ctx.input', async () => {
|
||||
expect.assertions(2);
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://notoken.bitbucket.com/rest/api/1.0/projects/project/repos',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('Authorization')).toBe('Bearer lols');
|
||||
expect(req.body).toEqual({ public: false, name: 'repo' });
|
||||
return res(
|
||||
ctx.status(201),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json({
|
||||
links: {
|
||||
self: [
|
||||
{
|
||||
href: 'https://bitbucket.mycompany.com/projects/project/repos/repo',
|
||||
},
|
||||
],
|
||||
clone: [
|
||||
{
|
||||
name: 'http',
|
||||
href: 'https://bitbucket.mycompany.com/scm/project/repo',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
input: {
|
||||
repoUrl: 'notoken.bitbucket.com?project=project&repo=repo',
|
||||
token: 'lols',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('LFS for hosted bitbucket', () => {
|
||||
const repoCreationResponse = {
|
||||
links: {
|
||||
self: [
|
||||
{
|
||||
href: 'https://bitbucket.mycompany.com/projects/project/repos/repo',
|
||||
},
|
||||
],
|
||||
clone: [
|
||||
{
|
||||
name: 'http',
|
||||
href: 'https://bitbucket.mycompany.com/scm/project/repo',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
it('should call the correct APIs to enable LFS if requested and the host is hosted bitbucket', async () => {
|
||||
expect.assertions(1);
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos',
|
||||
(_, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(201),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json(repoCreationResponse),
|
||||
);
|
||||
},
|
||||
),
|
||||
rest.put(
|
||||
'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('Authorization')).toBe('Bearer thing');
|
||||
return res(ctx.status(204));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
input: {
|
||||
...mockContext.input,
|
||||
repoUrl: 'hosted.bitbucket.com?project=project&repo=repo',
|
||||
enableLFS: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should report an error if enabling LFS fails', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos',
|
||||
(_, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(201),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json(repoCreationResponse),
|
||||
);
|
||||
},
|
||||
),
|
||||
rest.put(
|
||||
'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled',
|
||||
(_, res, ctx) => {
|
||||
return res(ctx.status(500));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: {
|
||||
...mockContext.input,
|
||||
repoUrl: 'hosted.bitbucket.com?project=project&repo=repo',
|
||||
enableLFS: true,
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(/Failed to enable LFS/);
|
||||
});
|
||||
});
|
||||
|
||||
it('should call initAndPush with the correct values', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/workspace/cloneurl',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler(mockContext);
|
||||
|
||||
expect(initRepoAndPush).toHaveBeenCalledWith({
|
||||
dir: mockContext.workspacePath,
|
||||
remoteUrl: 'https://bitbucket.org/workspace/cloneurl',
|
||||
defaultBranch: 'master',
|
||||
auth: { username: 'x-token-auth', password: 'tokenlols' },
|
||||
logger: mockContext.logger,
|
||||
commitMessage: 'initial commit',
|
||||
gitAuthorInfo: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should call initAndPush with the correct default branch', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/workspace/cloneurl',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
input: {
|
||||
...mockContext.input,
|
||||
defaultBranch: 'main',
|
||||
},
|
||||
});
|
||||
|
||||
expect(initRepoAndPush).toHaveBeenCalledWith({
|
||||
dir: mockContext.workspacePath,
|
||||
remoteUrl: 'https://bitbucket.org/workspace/cloneurl',
|
||||
defaultBranch: 'main',
|
||||
auth: { username: 'x-token-auth', password: 'tokenlols' },
|
||||
logger: mockContext.logger,
|
||||
commitMessage: 'initial commit',
|
||||
gitAuthorInfo: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should call initAndPush with the configured defaultAuthor', async () => {
|
||||
const customAuthorConfig = new ConfigReader({
|
||||
integrations: {
|
||||
bitbucket: [
|
||||
{
|
||||
host: 'bitbucket.org',
|
||||
token: 'tokenlols',
|
||||
},
|
||||
{
|
||||
host: 'hosted.bitbucket.com',
|
||||
token: 'thing',
|
||||
apiBaseUrl: 'https://hosted.bitbucket.com/rest/api/1.0',
|
||||
},
|
||||
{
|
||||
host: 'notoken.bitbucket.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
scaffolder: {
|
||||
defaultAuthor: {
|
||||
name: 'Test',
|
||||
email: 'example@example.com',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const customAuthorIntegrations =
|
||||
ScmIntegrations.fromConfig(customAuthorConfig);
|
||||
const customAuthorAction = createPublishBitbucketAction({
|
||||
integrations: customAuthorIntegrations,
|
||||
config: customAuthorConfig,
|
||||
});
|
||||
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/workspace/cloneurl',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await customAuthorAction.handler(mockContext);
|
||||
|
||||
expect(initRepoAndPush).toHaveBeenCalledWith({
|
||||
dir: mockContext.workspacePath,
|
||||
remoteUrl: 'https://bitbucket.org/workspace/cloneurl',
|
||||
auth: { username: 'x-token-auth', password: 'tokenlols' },
|
||||
logger: mockContext.logger,
|
||||
defaultBranch: 'master',
|
||||
commitMessage: 'initial commit',
|
||||
gitAuthorInfo: { name: 'Test', email: 'example@example.com' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should call initAndPush with the configured defaultCommitMessage', async () => {
|
||||
const customAuthorConfig = new ConfigReader({
|
||||
integrations: {
|
||||
bitbucket: [
|
||||
{
|
||||
host: 'bitbucket.org',
|
||||
token: 'tokenlols',
|
||||
},
|
||||
{
|
||||
host: 'hosted.bitbucket.com',
|
||||
token: 'thing',
|
||||
apiBaseUrl: 'https://hosted.bitbucket.com/rest/api/1.0',
|
||||
},
|
||||
{
|
||||
host: 'notoken.bitbucket.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
scaffolder: {
|
||||
defaultCommitMessage: 'Test commit message',
|
||||
},
|
||||
});
|
||||
|
||||
const customAuthorIntegrations =
|
||||
ScmIntegrations.fromConfig(customAuthorConfig);
|
||||
const customAuthorAction = createPublishBitbucketAction({
|
||||
integrations: customAuthorIntegrations,
|
||||
config: customAuthorConfig,
|
||||
});
|
||||
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/workspace/cloneurl',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await customAuthorAction.handler(mockContext);
|
||||
|
||||
expect(initRepoAndPush).toHaveBeenCalledWith({
|
||||
dir: mockContext.workspacePath,
|
||||
remoteUrl: 'https://bitbucket.org/workspace/cloneurl',
|
||||
auth: { username: 'x-token-auth', password: 'tokenlols' },
|
||||
logger: mockContext.logger,
|
||||
defaultBranch: 'master',
|
||||
commitMessage: 'initial commit',
|
||||
gitAuthorInfo: { email: undefined, name: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
it('should call outputs with the correct urls', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/workspace/cloneurl',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler(mockContext);
|
||||
|
||||
expect(mockContext.output).toHaveBeenCalledWith(
|
||||
'remoteUrl',
|
||||
'https://bitbucket.org/workspace/cloneurl',
|
||||
);
|
||||
expect(mockContext.output).toHaveBeenCalledWith(
|
||||
'repoContentsUrl',
|
||||
'https://bitbucket.org/workspace/repo/src/master',
|
||||
);
|
||||
});
|
||||
|
||||
it('should call outputs with the correct urls with correct default branch', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/workspace/cloneurl',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
input: {
|
||||
...mockContext.input,
|
||||
defaultBranch: 'main',
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockContext.output).toHaveBeenCalledWith(
|
||||
'remoteUrl',
|
||||
'https://bitbucket.org/workspace/cloneurl',
|
||||
);
|
||||
expect(mockContext.output).toHaveBeenCalledWith(
|
||||
'repoContentsUrl',
|
||||
'https://bitbucket.org/workspace/repo/src/main',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,450 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { InputError } from '@backstage/errors';
|
||||
import {
|
||||
BitbucketIntegrationConfig,
|
||||
ScmIntegrationRegistry,
|
||||
} from '@backstage/integration';
|
||||
import {
|
||||
createTemplateAction,
|
||||
getRepoSourceDirectory,
|
||||
initRepoAndPush,
|
||||
parseRepoUrl,
|
||||
} from '@backstage/plugin-scaffolder-node';
|
||||
import { Config } from '@backstage/config';
|
||||
import { examples } from './bitbucket.examples';
|
||||
|
||||
const createBitbucketCloudRepository = async (opts: {
|
||||
workspace: string;
|
||||
project: string;
|
||||
repo: string;
|
||||
description?: string;
|
||||
repoVisibility: 'private' | 'public';
|
||||
mainBranch: string;
|
||||
authorization: string;
|
||||
apiBaseUrl: string;
|
||||
}) => {
|
||||
const {
|
||||
workspace,
|
||||
project,
|
||||
repo,
|
||||
description,
|
||||
repoVisibility,
|
||||
mainBranch,
|
||||
authorization,
|
||||
apiBaseUrl,
|
||||
} = opts;
|
||||
|
||||
const options: RequestInit = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
scm: 'git',
|
||||
description: description,
|
||||
is_private: repoVisibility === 'private',
|
||||
project: { key: project },
|
||||
}),
|
||||
headers: {
|
||||
Authorization: authorization,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(
|
||||
`${apiBaseUrl}/repositories/${workspace}/${repo}`,
|
||||
options,
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(`Unable to create repository, ${e}`);
|
||||
}
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(
|
||||
`Unable to create repository, ${response.status} ${
|
||||
response.statusText
|
||||
}, ${await response.text()}`,
|
||||
);
|
||||
}
|
||||
|
||||
const r = await response.json();
|
||||
let remoteUrl = '';
|
||||
for (const link of r.links.clone) {
|
||||
if (link.name === 'https') {
|
||||
remoteUrl = link.href;
|
||||
}
|
||||
}
|
||||
|
||||
// "mainbranch.name" cannot be set neither at create nor update of the repo
|
||||
// the first pushed branch will be set as "main branch" then
|
||||
const repoContentsUrl = `${r.links.html.href}/src/${mainBranch}`;
|
||||
return { remoteUrl, repoContentsUrl };
|
||||
};
|
||||
|
||||
const createBitbucketServerRepository = async (opts: {
|
||||
project: string;
|
||||
repo: string;
|
||||
description?: string;
|
||||
repoVisibility: 'private' | 'public';
|
||||
authorization: string;
|
||||
apiBaseUrl: string;
|
||||
}) => {
|
||||
const {
|
||||
project,
|
||||
repo,
|
||||
description,
|
||||
authorization,
|
||||
repoVisibility,
|
||||
apiBaseUrl,
|
||||
} = opts;
|
||||
|
||||
let response: Response;
|
||||
const options: RequestInit = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: repo,
|
||||
description: description,
|
||||
public: repoVisibility === 'public',
|
||||
}),
|
||||
headers: {
|
||||
Authorization: authorization,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
response = await fetch(`${apiBaseUrl}/projects/${project}/repos`, options);
|
||||
} catch (e) {
|
||||
throw new Error(`Unable to create repository, ${e}`);
|
||||
}
|
||||
|
||||
if (response.status !== 201) {
|
||||
throw new Error(
|
||||
`Unable to create repository, ${response.status} ${
|
||||
response.statusText
|
||||
}, ${await response.text()}`,
|
||||
);
|
||||
}
|
||||
|
||||
const r = await response.json();
|
||||
let remoteUrl = '';
|
||||
for (const link of r.links.clone) {
|
||||
if (link.name === 'http') {
|
||||
remoteUrl = link.href;
|
||||
}
|
||||
}
|
||||
|
||||
const repoContentsUrl = `${r.links.self[0].href}`;
|
||||
return { remoteUrl, repoContentsUrl };
|
||||
};
|
||||
|
||||
const getAuthorizationHeader = (config: BitbucketIntegrationConfig) => {
|
||||
if (config.username && (config.token ?? config.appPassword)) {
|
||||
const buffer = Buffer.from(
|
||||
`${config.username}:${config.token ?? config.appPassword}`,
|
||||
'utf8',
|
||||
);
|
||||
|
||||
return `Basic ${buffer.toString('base64')}`;
|
||||
}
|
||||
|
||||
if (config.token) {
|
||||
return `Bearer ${config.token}`;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Authorization has not been provided for Bitbucket. Please add either provide a username and token or username and appPassword to the Integrations config`,
|
||||
);
|
||||
};
|
||||
|
||||
const performEnableLFS = async (opts: {
|
||||
authorization: string;
|
||||
host: string;
|
||||
project: string;
|
||||
repo: string;
|
||||
}) => {
|
||||
const { authorization, host, project, repo } = opts;
|
||||
|
||||
const options: RequestInit = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: authorization,
|
||||
},
|
||||
};
|
||||
|
||||
const { ok, status, statusText } = await fetch(
|
||||
`https://${host}/rest/git-lfs/admin/projects/${project}/repos/${repo}/enabled`,
|
||||
options,
|
||||
);
|
||||
|
||||
if (!ok)
|
||||
throw new Error(
|
||||
`Failed to enable LFS in the repository, ${status}: ${statusText}`,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new action that initializes a git repository of the content in the workspace
|
||||
* and publishes it to Bitbucket.
|
||||
* @public
|
||||
* @deprecated in favor of "createPublishBitbucketCloudAction" by \@backstage/plugin-scaffolder-backend-module-bitbucket-cloud and "createPublishBitbucketServerAction" by \@backstage/plugin-scaffolder-backend-module-bitbucket-server
|
||||
*/
|
||||
export function createPublishBitbucketAction(options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
config: Config;
|
||||
}) {
|
||||
const { integrations, config } = options;
|
||||
|
||||
return createTemplateAction({
|
||||
id: 'publish:bitbucket',
|
||||
description:
|
||||
'Initializes a git repository of the content in the workspace, and publishes it to Bitbucket.',
|
||||
examples,
|
||||
schema: {
|
||||
input: {
|
||||
repoUrl: z =>
|
||||
z.string({
|
||||
description: 'Repository Location',
|
||||
}),
|
||||
description: z =>
|
||||
z
|
||||
.string({
|
||||
description: 'Repository Description',
|
||||
})
|
||||
.optional(),
|
||||
repoVisibility: z =>
|
||||
z
|
||||
.enum(['private', 'public'], {
|
||||
description: 'Repository Visibility',
|
||||
})
|
||||
.optional(),
|
||||
defaultBranch: z =>
|
||||
z
|
||||
.string({
|
||||
description: `Sets the default branch on the repository. The default value is 'master'`,
|
||||
})
|
||||
.optional(),
|
||||
sourcePath: z =>
|
||||
z
|
||||
.string({
|
||||
description:
|
||||
'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.',
|
||||
})
|
||||
.optional(),
|
||||
enableLFS: z =>
|
||||
z
|
||||
.boolean({
|
||||
description:
|
||||
'Enable LFS for the repository. Only available for hosted Bitbucket.',
|
||||
})
|
||||
.optional(),
|
||||
token: z =>
|
||||
z
|
||||
.string({
|
||||
description: 'The token to use for authorization to BitBucket',
|
||||
})
|
||||
.optional(),
|
||||
gitCommitMessage: z =>
|
||||
z
|
||||
.string({
|
||||
description: `Sets the commit message on the repository. The default value is 'initial commit'`,
|
||||
})
|
||||
.optional(),
|
||||
gitAuthorName: z =>
|
||||
z
|
||||
.string({
|
||||
description: `Sets the default author name for the commit. The default value is 'Scaffolder'`,
|
||||
})
|
||||
.optional(),
|
||||
gitAuthorEmail: z =>
|
||||
z
|
||||
.string({
|
||||
description: `Sets the default author email for the commit.`,
|
||||
})
|
||||
.optional(),
|
||||
signCommit: z =>
|
||||
z
|
||||
.boolean({
|
||||
description: 'Sign commit with configured PGP private key',
|
||||
})
|
||||
.optional(),
|
||||
},
|
||||
output: {
|
||||
remoteUrl: z =>
|
||||
z
|
||||
.string({
|
||||
description: 'A URL to the repository with the provider',
|
||||
})
|
||||
.optional(),
|
||||
repoContentsUrl: z =>
|
||||
z
|
||||
.string({
|
||||
description: 'A URL to the root of the repository',
|
||||
})
|
||||
.optional(),
|
||||
commitHash: z =>
|
||||
z
|
||||
.string({
|
||||
description: 'The git commit hash of the initial commit',
|
||||
})
|
||||
.optional(),
|
||||
},
|
||||
},
|
||||
async handler(ctx) {
|
||||
ctx.logger.warn(
|
||||
`[Deprecated] Please migrate the use of action "publish:bitbucket" to "publish:bitbucketCloud" or "publish:bitbucketServer".`,
|
||||
);
|
||||
const {
|
||||
repoUrl,
|
||||
description,
|
||||
defaultBranch = 'master',
|
||||
repoVisibility = 'private',
|
||||
enableLFS = false,
|
||||
gitCommitMessage = 'initial commit',
|
||||
gitAuthorName,
|
||||
gitAuthorEmail,
|
||||
signCommit,
|
||||
} = ctx.input;
|
||||
|
||||
const { workspace, project, repo, host } = parseRepoUrl(
|
||||
repoUrl,
|
||||
integrations,
|
||||
);
|
||||
|
||||
// Workspace is only required for bitbucket cloud
|
||||
if (host === 'bitbucket.org') {
|
||||
if (!workspace) {
|
||||
throw new InputError(
|
||||
`Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing workspace`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Project is required for both bitbucket cloud and bitbucket server
|
||||
if (!project) {
|
||||
throw new InputError(
|
||||
`Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing project`,
|
||||
);
|
||||
}
|
||||
|
||||
const integrationConfig = integrations.bitbucket.byHost(host);
|
||||
|
||||
if (!integrationConfig) {
|
||||
throw new InputError(
|
||||
`No matching integration configuration for host ${host}, please check your integrations config`,
|
||||
);
|
||||
}
|
||||
|
||||
const authorization = getAuthorizationHeader(
|
||||
ctx.input.token
|
||||
? {
|
||||
host: integrationConfig.config.host,
|
||||
apiBaseUrl: integrationConfig.config.apiBaseUrl,
|
||||
token: ctx.input.token,
|
||||
}
|
||||
: integrationConfig.config,
|
||||
);
|
||||
|
||||
const apiBaseUrl = integrationConfig.config.apiBaseUrl;
|
||||
|
||||
const createMethod =
|
||||
host === 'bitbucket.org'
|
||||
? createBitbucketCloudRepository
|
||||
: createBitbucketServerRepository;
|
||||
|
||||
const { remoteUrl, repoContentsUrl } = await ctx.checkpoint({
|
||||
key: `create.repo.${host}.${repo}`,
|
||||
fn: async () =>
|
||||
createMethod({
|
||||
authorization,
|
||||
workspace: workspace || '',
|
||||
project,
|
||||
repo,
|
||||
repoVisibility,
|
||||
mainBranch: defaultBranch,
|
||||
description,
|
||||
apiBaseUrl,
|
||||
}),
|
||||
});
|
||||
|
||||
const gitAuthorInfo = {
|
||||
name: gitAuthorName
|
||||
? gitAuthorName
|
||||
: config.getOptionalString('scaffolder.defaultAuthor.name'),
|
||||
email: gitAuthorEmail
|
||||
? gitAuthorEmail
|
||||
: config.getOptionalString('scaffolder.defaultAuthor.email'),
|
||||
};
|
||||
const signingKey =
|
||||
integrationConfig.config.commitSigningKey ??
|
||||
config.getOptionalString('scaffolder.defaultCommitSigningKey');
|
||||
if (signCommit && !signingKey) {
|
||||
throw new Error(
|
||||
'Signing commits is enabled but no signing key is provided in the configuration',
|
||||
);
|
||||
}
|
||||
|
||||
let auth;
|
||||
|
||||
if (ctx.input.token) {
|
||||
auth = {
|
||||
username: 'x-token-auth',
|
||||
password: ctx.input.token,
|
||||
};
|
||||
} else {
|
||||
auth = {
|
||||
username: integrationConfig.config.username
|
||||
? integrationConfig.config.username
|
||||
: 'x-token-auth',
|
||||
password: integrationConfig.config.appPassword
|
||||
? integrationConfig.config.appPassword
|
||||
: integrationConfig.config.token ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
const commitHash = await ctx.checkpoint({
|
||||
key: `init.repo.and.push${host}.${repo}`,
|
||||
fn: async () => {
|
||||
const commitResult = await initRepoAndPush({
|
||||
dir: getRepoSourceDirectory(
|
||||
ctx.workspacePath,
|
||||
ctx.input.sourcePath,
|
||||
),
|
||||
remoteUrl,
|
||||
auth,
|
||||
defaultBranch,
|
||||
logger: ctx.logger,
|
||||
commitMessage: gitCommitMessage
|
||||
? gitCommitMessage
|
||||
: config.getOptionalString('scaffolder.defaultCommitMessage'),
|
||||
gitAuthorInfo,
|
||||
signingKey: signCommit ? signingKey : undefined,
|
||||
});
|
||||
return commitResult?.commitHash;
|
||||
},
|
||||
});
|
||||
|
||||
if (enableLFS && host !== 'bitbucket.org') {
|
||||
await performEnableLFS({ authorization, host, project, repo });
|
||||
}
|
||||
|
||||
ctx.output('commitHash', commitHash);
|
||||
ctx.output('remoteUrl', remoteUrl);
|
||||
ctx.output('repoContentsUrl', repoContentsUrl);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as bitbucketCloud from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud';
|
||||
import * as bitbucketServer from '@backstage/plugin-scaffolder-backend-module-bitbucket-server';
|
||||
|
||||
export { createPublishBitbucketAction } from './actions/bitbucket';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-cloud` instead
|
||||
*/
|
||||
export const createPublishBitbucketCloudAction =
|
||||
bitbucketCloud.createPublishBitbucketCloudAction;
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-cloud` instead
|
||||
*/
|
||||
export const createBitbucketPipelinesRunAction =
|
||||
bitbucketCloud.createBitbucketPipelinesRunAction;
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead
|
||||
*/
|
||||
export const createPublishBitbucketServerAction =
|
||||
bitbucketServer.createPublishBitbucketServerAction;
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead
|
||||
*/
|
||||
export const createPublishBitbucketServerPullRequestAction =
|
||||
bitbucketServer.createPublishBitbucketServerPullRequestAction;
|
||||
@@ -1,24 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A module for the scaffolder backend that lets you interact with bitbucket
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export * from './deprecated';
|
||||
export { bitbucketModule as default } from './module';
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
coreServices,
|
||||
createBackendModule,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node';
|
||||
import {
|
||||
createBitbucketPipelinesRunAction,
|
||||
createPublishBitbucketCloudAction,
|
||||
createPublishBitbucketServerAction,
|
||||
createPublishBitbucketServerPullRequestAction,
|
||||
} from './deprecated';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
|
||||
/**
|
||||
* The Bitbucket Module for the Scaffolder Backend
|
||||
* @public
|
||||
* @deprecated use module by \@backstage/plugin-scaffolder-backend-module-bitbucket-cloud or \@backstage/plugin-scaffolder-backend-module-bitbucket-server instead
|
||||
*/
|
||||
export const bitbucketModule = createBackendModule({
|
||||
moduleId: 'bitbucket',
|
||||
pluginId: 'scaffolder',
|
||||
register({ registerInit }) {
|
||||
registerInit({
|
||||
deps: {
|
||||
scaffolder: scaffolderActionsExtensionPoint,
|
||||
config: coreServices.rootConfig,
|
||||
},
|
||||
async init({ scaffolder, config }) {
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
|
||||
scaffolder.addActions(
|
||||
createPublishBitbucketCloudAction({ integrations, config }),
|
||||
createPublishBitbucketServerAction({ integrations, config }),
|
||||
createPublishBitbucketServerPullRequestAction({
|
||||
integrations,
|
||||
config,
|
||||
}),
|
||||
createBitbucketPipelinesRunAction({ integrations }),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -125,13 +125,6 @@ export class ScaffolderClient implements ScaffolderApi {
|
||||
): Promise<ScaffolderGetIntegrationsListResponse> {
|
||||
const integrations = [
|
||||
...this.scmIntegrationsApi.azure.list(),
|
||||
...this.scmIntegrationsApi.bitbucket
|
||||
.list()
|
||||
.filter(
|
||||
item =>
|
||||
!this.scmIntegrationsApi.bitbucketCloud.byHost(item.config.host) &&
|
||||
!this.scmIntegrationsApi.bitbucketServer.byHost(item.config.host),
|
||||
),
|
||||
...this.scmIntegrationsApi.bitbucketCloud.list(),
|
||||
...this.scmIntegrationsApi.bitbucketServer.list(),
|
||||
...this.scmIntegrationsApi.gerrit.list(),
|
||||
|
||||
@@ -54,11 +54,11 @@ describe('scaffolder action utils', () => {
|
||||
/No matching integration configuration for host/,
|
||||
);
|
||||
});
|
||||
describe('bitbucket', () => {
|
||||
beforeEach(() => byHost.mockReturnValue({ type: 'bitbucket' }));
|
||||
describe('bitbucketCloud', () => {
|
||||
beforeEach(() => byHost.mockReturnValue({ type: 'bitbucketCloud' }));
|
||||
describe('cloud', () => {
|
||||
const [host, workspace, project, repo] = [
|
||||
'www.bitbucket.org',
|
||||
'bitbucket.org',
|
||||
'foo',
|
||||
'bar',
|
||||
'baz',
|
||||
@@ -97,6 +97,9 @@ describe('scaffolder action utils', () => {
|
||||
repo,
|
||||
}));
|
||||
});
|
||||
});
|
||||
describe('bitbucketServer', () => {
|
||||
beforeEach(() => byHost.mockReturnValue({ type: 'bitbucketServer' }));
|
||||
describe('other', () => {
|
||||
const [host, project, repo] = ['bitbucket.other', 'foo', 'bar'];
|
||||
it('requires project', () =>
|
||||
|
||||
@@ -84,10 +84,11 @@ export const parseRepoUrl = (
|
||||
]),
|
||||
);
|
||||
switch (type) {
|
||||
case 'bitbucket': {
|
||||
if (host === 'www.bitbucket.org') {
|
||||
checkRequiredParams(parsed, 'workspace');
|
||||
}
|
||||
case 'bitbucketCloud': {
|
||||
checkRequiredParams(parsed, 'workspace', 'project', 'repo');
|
||||
break;
|
||||
}
|
||||
case 'bitbucketServer': {
|
||||
checkRequiredParams(parsed, 'project', 'repo');
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -28,10 +28,12 @@ describe('RepoPicker Validation', () => {
|
||||
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
bitbucket: [
|
||||
bitbucketCloud: [
|
||||
{
|
||||
host: 'bitbucket.org',
|
||||
},
|
||||
],
|
||||
bitbucketServer: [
|
||||
{
|
||||
host: 'server.bitbucket.com',
|
||||
},
|
||||
|
||||
@@ -39,9 +39,16 @@ export const repoPickerValidation = (
|
||||
'Incomplete repository location provided, host not provided',
|
||||
);
|
||||
} else {
|
||||
if (integrationApi?.byHost(host)?.type === 'bitbucket') {
|
||||
const integrationType = integrationApi?.byHost(host)?.type;
|
||||
if (
|
||||
integrationType === 'bitbucketCloud' ||
|
||||
integrationType === 'bitbucketServer'
|
||||
) {
|
||||
// workspace is only applicable for bitbucket cloud
|
||||
if (host === 'bitbucket.org' && !searchParams.get('workspace')) {
|
||||
if (
|
||||
integrationType === 'bitbucketCloud' &&
|
||||
!searchParams.get('workspace')
|
||||
) {
|
||||
validation.addError(
|
||||
'Incomplete repository location provided, workspace not provided',
|
||||
);
|
||||
@@ -52,7 +59,7 @@ export const repoPickerValidation = (
|
||||
'Incomplete repository location provided, project not provided',
|
||||
);
|
||||
}
|
||||
} else if (integrationApi?.byHost(host)?.type === 'azure') {
|
||||
} else if (integrationType === 'azure') {
|
||||
if (!searchParams.get('project')) {
|
||||
validation.addError(
|
||||
'Incomplete repository location provided, project not provided',
|
||||
|
||||
@@ -6709,7 +6709,7 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-scaffolder-backend-module-bitbucket-cloud@workspace:^, @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@workspace:plugins/scaffolder-backend-module-bitbucket-cloud":
|
||||
"@backstage/plugin-scaffolder-backend-module-bitbucket-cloud@workspace:plugins/scaffolder-backend-module-bitbucket-cloud":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud@workspace:plugins/scaffolder-backend-module-bitbucket-cloud"
|
||||
dependencies:
|
||||
@@ -6730,7 +6730,7 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-scaffolder-backend-module-bitbucket-server@workspace:^, @backstage/plugin-scaffolder-backend-module-bitbucket-server@workspace:plugins/scaffolder-backend-module-bitbucket-server":
|
||||
"@backstage/plugin-scaffolder-backend-module-bitbucket-server@workspace:plugins/scaffolder-backend-module-bitbucket-server":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-scaffolder-backend-module-bitbucket-server@workspace:plugins/scaffolder-backend-module-bitbucket-server"
|
||||
dependencies:
|
||||
@@ -6748,26 +6748,6 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-scaffolder-backend-module-bitbucket@workspace:plugins/scaffolder-backend-module-bitbucket":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-scaffolder-backend-module-bitbucket@workspace:plugins/scaffolder-backend-module-bitbucket"
|
||||
dependencies:
|
||||
"@backstage/backend-plugin-api": "workspace:^"
|
||||
"@backstage/backend-test-utils": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/config": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@backstage/integration": "workspace:^"
|
||||
"@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "workspace:^"
|
||||
"@backstage/plugin-scaffolder-backend-module-bitbucket-server": "workspace:^"
|
||||
"@backstage/plugin-scaffolder-node": "workspace:^"
|
||||
"@backstage/plugin-scaffolder-node-test-utils": "workspace:^"
|
||||
fs-extra: "npm:^11.2.0"
|
||||
msw: "npm:^1.0.0"
|
||||
yaml: "npm:^2.0.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-scaffolder-backend-module-confluence-to-markdown@workspace:plugins/scaffolder-backend-module-confluence-to-markdown":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown@workspace:plugins/scaffolder-backend-module-confluence-to-markdown"
|
||||
|
||||
Reference in New Issue
Block a user