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
@@ -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>;