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:
Andre Wanlin
2026-02-06 14:07:52 -06:00
parent 32cb991728
commit 527cf88a90
56 changed files with 60 additions and 6243 deletions
+10
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-defaults': minor
---
**BREAKING** Removed deprecated `BitbucketUrlReader`. Use the `BitbucketCloudUrlReader` or the `BitbucketServerUrlReader` instead.
+5
View File
@@ -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';
+1 -5
View File
@@ -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>
+1 -1
View File
@@ -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
});
});
-58
View File
@@ -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<{
/**
-108
View File
@@ -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);
+1 -24
View File
@@ -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 })),
-18
View File
@@ -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 };
}
-2
View File
@@ -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');
});
});
});
-183
View File
@@ -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';
+1 -140
View File
@@ -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.', () => {
+7 -91
View File
@@ -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}`;
}
-2
View File
@@ -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';
+1 -23
View File
@@ -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: '' };
-24
View File
@@ -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,
+1 -1
View File
@@ -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,
-1
View File
@@ -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';
-5
View File
@@ -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', () =>
+5 -4
View File
@@ -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',
+2 -22
View File
@@ -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"