feat: split integrations.bitbucket -> bitbucketCloud / bitbucketServer
Split `integrations.bitbucket` into `integrations.bitbucketCloud` and `integrations.bitbucketServer` while staying backwards compatible for now (== `BitbucketIntegration` loads from the new configs, too, if the old is not used). Relates-to: #9923 Signed-off-by: Patrick Jungermann <Patrick.Jungermann@gmail.com>
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
---
|
||||
'@backstage/integration': minor
|
||||
'@backstage/integration-react': minor
|
||||
---
|
||||
|
||||
Split `bitbucket` integration into `bitbucketCloud` and `bitbucketServer`
|
||||
(backwards compatible).
|
||||
|
||||
In order to migrate to the new integration configs,
|
||||
move your configs from `integrations.bitbucket`
|
||||
to `integrations.bitbucketCloud` or `integrations.bitbucketServer`.
|
||||
|
||||
Migration example:
|
||||
|
||||
**Before:**
|
||||
|
||||
```yaml
|
||||
integrations:
|
||||
bitbucket:
|
||||
- host: bitbucket.org
|
||||
username: bitbucket_user
|
||||
appPassword: app-password
|
||||
- host: bitbucket-server.company.com
|
||||
token: my-token
|
||||
```
|
||||
|
||||
**After:**
|
||||
|
||||
```yaml
|
||||
integrations:
|
||||
bitbucketCloud:
|
||||
- username: bitbucket_user
|
||||
appPassword: app-password
|
||||
bitbucketServer:
|
||||
- host: bitbucket-server.company.com
|
||||
token: my-token
|
||||
```
|
||||
@@ -6,38 +6,51 @@ sidebar_label: Locations
|
||||
description: Integrating source code stored in Bitbucket into the Backstage catalog
|
||||
---
|
||||
|
||||
The Bitbucket integration supports loading catalog entities from bitbucket.org
|
||||
or a self-hosted Bitbucket. Entities can be added to
|
||||
The Bitbucket integration supports loading catalog entities from bitbucket.org (Bitbucket Cloud)
|
||||
or Bitbucket Server. Entities can be added to
|
||||
[static catalog configuration](../../features/software-catalog/configuration.md),
|
||||
or registered with the
|
||||
[catalog-import](https://github.com/backstage/backstage/tree/master/plugins/catalog-import)
|
||||
plugin.
|
||||
|
||||
## Bitbucket Cloud
|
||||
|
||||
```yaml
|
||||
integrations:
|
||||
bitbucket:
|
||||
- host: bitbucket.org
|
||||
token: ${BITBUCKET_TOKEN}
|
||||
bitbucketCloud:
|
||||
- username: ${BITBUCKET_CLOUD_USERNAME}
|
||||
appPassword: ${BITBUCKET_CLOUD_PASSWORD}
|
||||
```
|
||||
|
||||
> Note: A public Bitbucket provider is added automatically at startup for
|
||||
> convenience, so you only need to list it if you want to supply a
|
||||
> [token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html).
|
||||
> Note: A public Bitbucket Cloud provider is added automatically at startup for
|
||||
> convenience, so you only need to list it if you want to supply credentials.
|
||||
|
||||
Directly under the `bitbucket` key is a list of provider configurations, where
|
||||
you can list the Bitbucket providers you want to fetch data from. Each entry is
|
||||
a structure with up to four elements:
|
||||
Directly under the `bitbucketCloud` key is a list of provider configurations, where
|
||||
you can list the Bitbucket Cloud providers you want to fetch data from.
|
||||
In the case of Bitbucket Cloud, you will have up to one entry.
|
||||
|
||||
- `host`: The host of the Bitbucket instance, e.g. `bitbucket.company.com`.
|
||||
- `token` (optional): An personal access token as expected by Bitbucket. Either
|
||||
an access token **or** a username + appPassword may be supplied.
|
||||
- `username` (optional): The Bitbucket username to use in API requests. If
|
||||
This one entry will have the following elements:
|
||||
|
||||
- `username`: The Bitbucket Cloud username to use in API requests. If
|
||||
neither a username nor token are supplied, anonymous access will be used.
|
||||
- `appPassword` (optional): The password for the Bitbucket user. Only needed
|
||||
when using `username` instead of `token`.
|
||||
- `apiBaseUrl` (optional): The URL of the Bitbucket API. For self-hosted
|
||||
installations, it is commonly at `https://<host>/rest/api/1.0`. For
|
||||
bitbucket.org, this configuration is not needed as it can be inferred.
|
||||
- `appPassword`: The app password for the Bitbucket Cloud user.
|
||||
|
||||
> Note: If you are using Bitbucket server you MUST set the username as well as
|
||||
> the token or appPassword.
|
||||
## Bitbucket Server
|
||||
|
||||
```yaml
|
||||
integrations:
|
||||
bitbucketServer:
|
||||
- host: bitbucket.company.com
|
||||
token: ${BITBUCKET_SERVER_TOKEN}
|
||||
```
|
||||
|
||||
Directly under the `bitbucketServer` key is a list of provider configurations, where
|
||||
you can list the Bitbucket Server providers you want to fetch data from. Each entry is
|
||||
a structure with the following elements:
|
||||
|
||||
- `host`: The host of the Bitbucket Server instance, e.g. `bitbucket.company.com`.
|
||||
- `token` (optional):
|
||||
An [personal access token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html)
|
||||
as expected by Bitbucket Server.
|
||||
- `apiBaseUrl` (optional): The URL of the Bitbucket Server API. For self-hosted
|
||||
installations, it is commonly at `https://<host>/rest/api/1.0`.
|
||||
|
||||
@@ -53,6 +53,14 @@ export const DevPage = () => {
|
||||
Bitbucket
|
||||
</Typography>
|
||||
<Integrations group={integrations.bitbucket} />
|
||||
<Typography paragraph variant="h2">
|
||||
Bitbucket Cloud
|
||||
</Typography>
|
||||
<Integrations group={integrations.bitbucketCloud} />
|
||||
<Typography paragraph variant="h2">
|
||||
Bitbucket Server
|
||||
</Typography>
|
||||
<Integrations group={integrations.bitbucketServer} />
|
||||
<Typography paragraph variant="h2">
|
||||
GitHub
|
||||
</Typography>
|
||||
|
||||
@@ -26,6 +26,6 @@ describe('scmIntegrationsApiRef', () => {
|
||||
|
||||
it('should be instantiated', () => {
|
||||
const i = ScmIntegrationsApi.fromConfig(new ConfigReader({}));
|
||||
expect(i.list().length).toBe(5); // The default ones
|
||||
expect(i.list().length).toBe(6); // The default ones
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,6 +66,35 @@ export type AzureIntegrationConfig = {
|
||||
};
|
||||
|
||||
// @public
|
||||
export class BitbucketCloudIntegration implements ScmIntegration {
|
||||
constructor(integrationConfig: BitbucketCloudIntegrationConfig);
|
||||
// (undocumented)
|
||||
get config(): BitbucketCloudIntegrationConfig;
|
||||
// (undocumented)
|
||||
static factory: ScmIntegrationsFactory<BitbucketCloudIntegration>;
|
||||
// (undocumented)
|
||||
resolveEditUrl(url: string): string;
|
||||
// (undocumented)
|
||||
resolveUrl(options: {
|
||||
url: string;
|
||||
base: string;
|
||||
lineNumber?: number;
|
||||
}): string;
|
||||
// (undocumented)
|
||||
get title(): string;
|
||||
// (undocumented)
|
||||
get type(): string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type BitbucketCloudIntegrationConfig = {
|
||||
host: string;
|
||||
apiBaseUrl: string;
|
||||
username?: string;
|
||||
appPassword?: string;
|
||||
};
|
||||
|
||||
// @public @deprecated
|
||||
export class BitbucketIntegration implements ScmIntegration {
|
||||
constructor(integrationConfig: BitbucketIntegrationConfig);
|
||||
// (undocumented)
|
||||
@@ -86,7 +115,7 @@ export class BitbucketIntegration implements ScmIntegration {
|
||||
get type(): string;
|
||||
}
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type BitbucketIntegrationConfig = {
|
||||
host: string;
|
||||
apiBaseUrl: string;
|
||||
@@ -95,6 +124,34 @@ export type BitbucketIntegrationConfig = {
|
||||
appPassword?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export class BitbucketServerIntegration implements ScmIntegration {
|
||||
constructor(integrationConfig: BitbucketServerIntegrationConfig);
|
||||
// (undocumented)
|
||||
get config(): BitbucketServerIntegrationConfig;
|
||||
// (undocumented)
|
||||
static factory: ScmIntegrationsFactory<BitbucketServerIntegration>;
|
||||
// (undocumented)
|
||||
resolveEditUrl(url: string): string;
|
||||
// (undocumented)
|
||||
resolveUrl(options: {
|
||||
url: string;
|
||||
base: string;
|
||||
lineNumber?: number;
|
||||
}): string;
|
||||
// (undocumented)
|
||||
get title(): string;
|
||||
// (undocumented)
|
||||
get type(): string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type BitbucketServerIntegrationConfig = {
|
||||
host: string;
|
||||
apiBaseUrl: string;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export class DefaultGithubCredentialsProvider
|
||||
implements GithubCredentialsProvider
|
||||
@@ -161,30 +218,80 @@ export function getAzureRequestOptions(
|
||||
};
|
||||
|
||||
// @public
|
||||
export function getBitbucketCloudDefaultBranch(
|
||||
url: string,
|
||||
config: BitbucketCloudIntegrationConfig,
|
||||
): Promise<string>;
|
||||
|
||||
// @public
|
||||
export function getBitbucketCloudDownloadUrl(
|
||||
url: string,
|
||||
config: BitbucketCloudIntegrationConfig,
|
||||
): Promise<string>;
|
||||
|
||||
// @public
|
||||
export function getBitbucketCloudFileFetchUrl(
|
||||
url: string,
|
||||
config: BitbucketCloudIntegrationConfig,
|
||||
): string;
|
||||
|
||||
// @public
|
||||
export function getBitbucketCloudRequestOptions(
|
||||
config: BitbucketCloudIntegrationConfig,
|
||||
): {
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
|
||||
// @public @deprecated
|
||||
export function getBitbucketDefaultBranch(
|
||||
url: string,
|
||||
config: BitbucketIntegrationConfig,
|
||||
): Promise<string>;
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export function getBitbucketDownloadUrl(
|
||||
url: string,
|
||||
config: BitbucketIntegrationConfig,
|
||||
): Promise<string>;
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export function getBitbucketFileFetchUrl(
|
||||
url: string,
|
||||
config: BitbucketIntegrationConfig,
|
||||
): string;
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export function getBitbucketRequestOptions(
|
||||
config: BitbucketIntegrationConfig,
|
||||
): {
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function getBitbucketServerDefaultBranch(
|
||||
url: string,
|
||||
config: BitbucketServerIntegrationConfig,
|
||||
): Promise<string>;
|
||||
|
||||
// @public
|
||||
export function getBitbucketServerDownloadUrl(
|
||||
url: string,
|
||||
config: BitbucketServerIntegrationConfig,
|
||||
): Promise<string>;
|
||||
|
||||
// @public
|
||||
export function getBitbucketServerFileFetchUrl(
|
||||
url: string,
|
||||
config: BitbucketServerIntegrationConfig,
|
||||
): string;
|
||||
|
||||
// @public
|
||||
export function getBitbucketServerRequestOptions(
|
||||
config: BitbucketServerIntegrationConfig,
|
||||
): {
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function getGerritFileContentsApiUrl(
|
||||
config: GerritIntegrationConfig,
|
||||
@@ -332,9 +439,13 @@ export interface IntegrationsByType {
|
||||
awsS3: ScmIntegrationsGroup<AwsS3Integration>;
|
||||
// (undocumented)
|
||||
azure: ScmIntegrationsGroup<AzureIntegration>;
|
||||
// (undocumented)
|
||||
// @deprecated (undocumented)
|
||||
bitbucket: ScmIntegrationsGroup<BitbucketIntegration>;
|
||||
// (undocumented)
|
||||
bitbucketCloud: ScmIntegrationsGroup<BitbucketCloudIntegration>;
|
||||
// (undocumented)
|
||||
bitbucketServer: ScmIntegrationsGroup<BitbucketServerIntegration>;
|
||||
// (undocumented)
|
||||
gerrit: ScmIntegrationsGroup<GerritIntegration>;
|
||||
// (undocumented)
|
||||
github: ScmIntegrationsGroup<GitHubIntegration>;
|
||||
@@ -366,15 +477,35 @@ export function readAzureIntegrationConfigs(
|
||||
): AzureIntegrationConfig[];
|
||||
|
||||
// @public
|
||||
export function readBitbucketCloudIntegrationConfig(
|
||||
config: Config,
|
||||
): BitbucketCloudIntegrationConfig;
|
||||
|
||||
// @public
|
||||
export function readBitbucketCloudIntegrationConfigs(
|
||||
configs: Config[],
|
||||
): BitbucketCloudIntegrationConfig[];
|
||||
|
||||
// @public @deprecated
|
||||
export function readBitbucketIntegrationConfig(
|
||||
config: Config,
|
||||
): BitbucketIntegrationConfig;
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export function readBitbucketIntegrationConfigs(
|
||||
configs: Config[],
|
||||
): BitbucketIntegrationConfig[];
|
||||
|
||||
// @public
|
||||
export function readBitbucketServerIntegrationConfig(
|
||||
config: Config,
|
||||
): BitbucketServerIntegrationConfig;
|
||||
|
||||
// @public
|
||||
export function readBitbucketServerIntegrationConfigs(
|
||||
configs: Config[],
|
||||
): BitbucketServerIntegrationConfig[];
|
||||
|
||||
// @public
|
||||
export function readGerritIntegrationConfig(
|
||||
config: Config,
|
||||
@@ -441,9 +572,13 @@ export interface ScmIntegrationRegistry
|
||||
awsS3: ScmIntegrationsGroup<AwsS3Integration>;
|
||||
// (undocumented)
|
||||
azure: ScmIntegrationsGroup<AzureIntegration>;
|
||||
// (undocumented)
|
||||
// @deprecated (undocumented)
|
||||
bitbucket: ScmIntegrationsGroup<BitbucketIntegration>;
|
||||
// (undocumented)
|
||||
bitbucketCloud: ScmIntegrationsGroup<BitbucketCloudIntegration>;
|
||||
// (undocumented)
|
||||
bitbucketServer: ScmIntegrationsGroup<BitbucketServerIntegration>;
|
||||
// (undocumented)
|
||||
gerrit: ScmIntegrationsGroup<GerritIntegration>;
|
||||
// (undocumented)
|
||||
github: ScmIntegrationsGroup<GitHubIntegration>;
|
||||
@@ -464,9 +599,13 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
|
||||
get awsS3(): ScmIntegrationsGroup<AwsS3Integration>;
|
||||
// (undocumented)
|
||||
get azure(): ScmIntegrationsGroup<AzureIntegration>;
|
||||
// (undocumented)
|
||||
// @deprecated (undocumented)
|
||||
get bitbucket(): ScmIntegrationsGroup<BitbucketIntegration>;
|
||||
// (undocumented)
|
||||
get bitbucketCloud(): ScmIntegrationsGroup<BitbucketCloudIntegration>;
|
||||
// (undocumented)
|
||||
get bitbucketServer(): ScmIntegrationsGroup<BitbucketServerIntegration>;
|
||||
// (undocumented)
|
||||
byHost(host: string): ScmIntegration | undefined;
|
||||
// (undocumented)
|
||||
byUrl(url: string | URL): ScmIntegration | undefined;
|
||||
|
||||
Vendored
+37
-1
@@ -31,7 +31,10 @@ export interface Config {
|
||||
token?: string;
|
||||
}>;
|
||||
|
||||
/** Integration configuration for Bitbucket */
|
||||
/**
|
||||
* Integration configuration for Bitbucket
|
||||
* @deprecated replaced by bitbucketCloud and bitbucketServer
|
||||
*/
|
||||
bitbucket?: Array<{
|
||||
/**
|
||||
* The hostname of the given Bitbucket instance
|
||||
@@ -60,6 +63,39 @@ export interface Config {
|
||||
appPassword?: string;
|
||||
}>;
|
||||
|
||||
/** Integration configuration for Bitbucket Cloud */
|
||||
bitbucketCloud?: Array<{
|
||||
/**
|
||||
* The username to use for authenticated requests.
|
||||
* @visibility secret
|
||||
*/
|
||||
username: string;
|
||||
/**
|
||||
* Bitbucket Cloud app password used to authenticate requests.
|
||||
* @visibility secret
|
||||
*/
|
||||
appPassword: string;
|
||||
}>;
|
||||
|
||||
/** Integration configuration for Bitbucket Server */
|
||||
bitbucketServer?: Array<{
|
||||
/**
|
||||
* The hostname of the given Bitbucket Server instance
|
||||
* @visibility frontend
|
||||
*/
|
||||
host: string;
|
||||
/**
|
||||
* Token used to authenticate requests.
|
||||
* @visibility secret
|
||||
*/
|
||||
token?: string;
|
||||
/**
|
||||
* The base url for the Bitbucket Server API, for example https://<host>/rest/api/1.0
|
||||
* @visibility frontend
|
||||
*/
|
||||
apiBaseUrl?: string;
|
||||
}>;
|
||||
|
||||
/** Integration configuration for Gerrit */
|
||||
gerrit?: Array<{
|
||||
/**
|
||||
|
||||
@@ -17,8 +17,16 @@ import { AwsS3IntegrationConfig } from './awsS3';
|
||||
import { AwsS3Integration } from './awsS3/AwsS3Integration';
|
||||
import { AzureIntegrationConfig } from './azure';
|
||||
import { AzureIntegration } from './azure/AzureIntegration';
|
||||
import {
|
||||
BitbucketCloudIntegration,
|
||||
BitbucketCloudIntegrationConfig,
|
||||
} from './bitbucketCloud';
|
||||
import { BitbucketIntegrationConfig } from './bitbucket';
|
||||
import { BitbucketIntegration } from './bitbucket/BitbucketIntegration';
|
||||
import {
|
||||
BitbucketServerIntegration,
|
||||
BitbucketServerIntegrationConfig,
|
||||
} from './bitbucketServer';
|
||||
import { GerritIntegrationConfig } from './gerrit';
|
||||
import { GerritIntegration } from './gerrit/GerritIntegration';
|
||||
import { GitHubIntegrationConfig } from './github';
|
||||
@@ -41,6 +49,14 @@ describe('ScmIntegrations', () => {
|
||||
host: 'bitbucket.local',
|
||||
} as BitbucketIntegrationConfig);
|
||||
|
||||
const bitbucketCloud = new BitbucketCloudIntegration({
|
||||
host: 'bitbucket.org',
|
||||
} as BitbucketCloudIntegrationConfig);
|
||||
|
||||
const bitbucketServer = new BitbucketServerIntegration({
|
||||
host: 'bitbucket-server.local',
|
||||
} as BitbucketServerIntegrationConfig);
|
||||
|
||||
const gerrit = new GerritIntegration({
|
||||
host: 'gerrit.local',
|
||||
} as GerritIntegrationConfig);
|
||||
@@ -57,6 +73,11 @@ describe('ScmIntegrations', () => {
|
||||
awsS3: basicIntegrations([awsS3], item => item.config.host),
|
||||
azure: basicIntegrations([azure], item => item.config.host),
|
||||
bitbucket: basicIntegrations([bitbucket], item => item.config.host),
|
||||
bitbucketCloud: basicIntegrations([bitbucketCloud], item => item.title),
|
||||
bitbucketServer: basicIntegrations(
|
||||
[bitbucketServer],
|
||||
item => item.config.host,
|
||||
),
|
||||
gerrit: basicIntegrations([gerrit], item => item.config.host),
|
||||
github: basicIntegrations([github], item => item.config.host),
|
||||
gitlab: basicIntegrations([gitlab], item => item.config.host),
|
||||
@@ -66,6 +87,12 @@ describe('ScmIntegrations', () => {
|
||||
expect(i.awsS3.byUrl('https://awss3.local')).toBe(awsS3);
|
||||
expect(i.azure.byUrl('https://azure.local')).toBe(azure);
|
||||
expect(i.bitbucket.byUrl('https://bitbucket.local')).toBe(bitbucket);
|
||||
expect(i.bitbucketCloud.byUrl('https://bitbucket.org')).toBe(
|
||||
bitbucketCloud,
|
||||
);
|
||||
expect(i.bitbucketServer.byUrl('https://bitbucket-server.local')).toBe(
|
||||
bitbucketServer,
|
||||
);
|
||||
expect(i.gerrit.byUrl('https://gerrit.local')).toBe(gerrit);
|
||||
expect(i.github.byUrl('https://github.local')).toBe(github);
|
||||
expect(i.gitlab.byUrl('https://gitlab.local')).toBe(gitlab);
|
||||
@@ -73,7 +100,16 @@ describe('ScmIntegrations', () => {
|
||||
|
||||
it('can list', () => {
|
||||
expect(i.list()).toEqual(
|
||||
expect.arrayContaining([awsS3, azure, bitbucket, gerrit, github, gitlab]),
|
||||
expect.arrayContaining([
|
||||
awsS3,
|
||||
azure,
|
||||
bitbucket,
|
||||
bitbucketCloud,
|
||||
bitbucketServer,
|
||||
gerrit,
|
||||
github,
|
||||
gitlab,
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -81,6 +117,8 @@ describe('ScmIntegrations', () => {
|
||||
expect(i.byUrl('https://awss3.local')).toBe(awsS3);
|
||||
expect(i.byUrl('https://azure.local')).toBe(azure);
|
||||
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);
|
||||
expect(i.byUrl('https://github.local')).toBe(github);
|
||||
expect(i.byUrl('https://gitlab.local')).toBe(gitlab);
|
||||
@@ -88,6 +126,8 @@ describe('ScmIntegrations', () => {
|
||||
expect(i.byHost('awss3.local')).toBe(awsS3);
|
||||
expect(i.byHost('azure.local')).toBe(azure);
|
||||
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);
|
||||
expect(i.byHost('github.local')).toBe(github);
|
||||
expect(i.byHost('gitlab.local')).toBe(gitlab);
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
import { Config } from '@backstage/config';
|
||||
import { AwsS3Integration } from './awsS3/AwsS3Integration';
|
||||
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';
|
||||
import { GitLabIntegration } from './gitlab/GitLabIntegration';
|
||||
@@ -33,7 +35,12 @@ import { ScmIntegrationRegistry } from './registry';
|
||||
export interface IntegrationsByType {
|
||||
awsS3: ScmIntegrationsGroup<AwsS3Integration>;
|
||||
azure: ScmIntegrationsGroup<AzureIntegration>;
|
||||
/**
|
||||
* @deprecated in favor of `bitbucketCloud` and `bitbucketServer`
|
||||
*/
|
||||
bitbucket: ScmIntegrationsGroup<BitbucketIntegration>;
|
||||
bitbucketCloud: ScmIntegrationsGroup<BitbucketCloudIntegration>;
|
||||
bitbucketServer: ScmIntegrationsGroup<BitbucketServerIntegration>;
|
||||
gerrit: ScmIntegrationsGroup<GerritIntegration>;
|
||||
github: ScmIntegrationsGroup<GitHubIntegration>;
|
||||
gitlab: ScmIntegrationsGroup<GitLabIntegration>;
|
||||
@@ -52,6 +59,8 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
|
||||
awsS3: AwsS3Integration.factory({ config }),
|
||||
azure: AzureIntegration.factory({ config }),
|
||||
bitbucket: BitbucketIntegration.factory({ config }),
|
||||
bitbucketCloud: BitbucketCloudIntegration.factory({ config }),
|
||||
bitbucketServer: BitbucketServerIntegration.factory({ config }),
|
||||
gerrit: GerritIntegration.factory({ config }),
|
||||
github: GitHubIntegration.factory({ config }),
|
||||
gitlab: GitLabIntegration.factory({ config }),
|
||||
@@ -70,10 +79,21 @@ 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;
|
||||
}
|
||||
|
||||
get bitbucketServer(): ScmIntegrationsGroup<BitbucketServerIntegration> {
|
||||
return this.byType.bitbucketServer;
|
||||
}
|
||||
|
||||
get gerrit(): ScmIntegrationsGroup<GerritIntegration> {
|
||||
return this.byType.gerrit;
|
||||
}
|
||||
|
||||
@@ -18,25 +18,52 @@ import { ConfigReader } from '@backstage/config';
|
||||
import { BitbucketIntegration } from './BitbucketIntegration';
|
||||
|
||||
describe('BitbucketIntegration', () => {
|
||||
it('has a working factory', () => {
|
||||
const integrations = BitbucketIntegration.factory({
|
||||
config: new ConfigReader({
|
||||
integrations: {
|
||||
bitbucket: [
|
||||
{
|
||||
host: 'h.com',
|
||||
apiBaseUrl: 'a',
|
||||
token: 't',
|
||||
username: 'u',
|
||||
appPassword: 'p',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
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');
|
||||
});
|
||||
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('returns the basics', () => {
|
||||
|
||||
@@ -26,13 +26,21 @@ import {
|
||||
* 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') ?? [],
|
||||
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)),
|
||||
|
||||
@@ -25,6 +25,7 @@ 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 = {
|
||||
/**
|
||||
@@ -68,6 +69,7 @@ export type BitbucketIntegrationConfig = {
|
||||
*
|
||||
* @param config - The config object of a single integration
|
||||
* @public
|
||||
* @deprecated bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer.
|
||||
*/
|
||||
export function readBitbucketIntegrationConfig(
|
||||
config: Config,
|
||||
@@ -107,6 +109,7 @@ export function readBitbucketIntegrationConfig(
|
||||
*
|
||||
* @param configs - All of the integration config objects
|
||||
* @public
|
||||
* @deprecated bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer.
|
||||
*/
|
||||
export function readBitbucketIntegrationConfigs(
|
||||
configs: Config[],
|
||||
|
||||
@@ -24,6 +24,7 @@ import { BitbucketIntegrationConfig } from './config';
|
||||
* @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,
|
||||
@@ -75,6 +76,7 @@ export async function getBitbucketDefaultBranch(
|
||||
* @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,
|
||||
@@ -119,6 +121,7 @@ export async function getBitbucketDownloadUrl(
|
||||
* @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,
|
||||
@@ -155,6 +158,7 @@ export function getBitbucketFileFetchUrl(
|
||||
*
|
||||
* @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,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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 { BitbucketCloudIntegration } from './BitbucketCloudIntegration';
|
||||
|
||||
describe('BitbucketCloudIntegration', () => {
|
||||
it('has a working factory', () => {
|
||||
const integrations = BitbucketCloudIntegration.factory({
|
||||
config: new ConfigReader({
|
||||
integrations: {
|
||||
bitbucketCloud: [
|
||||
{
|
||||
username: 'u',
|
||||
appPassword: 'p',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(integrations.list().length).toBe(1);
|
||||
expect(integrations.list()[0].config.username).toBe('u');
|
||||
expect(integrations.list()[0].config.appPassword).toBe('p');
|
||||
});
|
||||
|
||||
it('returns the basics', () => {
|
||||
const integration = new BitbucketCloudIntegration({
|
||||
host: 'bitbucket.org',
|
||||
} as any);
|
||||
expect(integration.type).toBe('bitbucketCloud');
|
||||
expect(integration.title).toBe('bitbucket.org');
|
||||
});
|
||||
|
||||
it('resolves url line number correctly', () => {
|
||||
const integration = new BitbucketCloudIntegration({} 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('resolve edit URL', () => {
|
||||
const integration = new BitbucketCloudIntegration({} 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&at=master',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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 {
|
||||
BitbucketCloudIntegrationConfig,
|
||||
readBitbucketCloudIntegrationConfigs,
|
||||
} from './config';
|
||||
|
||||
/**
|
||||
* A Bitbucket Cloud based integration.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class BitbucketCloudIntegration implements ScmIntegration {
|
||||
static factory: ScmIntegrationsFactory<BitbucketCloudIntegration> = ({
|
||||
config,
|
||||
}) => {
|
||||
const configs = readBitbucketCloudIntegrationConfigs(
|
||||
config.getOptionalConfigArray('integrations.bitbucketCloud') ?? [],
|
||||
);
|
||||
return basicIntegrations(
|
||||
configs.map(c => new BitbucketCloudIntegration(c)),
|
||||
i => i.config.host,
|
||||
);
|
||||
};
|
||||
|
||||
constructor(
|
||||
private readonly integrationConfig: BitbucketCloudIntegrationConfig,
|
||||
) {}
|
||||
|
||||
get type(): string {
|
||||
return 'bitbucketCloud';
|
||||
}
|
||||
|
||||
get title(): string {
|
||||
return this.integrationConfig.host;
|
||||
}
|
||||
|
||||
get config(): BitbucketCloudIntegrationConfig {
|
||||
return this.integrationConfig;
|
||||
}
|
||||
|
||||
resolveUrl(options: {
|
||||
url: string;
|
||||
base: string;
|
||||
lineNumber?: number;
|
||||
}): string {
|
||||
const resolved = defaultScmResolveUrl(options);
|
||||
|
||||
// Bitbucket Cloud line numbers use the syntax #lines-42, rather than #L42
|
||||
if (options.lineNumber) {
|
||||
const url = new URL(resolved);
|
||||
|
||||
url.hash = `lines-${options.lineNumber}`;
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
resolveEditUrl(url: string): string {
|
||||
const urlData = parseGitUrl(url);
|
||||
const editUrl = new URL(url);
|
||||
|
||||
editUrl.searchParams.set('mode', 'edit');
|
||||
editUrl.searchParams.set('at', urlData.ref);
|
||||
return editUrl.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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 {
|
||||
BitbucketCloudIntegrationConfig,
|
||||
readBitbucketCloudIntegrationConfig,
|
||||
readBitbucketCloudIntegrationConfigs,
|
||||
} from './config';
|
||||
|
||||
describe('readBitbucketCloudIntegrationConfig', () => {
|
||||
function buildConfig(data: Partial<BitbucketCloudIntegrationConfig>): Config {
|
||||
return new ConfigReader(data);
|
||||
}
|
||||
|
||||
async function buildFrontendConfig(
|
||||
data: Partial<BitbucketCloudIntegrationConfig>,
|
||||
): 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: { bitbucketCloud: [data] } }, context: 'app' }],
|
||||
{ visibility: ['frontend'] },
|
||||
);
|
||||
return new ConfigReader(processed[0].data as any);
|
||||
}
|
||||
|
||||
it('reads all values', () => {
|
||||
const output = readBitbucketCloudIntegrationConfig(
|
||||
buildConfig({
|
||||
username: 'u',
|
||||
appPassword: 'p',
|
||||
}),
|
||||
);
|
||||
expect(output).toEqual({
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
appPassword: 'p',
|
||||
host: 'bitbucket.org',
|
||||
username: 'u',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects funky configs', () => {
|
||||
const valid: any = {
|
||||
username: 'u',
|
||||
appPassword: 'p',
|
||||
};
|
||||
expect(() =>
|
||||
readBitbucketCloudIntegrationConfig(
|
||||
buildConfig({ ...valid, username: 7 }),
|
||||
),
|
||||
).toThrow(/username/);
|
||||
expect(() =>
|
||||
readBitbucketCloudIntegrationConfig(
|
||||
buildConfig({ ...valid, appPassword: 7 }),
|
||||
),
|
||||
).toThrow(/appPassword/);
|
||||
});
|
||||
|
||||
it('credentials hidden on the frontend', async () => {
|
||||
const frontendConfig = await buildFrontendConfig({
|
||||
appPassword: 'p',
|
||||
username: 'u',
|
||||
});
|
||||
expect(
|
||||
readBitbucketCloudIntegrationConfigs(
|
||||
frontendConfig.getOptionalConfigArray('integrations.bitbucketCloud') ??
|
||||
[],
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
host: 'bitbucket.org',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readBitbucketCloudIntegrationConfigs', () => {
|
||||
function buildConfig(
|
||||
data: Partial<BitbucketCloudIntegrationConfig>[],
|
||||
): Config[] {
|
||||
return data.map(item => new ConfigReader(item));
|
||||
}
|
||||
|
||||
it('reads all values', () => {
|
||||
const output = readBitbucketCloudIntegrationConfigs(
|
||||
buildConfig([
|
||||
{
|
||||
username: 'u',
|
||||
appPassword: 'p',
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(output).toContainEqual({
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
appPassword: 'p',
|
||||
host: 'bitbucket.org',
|
||||
username: 'u',
|
||||
});
|
||||
});
|
||||
|
||||
it('adds a default Bitbucket Cloud entry when missing', () => {
|
||||
const output = readBitbucketCloudIntegrationConfigs(buildConfig([]));
|
||||
expect(output).toEqual([
|
||||
{
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
host: 'bitbucket.org',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
const BITBUCKET_CLOUD_HOST = 'bitbucket.org';
|
||||
const BITBUCKET_CLOUD_API_BASE_URL = 'https://api.bitbucket.org/2.0';
|
||||
|
||||
/**
|
||||
* The configuration parameters for a single Bitbucket Cloud API provider.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type BitbucketCloudIntegrationConfig = {
|
||||
/**
|
||||
* Constant. bitbucket.org
|
||||
*/
|
||||
host: string;
|
||||
|
||||
/**
|
||||
* Constant. https://api.bitbucket.org/2.0
|
||||
*/
|
||||
apiBaseUrl: 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads a single Bitbucket Cloud integration config.
|
||||
*
|
||||
* @param config - The config object of a single integration
|
||||
* @public
|
||||
*/
|
||||
export function readBitbucketCloudIntegrationConfig(
|
||||
config: Config,
|
||||
): BitbucketCloudIntegrationConfig {
|
||||
const host = BITBUCKET_CLOUD_HOST;
|
||||
const apiBaseUrl = BITBUCKET_CLOUD_API_BASE_URL;
|
||||
// If config is provided, we assume authenticated access is desired
|
||||
// (as the anonymous one is provided by default).
|
||||
const username = config.getString('username');
|
||||
const appPassword = config.getString('appPassword');
|
||||
|
||||
return {
|
||||
host,
|
||||
apiBaseUrl,
|
||||
username,
|
||||
appPassword,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a set of Bitbucket Cloud integration configs,
|
||||
* and inserts one for public Bitbucket Cloud if none specified.
|
||||
*
|
||||
* @param configs - All of the integration config objects
|
||||
* @public
|
||||
*/
|
||||
export function readBitbucketCloudIntegrationConfigs(
|
||||
configs: Config[],
|
||||
): BitbucketCloudIntegrationConfig[] {
|
||||
// First read all the explicit integrations
|
||||
const result = configs.map(readBitbucketCloudIntegrationConfig);
|
||||
|
||||
// If no explicit bitbucket.org integration was added,
|
||||
// put one in the list as a convenience
|
||||
if (result.length === 0) {
|
||||
result.push({
|
||||
host: BITBUCKET_CLOUD_HOST,
|
||||
apiBaseUrl: BITBUCKET_CLOUD_API_BASE_URL,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* 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 { setupRequestMockHandlers } from '@backstage/test-utils';
|
||||
import { BitbucketCloudIntegrationConfig } from './config';
|
||||
import {
|
||||
getBitbucketCloudDefaultBranch,
|
||||
getBitbucketCloudDownloadUrl,
|
||||
getBitbucketCloudFileFetchUrl,
|
||||
getBitbucketCloudRequestOptions,
|
||||
} from './core';
|
||||
|
||||
describe('bitbucketCloud core', () => {
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
|
||||
describe('getBitbucketCloudRequestOptions', () => {
|
||||
it('insert basic auth when needed', () => {
|
||||
const withUsernameAndPassword: BitbucketCloudIntegrationConfig = {
|
||||
host: 'bitbucket.org',
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
username: 'some-user',
|
||||
appPassword: 'my-secret',
|
||||
};
|
||||
const withoutUsernameAndPassword: BitbucketCloudIntegrationConfig = {
|
||||
host: 'bitbucket.org',
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
};
|
||||
expect(
|
||||
(
|
||||
getBitbucketCloudRequestOptions(withUsernameAndPassword)
|
||||
.headers as any
|
||||
).Authorization,
|
||||
).toEqual('Basic c29tZS11c2VyOm15LXNlY3JldA==');
|
||||
expect(
|
||||
(
|
||||
getBitbucketCloudRequestOptions(withoutUsernameAndPassword)
|
||||
.headers as any
|
||||
).Authorization,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBitbucketCloudFileFetchUrl', () => {
|
||||
it('rejects targets that do not look like URLs', () => {
|
||||
const config: BitbucketCloudIntegrationConfig = {
|
||||
host: '',
|
||||
apiBaseUrl: '',
|
||||
};
|
||||
expect(() => getBitbucketCloudFileFetchUrl('a/b', config)).toThrow(
|
||||
/Incorrect URL: a\/b/,
|
||||
);
|
||||
});
|
||||
|
||||
it('happy path for Bitbucket Cloud', () => {
|
||||
const config: BitbucketCloudIntegrationConfig = {
|
||||
host: 'bitbucket.org',
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
};
|
||||
expect(
|
||||
getBitbucketCloudFileFetchUrl(
|
||||
'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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBitbucketCloudDownloadUrl', () => {
|
||||
it('do not add path param for Bitbucket Cloud', async () => {
|
||||
const config: BitbucketCloudIntegrationConfig = {
|
||||
host: 'bitbucket.org',
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
};
|
||||
const result = await getBitbucketCloudDownloadUrl(
|
||||
'https://bitbucket.org/backstage/mock/src/master',
|
||||
config,
|
||||
);
|
||||
expect(result).toEqual(
|
||||
'https://bitbucket.org/backstage/mock/get/master.tar.gz',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBitbucketCloudDefaultBranch', () => {
|
||||
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: BitbucketCloudIntegrationConfig = {
|
||||
host: 'bitbucket.org',
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
};
|
||||
const defaultBranch = await getBitbucketCloudDefaultBranch(
|
||||
'https://bitbucket.org/backstage/mock/src/main',
|
||||
config,
|
||||
);
|
||||
expect(defaultBranch).toEqual('main');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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 { BitbucketCloudIntegrationConfig } 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
|
||||
*/
|
||||
export async function getBitbucketCloudDefaultBranch(
|
||||
url: string,
|
||||
config: BitbucketCloudIntegrationConfig,
|
||||
): Promise<string> {
|
||||
const { name: repoName, owner: project } = parseGitUrl(url);
|
||||
|
||||
const branchUrl = `${config.apiBaseUrl}/repositories/${project}/${repoName}`;
|
||||
const response = await fetch(
|
||||
branchUrl,
|
||||
getBitbucketCloudRequestOptions(config),
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const message = `Failed to retrieve default branch from ${branchUrl}, ${response.status} ${response.statusText}`;
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const repoInfo = await response.json();
|
||||
const defaultBranch = repoInfo.mainbranch.name;
|
||||
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
|
||||
*/
|
||||
export async function getBitbucketCloudDownloadUrl(
|
||||
url: string,
|
||||
config: BitbucketCloudIntegrationConfig,
|
||||
): Promise<string> {
|
||||
const {
|
||||
name: repoName,
|
||||
owner: project,
|
||||
ref,
|
||||
protocol,
|
||||
resource,
|
||||
} = parseGitUrl(url);
|
||||
|
||||
let branch = ref;
|
||||
if (!branch) {
|
||||
branch = await getBitbucketCloudDefaultBranch(url, config);
|
||||
}
|
||||
return `${protocol}://${resource}/${project}/${repoName}/get/${branch}.tar.gz`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export function getBitbucketCloudFileFetchUrl(
|
||||
url: string,
|
||||
config: BitbucketCloudIntegrationConfig,
|
||||
): string {
|
||||
try {
|
||||
const { owner, name, ref, filepathtype, filepath } = parseGitUrl(url);
|
||||
if (!owner || !name || (filepathtype !== 'src' && filepathtype !== 'raw')) {
|
||||
throw new Error('Invalid Bitbucket Cloud URL or file path');
|
||||
}
|
||||
|
||||
const pathWithoutSlash = filepath.replace(/^\//, '');
|
||||
|
||||
if (!ref) {
|
||||
throw new Error('Invalid Bitbucket Cloud URL or file path');
|
||||
}
|
||||
return `${config.apiBaseUrl}/repositories/${owner}/${name}/src/${ref}/${pathWithoutSlash}`;
|
||||
} 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
|
||||
*/
|
||||
export function getBitbucketCloudRequestOptions(
|
||||
config: BitbucketCloudIntegrationConfig,
|
||||
): { headers: Record<string, string> } {
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
if (config.username && config.appPassword) {
|
||||
const buffer = Buffer.from(
|
||||
`${config.username}:${config.appPassword}`,
|
||||
'utf8',
|
||||
);
|
||||
headers.Authorization = `Basic ${buffer.toString('base64')}`;
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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 { BitbucketCloudIntegration } from './BitbucketCloudIntegration';
|
||||
export {
|
||||
readBitbucketCloudIntegrationConfig,
|
||||
readBitbucketCloudIntegrationConfigs,
|
||||
} from './config';
|
||||
export type { BitbucketCloudIntegrationConfig } from './config';
|
||||
export {
|
||||
getBitbucketCloudDefaultBranch,
|
||||
getBitbucketCloudDownloadUrl,
|
||||
getBitbucketCloudFileFetchUrl,
|
||||
getBitbucketCloudRequestOptions,
|
||||
} from './core';
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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 { BitbucketServerIntegration } from './BitbucketServerIntegration';
|
||||
|
||||
describe('BitbucketServerIntegration', () => {
|
||||
it('has a working factory', () => {
|
||||
const integrations = BitbucketServerIntegration.factory({
|
||||
config: new ConfigReader({
|
||||
integrations: {
|
||||
bitbucketServer: [
|
||||
{
|
||||
host: 'h.com',
|
||||
apiBaseUrl: 'a',
|
||||
token: 't',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(integrations.list().length).toBe(1);
|
||||
expect(integrations.list()[0].config.host).toBe('h.com');
|
||||
});
|
||||
|
||||
it('returns the basics', () => {
|
||||
const integration = new BitbucketServerIntegration({
|
||||
host: 'h.com',
|
||||
} as any);
|
||||
expect(integration.type).toBe('bitbucketServer');
|
||||
expect(integration.title).toBe('h.com');
|
||||
});
|
||||
|
||||
it('resolves url line number correctly', () => {
|
||||
const integration = new BitbucketServerIntegration({
|
||||
host: 'h.com',
|
||||
} as any);
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: './a.yaml',
|
||||
base: 'https://h.com/my-owner/my-project/src/master/README.md',
|
||||
lineNumber: 14,
|
||||
}),
|
||||
).toBe('https://h.com/my-owner/my-project/src/master/a.yaml#a.yaml-14');
|
||||
});
|
||||
|
||||
it('resolve edit URL', () => {
|
||||
const integration = new BitbucketServerIntegration({
|
||||
host: 'h.com',
|
||||
} as any);
|
||||
|
||||
expect(
|
||||
integration.resolveEditUrl(
|
||||
'https://h.com/my-owner/my-project/src/master/README.md',
|
||||
),
|
||||
).toBe(
|
||||
'https://h.com/my-owner/my-project/src/master/README.md?mode=edit&spa=0&at=master',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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 {
|
||||
BitbucketServerIntegrationConfig,
|
||||
readBitbucketServerIntegrationConfigs,
|
||||
} from './config';
|
||||
|
||||
/**
|
||||
* A Bitbucket Server based integration.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class BitbucketServerIntegration implements ScmIntegration {
|
||||
static factory: ScmIntegrationsFactory<BitbucketServerIntegration> = ({
|
||||
config,
|
||||
}) => {
|
||||
const configs = readBitbucketServerIntegrationConfigs(
|
||||
config.getOptionalConfigArray('integrations.bitbucketServer') ?? [],
|
||||
);
|
||||
return basicIntegrations(
|
||||
configs.map(c => new BitbucketServerIntegration(c)),
|
||||
i => i.config.host,
|
||||
);
|
||||
};
|
||||
|
||||
constructor(
|
||||
private readonly integrationConfig: BitbucketServerIntegrationConfig,
|
||||
) {}
|
||||
|
||||
get type(): string {
|
||||
return 'bitbucketServer';
|
||||
}
|
||||
|
||||
get title(): string {
|
||||
return this.integrationConfig.host;
|
||||
}
|
||||
|
||||
get config(): BitbucketServerIntegrationConfig {
|
||||
return this.integrationConfig;
|
||||
}
|
||||
|
||||
resolveUrl(options: {
|
||||
url: string;
|
||||
base: string;
|
||||
lineNumber?: number;
|
||||
}): string {
|
||||
const resolved = defaultScmResolveUrl(options);
|
||||
|
||||
// Bitbucket Server line numbers use the syntax #example.txt-42, rather than #L42
|
||||
if (options.lineNumber) {
|
||||
const url = new URL(resolved);
|
||||
|
||||
const filename = url.pathname.split('/').slice(-1)[0];
|
||||
url.hash = `${filename}-${options.lineNumber}`;
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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 {
|
||||
BitbucketServerIntegrationConfig,
|
||||
readBitbucketServerIntegrationConfig,
|
||||
readBitbucketServerIntegrationConfigs,
|
||||
} from './config';
|
||||
|
||||
describe('readBitbucketServerIntegrationConfig', () => {
|
||||
function buildConfig(
|
||||
data: Partial<BitbucketServerIntegrationConfig>,
|
||||
): Config {
|
||||
return new ConfigReader(data);
|
||||
}
|
||||
|
||||
async function buildFrontendConfig(
|
||||
data: Partial<BitbucketServerIntegrationConfig>,
|
||||
): 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: { bitbucketServer: [data] } }, context: 'app' }],
|
||||
{ visibility: ['frontend'] },
|
||||
);
|
||||
return new ConfigReader(
|
||||
(processed[0].data as any).integrations.bitbucketServer[0],
|
||||
);
|
||||
}
|
||||
|
||||
it('reads all values', () => {
|
||||
const output = readBitbucketServerIntegrationConfig(
|
||||
buildConfig({
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
token: 't',
|
||||
}),
|
||||
);
|
||||
expect(output).toEqual({
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
token: 't',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects funky configs', () => {
|
||||
const valid: any = {
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
token: 't',
|
||||
};
|
||||
expect(() =>
|
||||
readBitbucketServerIntegrationConfig(buildConfig({ ...valid, host: 7 })),
|
||||
).toThrow(/host/);
|
||||
expect(() =>
|
||||
readBitbucketServerIntegrationConfig(
|
||||
buildConfig({ ...valid, apiBaseUrl: 7 }),
|
||||
),
|
||||
).toThrow(/apiBaseUrl/);
|
||||
expect(() =>
|
||||
readBitbucketServerIntegrationConfig(buildConfig({ ...valid, token: 7 })),
|
||||
).toThrow(/token/);
|
||||
});
|
||||
|
||||
it('works on the frontend', async () => {
|
||||
expect(
|
||||
readBitbucketServerIntegrationConfig(
|
||||
await buildFrontendConfig({
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
token: 't',
|
||||
}),
|
||||
),
|
||||
).toEqual({
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('readBitbucketServerIntegrationConfigs', () => {
|
||||
function buildConfig(
|
||||
data: Partial<BitbucketServerIntegrationConfig>[],
|
||||
): Config[] {
|
||||
return data.map(item => new ConfigReader(item));
|
||||
}
|
||||
|
||||
it('reads all values', () => {
|
||||
const output = readBitbucketServerIntegrationConfigs(
|
||||
buildConfig([
|
||||
{
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
token: 't',
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(output).toContainEqual({
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
token: 't',
|
||||
});
|
||||
});
|
||||
|
||||
it('adds no default Bitbucket Server entry when missing', () => {
|
||||
const output = readBitbucketServerIntegrationConfigs(buildConfig([]));
|
||||
expect(output).toEqual([]);
|
||||
});
|
||||
|
||||
it('injects the correct Bitbucket Server API base URL when missing', () => {
|
||||
const output = readBitbucketServerIntegrationConfigs(
|
||||
buildConfig([{ host: 'bitbucket.company.com' }]),
|
||||
);
|
||||
expect(output).toEqual([
|
||||
{
|
||||
host: 'bitbucket.company.com',
|
||||
apiBaseUrl: 'https://bitbucket.company.com/rest/api/1.0',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* The configuration parameters for a single Bitbucket Server API provider.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type BitbucketServerIntegrationConfig = {
|
||||
/**
|
||||
* The host of the target that this matches on, e.g. "bitbucket.company.com"
|
||||
*/
|
||||
host: string;
|
||||
|
||||
/**
|
||||
* The base URL of the API of this provider, e.g. "https://<host>/rest/api/1.0",
|
||||
* with no trailing slash.
|
||||
*
|
||||
* The API will always be preferred if both its base URL and a token are
|
||||
* present.
|
||||
*/
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads a single Bitbucket Server integration config.
|
||||
*
|
||||
* @param config - The config object of a single integration
|
||||
* @public
|
||||
*/
|
||||
export function readBitbucketServerIntegrationConfig(
|
||||
config: Config,
|
||||
): BitbucketServerIntegrationConfig {
|
||||
const host = config.getString('host');
|
||||
let apiBaseUrl = config.getOptionalString('apiBaseUrl');
|
||||
const token = config.getOptionalString('token');
|
||||
|
||||
if (!isValidHost(host)) {
|
||||
throw new Error(
|
||||
`Invalid Bitbucket Server integration config, '${host}' is not a valid host`,
|
||||
);
|
||||
}
|
||||
|
||||
if (apiBaseUrl) {
|
||||
apiBaseUrl = trimEnd(apiBaseUrl, '/');
|
||||
} else {
|
||||
apiBaseUrl = `https://${host}/rest/api/1.0`;
|
||||
}
|
||||
|
||||
return {
|
||||
host,
|
||||
apiBaseUrl,
|
||||
token,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a set of Bitbucket Server integration configs.
|
||||
*
|
||||
* @param configs - All of the integration config objects
|
||||
* @public
|
||||
*/
|
||||
export function readBitbucketServerIntegrationConfigs(
|
||||
configs: Config[],
|
||||
): BitbucketServerIntegrationConfig[] {
|
||||
// Read all the explicit integrations
|
||||
// No default integration will be added
|
||||
return configs.map(readBitbucketServerIntegrationConfig);
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* 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 { setupRequestMockHandlers } from '@backstage/test-utils';
|
||||
import { BitbucketServerIntegrationConfig } from './config';
|
||||
import {
|
||||
getBitbucketServerDefaultBranch,
|
||||
getBitbucketServerDownloadUrl,
|
||||
getBitbucketServerFileFetchUrl,
|
||||
getBitbucketServerRequestOptions,
|
||||
} from './core';
|
||||
|
||||
describe('bitbucketServer core', () => {
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
|
||||
describe('getBitbucketServerRequestOptions', () => {
|
||||
it('inserts a token when needed', () => {
|
||||
const withToken: BitbucketServerIntegrationConfig = {
|
||||
host: '',
|
||||
apiBaseUrl: '',
|
||||
token: 'A',
|
||||
};
|
||||
const withoutToken: BitbucketServerIntegrationConfig = {
|
||||
host: '',
|
||||
apiBaseUrl: '',
|
||||
};
|
||||
expect(
|
||||
(getBitbucketServerRequestOptions(withToken).headers as any)
|
||||
.Authorization,
|
||||
).toEqual('Bearer A');
|
||||
expect(
|
||||
(getBitbucketServerRequestOptions(withoutToken).headers as any)
|
||||
.Authorization,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBitbucketServerFileFetchUrl', () => {
|
||||
it('rejects targets that do not look like URLs', () => {
|
||||
const config: BitbucketServerIntegrationConfig = {
|
||||
host: '',
|
||||
apiBaseUrl: '',
|
||||
};
|
||||
expect(() => getBitbucketServerFileFetchUrl('a/b', config)).toThrow(
|
||||
/Incorrect URL: a\/b/,
|
||||
);
|
||||
});
|
||||
|
||||
it('happy path for Bitbucket Server', () => {
|
||||
const config: BitbucketServerIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://bitbucket.mycompany.net/rest/api/1.0',
|
||||
};
|
||||
expect(
|
||||
getBitbucketServerFileFetchUrl(
|
||||
'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('getBitbucketServerDownloadUrl', () => {
|
||||
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: BitbucketServerIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
|
||||
};
|
||||
const result = await getBitbucketServerDownloadUrl(
|
||||
'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('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: BitbucketServerIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
|
||||
};
|
||||
const result = await getBitbucketServerDownloadUrl(
|
||||
'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: BitbucketServerIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
|
||||
};
|
||||
const result = await getBitbucketServerDownloadUrl(
|
||||
'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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBitbucketServerDefaultBranch', () => {
|
||||
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: BitbucketServerIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
|
||||
};
|
||||
const defaultBranch = await getBitbucketServerDefaultBranch(
|
||||
'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: BitbucketServerIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
|
||||
};
|
||||
const defaultBranch = await getBitbucketServerDefaultBranch(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/README.md',
|
||||
config,
|
||||
);
|
||||
expect(defaultBranch).toEqual('main');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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 { BitbucketServerIntegrationConfig } 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
|
||||
*/
|
||||
export async function getBitbucketServerDefaultBranch(
|
||||
url: string,
|
||||
config: BitbucketServerIntegrationConfig,
|
||||
): Promise<string> {
|
||||
const { name: repoName, owner: project } = parseGitUrl(url);
|
||||
|
||||
// Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp184
|
||||
let branchUrl = `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/default-branch`;
|
||||
|
||||
let response = await fetch(
|
||||
branchUrl,
|
||||
getBitbucketServerRequestOptions(config),
|
||||
);
|
||||
|
||||
if (response.status === 404) {
|
||||
// First try the new format, and then if it gets specifically a 404 it should try the old format
|
||||
// (to support old Atlassian Bitbucket Server v5.11.1 format )
|
||||
branchUrl = `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/branches/default`;
|
||||
response = await fetch(branchUrl, getBitbucketServerRequestOptions(config));
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const message = `Failed to retrieve default branch from ${branchUrl}, ${response.status} ${response.statusText}`;
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const { displayId } = await response.json();
|
||||
const 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
|
||||
*/
|
||||
export async function getBitbucketServerDownloadUrl(
|
||||
url: string,
|
||||
config: BitbucketServerIntegrationConfig,
|
||||
): Promise<string> {
|
||||
const { name: repoName, owner: project, ref, filepath } = parseGitUrl(url);
|
||||
|
||||
let branch = ref;
|
||||
if (!branch) {
|
||||
branch = await getBitbucketServerDefaultBranch(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(filepath)}` : '';
|
||||
return `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/archive?format=tgz&at=${branch}&prefix=${project}-${repoName}${path}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.company.com/projectname/reponame/src/main/file.yaml
|
||||
* to: https://bitbucket.company.com/rest/api/1.0/project/projectname/reponame/raw/file.yaml?at=main
|
||||
*
|
||||
* @param url - A URL pointing to a file
|
||||
* @param config - The relevant provider config
|
||||
* @public
|
||||
*/
|
||||
export function getBitbucketServerFileFetchUrl(
|
||||
url: string,
|
||||
config: BitbucketServerIntegrationConfig,
|
||||
): string {
|
||||
try {
|
||||
const { owner, name, ref, filepathtype, filepath } = parseGitUrl(url);
|
||||
if (
|
||||
!owner ||
|
||||
!name ||
|
||||
(filepathtype !== 'browse' &&
|
||||
filepathtype !== 'raw' &&
|
||||
filepathtype !== 'src')
|
||||
) {
|
||||
throw new Error('Invalid Bitbucket Server URL or file path');
|
||||
}
|
||||
|
||||
const pathWithoutSlash = filepath.replace(/^\//, '');
|
||||
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
|
||||
*/
|
||||
export function getBitbucketServerRequestOptions(
|
||||
config: BitbucketServerIntegrationConfig,
|
||||
): { headers: Record<string, string> } {
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
if (config.token) {
|
||||
headers.Authorization = `Bearer ${config.token}`;
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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 { BitbucketServerIntegration } from './BitbucketServerIntegration';
|
||||
export {
|
||||
readBitbucketServerIntegrationConfig,
|
||||
readBitbucketServerIntegrationConfigs,
|
||||
} from './config';
|
||||
export type { BitbucketServerIntegrationConfig } from './config';
|
||||
export {
|
||||
getBitbucketServerDefaultBranch,
|
||||
getBitbucketServerDownloadUrl,
|
||||
getBitbucketServerFileFetchUrl,
|
||||
getBitbucketServerRequestOptions,
|
||||
} from './core';
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BitbucketIntegration } from './bitbucket';
|
||||
import { BitbucketServerIntegration } from './bitbucketServer';
|
||||
import {
|
||||
basicIntegrations,
|
||||
defaultScmResolveUrl,
|
||||
@@ -24,11 +24,11 @@ import {
|
||||
describe('basicIntegrations', () => {
|
||||
describe('byUrl', () => {
|
||||
it('handles hosts without a port', () => {
|
||||
const integration = new BitbucketIntegration({
|
||||
const integration = new BitbucketServerIntegration({
|
||||
host: 'host.com',
|
||||
apiBaseUrl: 'a',
|
||||
});
|
||||
const integrations = basicIntegrations<BitbucketIntegration>(
|
||||
const integrations = basicIntegrations<BitbucketServerIntegration>(
|
||||
[integration],
|
||||
i => i.config.host,
|
||||
);
|
||||
@@ -36,11 +36,11 @@ describe('basicIntegrations', () => {
|
||||
expect(integrations.byUrl('https://host.com:8080/a')).toBeUndefined();
|
||||
});
|
||||
it('handles hosts with a port', () => {
|
||||
const integration = new BitbucketIntegration({
|
||||
const integration = new BitbucketServerIntegration({
|
||||
host: 'host.com:8080',
|
||||
apiBaseUrl: 'a',
|
||||
});
|
||||
const integrations = basicIntegrations<BitbucketIntegration>(
|
||||
const integrations = basicIntegrations<BitbucketServerIntegration>(
|
||||
[integration],
|
||||
i => i.config.host,
|
||||
);
|
||||
|
||||
@@ -20,13 +20,15 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export * from './awsS3';
|
||||
export * from './azure';
|
||||
export * from './bitbucket';
|
||||
export * from './bitbucketCloud';
|
||||
export * from './bitbucketServer';
|
||||
export * from './gerrit';
|
||||
export * from './github';
|
||||
export * from './gitlab';
|
||||
export * from './googleGcs';
|
||||
export * from './awsS3';
|
||||
export { defaultScmResolveUrl } from './helpers';
|
||||
export { ScmIntegrations } from './ScmIntegrations';
|
||||
export type { IntegrationsByType } from './ScmIntegrations';
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
import { ScmIntegration, ScmIntegrationsGroup } from './types';
|
||||
import { AwsS3Integration } from './awsS3/AwsS3Integration';
|
||||
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';
|
||||
import { GitLabIntegration } from './gitlab/GitLabIntegration';
|
||||
@@ -31,7 +33,12 @@ export interface ScmIntegrationRegistry
|
||||
extends ScmIntegrationsGroup<ScmIntegration> {
|
||||
awsS3: ScmIntegrationsGroup<AwsS3Integration>;
|
||||
azure: ScmIntegrationsGroup<AzureIntegration>;
|
||||
/**
|
||||
* @deprecated in favor of `bitbucketCloud` and `bitbucketServer`
|
||||
*/
|
||||
bitbucket: ScmIntegrationsGroup<BitbucketIntegration>;
|
||||
bitbucketCloud: ScmIntegrationsGroup<BitbucketCloudIntegration>;
|
||||
bitbucketServer: ScmIntegrationsGroup<BitbucketServerIntegration>;
|
||||
gerrit: ScmIntegrationsGroup<GerritIntegration>;
|
||||
github: ScmIntegrationsGroup<GitHubIntegration>;
|
||||
gitlab: ScmIntegrationsGroup<GitLabIntegration>;
|
||||
|
||||
Reference in New Issue
Block a user