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:
@@ -10,7 +10,6 @@ import { AzureCredentialsManager } from '@backstage/integration';
|
||||
import { AzureDevOpsCredentialsProvider } from '@backstage/integration';
|
||||
import { AzureIntegration } from '@backstage/integration';
|
||||
import { BitbucketCloudIntegration } from '@backstage/integration';
|
||||
import { BitbucketIntegration } from '@backstage/integration';
|
||||
import { BitbucketServerIntegration } from '@backstage/integration';
|
||||
import { Config } from '@backstage/config';
|
||||
import { GerritIntegration } from '@backstage/integration';
|
||||
@@ -190,38 +189,6 @@ export class BitbucketServerUrlReader implements UrlReaderService {
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
// @public @deprecated
|
||||
export class BitbucketUrlReader implements UrlReaderService {
|
||||
constructor(
|
||||
integration: BitbucketIntegration,
|
||||
logger: LoggerService,
|
||||
deps: {
|
||||
treeResponseFactory: ReadTreeResponseFactory;
|
||||
},
|
||||
);
|
||||
// (undocumented)
|
||||
static factory: ReaderFactory;
|
||||
// (undocumented)
|
||||
read(url: string): Promise<Buffer>;
|
||||
// (undocumented)
|
||||
readTree(
|
||||
url: string,
|
||||
options?: UrlReaderServiceReadTreeOptions,
|
||||
): Promise<UrlReaderServiceReadTreeResponse>;
|
||||
// (undocumented)
|
||||
readUrl(
|
||||
url: string,
|
||||
options?: UrlReaderServiceReadUrlOptions,
|
||||
): Promise<UrlReaderServiceReadUrlResponse>;
|
||||
// (undocumented)
|
||||
search(
|
||||
url: string,
|
||||
options?: UrlReaderServiceSearchOptions,
|
||||
): Promise<UrlReaderServiceSearchResponse>;
|
||||
// (undocumented)
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class FetchUrlReader implements UrlReaderService {
|
||||
static factory: ReaderFactory;
|
||||
|
||||
@@ -1,656 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
BitbucketIntegration,
|
||||
readBitbucketIntegrationConfig,
|
||||
} from '@backstage/integration';
|
||||
import {
|
||||
createMockDirectory,
|
||||
mockServices,
|
||||
registerMswTestHooks,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import fs from 'fs-extra';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import path from 'node:path';
|
||||
import { NotModifiedError } from '@backstage/errors';
|
||||
import { BitbucketUrlReader } from './BitbucketUrlReader';
|
||||
import { DefaultReadTreeResponseFactory } from './tree';
|
||||
import getRawBody from 'raw-body';
|
||||
import { UrlReaderServiceReadUrlResponse } from '@backstage/backend-plugin-api';
|
||||
|
||||
const logger = mockServices.logger.mock();
|
||||
|
||||
describe('BitbucketUrlReader.factory', () => {
|
||||
it('only apply integration configs not inherited from bitbucketCloud or bitbucketServer', () => {
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
bitbucket: [],
|
||||
bitbucketCloud: [
|
||||
{
|
||||
username: 'username',
|
||||
appPassword: 'password',
|
||||
},
|
||||
],
|
||||
bitbucketServer: [
|
||||
{
|
||||
host: 'bitbucket-server.local',
|
||||
token: 'test-token',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
|
||||
config: config,
|
||||
});
|
||||
|
||||
const tuples = BitbucketUrlReader.factory({
|
||||
config,
|
||||
logger,
|
||||
treeResponseFactory,
|
||||
});
|
||||
|
||||
expect(tuples).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BitbucketUrlReader', () => {
|
||||
const mockDir = createMockDirectory({ mockOsTmpDir: true });
|
||||
|
||||
beforeEach(mockDir.clear);
|
||||
|
||||
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
|
||||
config: new ConfigReader({}),
|
||||
});
|
||||
|
||||
const bitbucketProcessor = new BitbucketUrlReader(
|
||||
new BitbucketIntegration(
|
||||
readBitbucketIntegrationConfig(
|
||||
new ConfigReader({
|
||||
host: 'bitbucket.org',
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
}),
|
||||
),
|
||||
),
|
||||
logger,
|
||||
{ treeResponseFactory },
|
||||
);
|
||||
|
||||
const hostedBitbucketProcessor = new BitbucketUrlReader(
|
||||
new BitbucketIntegration(
|
||||
readBitbucketIntegrationConfig(
|
||||
new ConfigReader({
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
|
||||
}),
|
||||
),
|
||||
),
|
||||
logger,
|
||||
{ treeResponseFactory },
|
||||
);
|
||||
|
||||
const worker = setupServer();
|
||||
registerMswTestHooks(worker);
|
||||
|
||||
describe('readUrl', () => {
|
||||
it('should be able to readUrl via buffer without ETag', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('If-None-Match')).toBeNull();
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.body('foo'),
|
||||
ctx.set('ETag', 'etag-value'),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const result = await bitbucketProcessor.readUrl(
|
||||
'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml',
|
||||
);
|
||||
const buffer = await result.buffer();
|
||||
expect(buffer.toString()).toBe('foo');
|
||||
});
|
||||
|
||||
it('should be able to readUrl using provided token', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('authorization')).toBe(
|
||||
'Bearer manual-token',
|
||||
);
|
||||
return res(ctx.status(200), ctx.body('foo'));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const result = await bitbucketProcessor.readUrl(
|
||||
'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml',
|
||||
{ token: 'manual-token' },
|
||||
);
|
||||
const buffer = await result.buffer();
|
||||
expect(buffer.toString()).toBe('foo');
|
||||
});
|
||||
|
||||
it('should be able to readUrl via stream without ETag', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('If-None-Match')).toBeNull();
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.body('foo'),
|
||||
ctx.set('ETag', 'etag-value'),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const result = await bitbucketProcessor.readUrl(
|
||||
'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml',
|
||||
);
|
||||
const fromStream = await getRawBody(result.stream!());
|
||||
expect(fromStream.toString()).toBe('foo');
|
||||
});
|
||||
|
||||
it('should be able to readUrl with matching ETag', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('If-None-Match')).toBe(
|
||||
'matching-etag-value',
|
||||
);
|
||||
return res(ctx.status(304));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
bitbucketProcessor.readUrl(
|
||||
'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml',
|
||||
{ etag: 'matching-etag-value' },
|
||||
),
|
||||
).rejects.toThrow(NotModifiedError);
|
||||
});
|
||||
|
||||
it('should be able to readUrl without matching ETag', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('If-None-Match')).toBe(
|
||||
'previous-etag-value',
|
||||
);
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.body('foo'),
|
||||
ctx.set('ETag', 'new-etag-value'),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const result = await bitbucketProcessor.readUrl(
|
||||
'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml',
|
||||
{ etag: 'previous-etag-value' },
|
||||
);
|
||||
const buffer = await result.buffer();
|
||||
expect(buffer.toString()).toBe('foo');
|
||||
expect(result.etag).toBe('new-etag-value');
|
||||
});
|
||||
|
||||
it('should be able to readUrl via buffer without If-Modified-Since', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('If-None-Match')).toBeNull();
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.body('foo'),
|
||||
ctx.set('ETag', 'etag-value'),
|
||||
ctx.set(
|
||||
'Last-Modified',
|
||||
new Date('2020-01-01T00:00:00Z').toUTCString(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const result = await bitbucketProcessor.readUrl(
|
||||
'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml',
|
||||
);
|
||||
const buffer = await result.buffer();
|
||||
expect(result.lastModifiedAt).toEqual(new Date('2020-01-01T00:00:00Z'));
|
||||
expect(buffer.toString()).toBe('foo');
|
||||
});
|
||||
|
||||
it('should be throw not modified when If-Modified-Since returns a 304', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('If-Modified-Since')).toBe(
|
||||
new Date('1999 12 31 23:59:59 GMT').toUTCString(),
|
||||
);
|
||||
return res(ctx.status(304));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
bitbucketProcessor.readUrl(
|
||||
'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml',
|
||||
{ lastModifiedAfter: new Date('1999 12 31 23:59:59 GMT') },
|
||||
),
|
||||
).rejects.toThrow(NotModifiedError);
|
||||
});
|
||||
|
||||
it('should be able to readUrl when If-Modified-Since is before Last-Modified', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('If-Modified-Since')).toBe(
|
||||
new Date('1999 12 31 23:59:59 GMT').toUTCString(),
|
||||
);
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.set(
|
||||
'Last-Modified',
|
||||
new Date('2020-01-01T00:00:00Z').toUTCString(),
|
||||
),
|
||||
ctx.body('foo'),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const result = await bitbucketProcessor.readUrl(
|
||||
'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml',
|
||||
{ lastModifiedAfter: new Date('1999 12 31 23:59:59 GMT') },
|
||||
);
|
||||
const buffer = await result.buffer();
|
||||
expect(buffer.toString()).toBe('foo');
|
||||
expect(result.lastModifiedAt).toEqual(new Date('2020-01-01T00:00:00Z'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('read', () => {
|
||||
it('rejects unknown targets', async () => {
|
||||
await expect(
|
||||
bitbucketProcessor.read('https://not.bitbucket.com/apa'),
|
||||
).rejects.toThrow(
|
||||
'Incorrect URL: https://not.bitbucket.com/apa, Error: Invalid Bitbucket URL or file path',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readTree', () => {
|
||||
const repoBuffer = fs.readFileSync(
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'__fixtures__/bitbucket-repo-with-commit-hash.tar.gz',
|
||||
),
|
||||
);
|
||||
|
||||
const privateBitbucketRepoBuffer = fs.readFileSync(
|
||||
path.resolve(__dirname, '__fixtures__/bitbucket-server-repo.tar.gz'),
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage/mock',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
mainbranch: {
|
||||
type: 'branch',
|
||||
name: 'master',
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://bitbucket.org/backstage/mock/get/master.tar.gz',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/zip'),
|
||||
ctx.set(
|
||||
'content-disposition',
|
||||
'attachment; filename=backstage-mock-12ab34cd56ef.tar.gz',
|
||||
),
|
||||
ctx.body(new Uint8Array(repoBuffer)),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage/mock/commits/master',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
values: [{ hash: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }],
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/zip'),
|
||||
ctx.set(
|
||||
'content-disposition',
|
||||
'attachment; filename=backstage-mock.tgz',
|
||||
),
|
||||
ctx.body(new Uint8Array(privateBitbucketRepoBuffer)),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/commits',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
values: [{ id: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }],
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the wanted files from an archive', async () => {
|
||||
const response = await bitbucketProcessor.readTree(
|
||||
'https://bitbucket.org/backstage/mock/src/master',
|
||||
);
|
||||
|
||||
expect(response.etag).toBe('12ab34cd56ef');
|
||||
|
||||
const files = await response.files();
|
||||
|
||||
expect(files.length).toBe(2);
|
||||
const mkDocsFile = await files[0].content();
|
||||
const indexMarkdownFile = await files[1].content();
|
||||
|
||||
expect(indexMarkdownFile.toString()).toBe('# Test\n');
|
||||
expect(mkDocsFile.toString()).toBe('site_name: Test\n');
|
||||
});
|
||||
|
||||
it('creates a directory with the wanted files', async () => {
|
||||
const response = await bitbucketProcessor.readTree(
|
||||
'https://bitbucket.org/backstage/mock',
|
||||
);
|
||||
|
||||
const dir = await response.dir({ targetDir: mockDir.path });
|
||||
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'),
|
||||
).resolves.toBe('site_name: Test\n');
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'docs', 'index.md'), 'utf8'),
|
||||
).resolves.toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('uses private bitbucket host', async () => {
|
||||
const response = await hostedBitbucketProcessor.readTree(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs?at=some-branch',
|
||||
);
|
||||
|
||||
expect(response.etag).toBe('12ab34cd56ef');
|
||||
|
||||
const files = await response.files();
|
||||
|
||||
expect(files.length).toBe(1);
|
||||
const indexMarkdownFile = await files[0].content();
|
||||
|
||||
expect(indexMarkdownFile.toString()).toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('returns the wanted files from an archive with a subpath', async () => {
|
||||
const response = await bitbucketProcessor.readTree(
|
||||
'https://bitbucket.org/backstage/mock/src/master/docs',
|
||||
);
|
||||
|
||||
expect(response.etag).toBe('12ab34cd56ef');
|
||||
|
||||
const files = await response.files();
|
||||
|
||||
expect(files.length).toBe(1);
|
||||
const indexMarkdownFile = await files[0].content();
|
||||
|
||||
expect(indexMarkdownFile.toString()).toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('creates a directory with the wanted files with a subpath', async () => {
|
||||
const response = await bitbucketProcessor.readTree(
|
||||
'https://bitbucket.org/backstage/mock/src/master/docs',
|
||||
);
|
||||
|
||||
const dir = await response.dir({ targetDir: mockDir.path });
|
||||
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'index.md'), 'utf8'),
|
||||
).resolves.toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('throws a NotModifiedError when given a etag in options', async () => {
|
||||
const fnBitbucket = async () => {
|
||||
await bitbucketProcessor.readTree(
|
||||
'https://bitbucket.org/backstage/mock',
|
||||
{ etag: '12ab34cd56ef' },
|
||||
);
|
||||
};
|
||||
|
||||
await expect(fnBitbucket).rejects.toThrow(NotModifiedError);
|
||||
});
|
||||
|
||||
it('should not throw a NotModifiedError when given an outdated etag in options', async () => {
|
||||
const response = await bitbucketProcessor.readTree(
|
||||
'https://bitbucket.org/backstage/mock',
|
||||
{ etag: 'outdatedetag123abc' },
|
||||
);
|
||||
|
||||
expect(response.etag).toBe('12ab34cd56ef');
|
||||
});
|
||||
});
|
||||
|
||||
describe('search hosted', () => {
|
||||
const repoBuffer = fs.readFileSync(
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'__fixtures__/bitbucket-repo-with-commit-hash.tar.gz',
|
||||
),
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage/mock',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
mainbranch: {
|
||||
type: 'branch',
|
||||
name: 'master',
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://bitbucket.org/backstage/mock/get/master.tar.gz',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/zip'),
|
||||
ctx.set(
|
||||
'content-disposition',
|
||||
'attachment; filename=backstage-mock-12ab34cd56ef.tar.gz',
|
||||
),
|
||||
ctx.body(new Uint8Array(repoBuffer)),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage/mock/commits/master',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
values: [{ hash: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }],
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('works for the naive case', async () => {
|
||||
const result = await bitbucketProcessor.search(
|
||||
'https://bitbucket.org/backstage/mock/src/master/**/index.*',
|
||||
);
|
||||
expect(result.etag).toBe('12ab34cd56ef');
|
||||
expect(result.files.length).toBe(1);
|
||||
expect(result.files[0].url).toBe(
|
||||
'https://bitbucket.org/backstage/mock/src/master/docs/index.md',
|
||||
);
|
||||
await expect(result.files[0].content()).resolves.toEqual(
|
||||
Buffer.from('# Test\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('works in nested folders', async () => {
|
||||
const result = await bitbucketProcessor.search(
|
||||
'https://bitbucket.org/backstage/mock/src/master/docs/index.*',
|
||||
);
|
||||
expect(result.etag).toBe('12ab34cd56ef');
|
||||
expect(result.files.length).toBe(1);
|
||||
expect(result.files[0].url).toBe(
|
||||
'https://bitbucket.org/backstage/mock/src/master/docs/index.md',
|
||||
);
|
||||
await expect(result.files[0].content()).resolves.toEqual(
|
||||
Buffer.from('# Test\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws NotModifiedError when same etag', async () => {
|
||||
await expect(
|
||||
bitbucketProcessor.search(
|
||||
'https://bitbucket.org/backstage/mock/src/master/**/index.*',
|
||||
{ etag: '12ab34cd56ef' },
|
||||
),
|
||||
).rejects.toThrow(NotModifiedError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('search private', () => {
|
||||
const privateBitbucketRepoBuffer = fs.readFileSync(
|
||||
path.resolve(__dirname, '__fixtures__/bitbucket-server-repo.tar.gz'),
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/zip'),
|
||||
ctx.set(
|
||||
'content-disposition',
|
||||
'attachment; filename=backstage-mock.tgz',
|
||||
),
|
||||
ctx.body(new Uint8Array(privateBitbucketRepoBuffer)),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/commits',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
values: [{ id: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }],
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('works for the naive case', async () => {
|
||||
const result = await hostedBitbucketProcessor.search(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/**/index.*?at=master',
|
||||
);
|
||||
expect(result.etag).toBe('12ab34cd56ef');
|
||||
expect(result.files.length).toBe(1);
|
||||
expect(result.files[0].url).toBe(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.md?at=master',
|
||||
);
|
||||
await expect(result.files[0].content()).resolves.toEqual(
|
||||
Buffer.from('# Test\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('works in nested folders', async () => {
|
||||
const result = await hostedBitbucketProcessor.search(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.*?at=master',
|
||||
);
|
||||
expect(result.etag).toBe('12ab34cd56ef');
|
||||
expect(result.files.length).toBe(1);
|
||||
expect(result.files[0].url).toBe(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.md?at=master',
|
||||
);
|
||||
await expect(result.files[0].content()).resolves.toEqual(
|
||||
Buffer.from('# Test\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws NotModifiedError when same etag', async () => {
|
||||
await expect(
|
||||
hostedBitbucketProcessor.search(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/**/index.*?at=master',
|
||||
{ etag: '12ab34cd56ef' },
|
||||
),
|
||||
).rejects.toThrow(NotModifiedError);
|
||||
});
|
||||
|
||||
it('should work for exact URLs', async () => {
|
||||
hostedBitbucketProcessor.readUrl = jest.fn().mockResolvedValue({
|
||||
buffer: async () => Buffer.from('content'),
|
||||
etag: 'etag',
|
||||
} as UrlReaderServiceReadUrlResponse);
|
||||
|
||||
const result = await hostedBitbucketProcessor.search(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.md?at=master',
|
||||
);
|
||||
expect(result.etag).toBe('etag');
|
||||
expect(result.files.length).toBe(1);
|
||||
expect(result.files[0].url).toBe(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.md?at=master',
|
||||
);
|
||||
expect((await result.files[0].content()).toString()).toEqual('content');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,313 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
UrlReaderService,
|
||||
UrlReaderServiceReadTreeOptions,
|
||||
UrlReaderServiceReadTreeResponse,
|
||||
UrlReaderServiceReadUrlOptions,
|
||||
UrlReaderServiceReadUrlResponse,
|
||||
UrlReaderServiceSearchOptions,
|
||||
UrlReaderServiceSearchResponse,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
assertError,
|
||||
NotFoundError,
|
||||
NotModifiedError,
|
||||
} from '@backstage/errors';
|
||||
import {
|
||||
BitbucketIntegration,
|
||||
getBitbucketDefaultBranch,
|
||||
getBitbucketDownloadUrl,
|
||||
getBitbucketFileFetchUrl,
|
||||
getBitbucketRequestOptions,
|
||||
ScmIntegrations,
|
||||
} from '@backstage/integration';
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import { trimEnd } from 'lodash';
|
||||
import { Minimatch } from 'minimatch';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { ReaderFactory, ReadTreeResponseFactory } from './types';
|
||||
import { ReadUrlResponseFactory } from './ReadUrlResponseFactory';
|
||||
|
||||
/**
|
||||
* Implements a {@link @backstage/backend-plugin-api#UrlReaderService} for files from Bitbucket v1 and v2 APIs, such
|
||||
* as the one exposed by Bitbucket Cloud itself.
|
||||
*
|
||||
* @public
|
||||
* @deprecated in favor of BitbucketCloudUrlReader and BitbucketServerUrlReader
|
||||
*/
|
||||
export class BitbucketUrlReader implements UrlReaderService {
|
||||
static factory: ReaderFactory = ({ config, logger, treeResponseFactory }) => {
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
return integrations.bitbucket
|
||||
.list()
|
||||
.filter(
|
||||
item =>
|
||||
!integrations.bitbucketCloud.byHost(item.config.host) &&
|
||||
!integrations.bitbucketServer.byHost(item.config.host),
|
||||
)
|
||||
.map(integration => {
|
||||
const reader = new BitbucketUrlReader(integration, logger, {
|
||||
treeResponseFactory,
|
||||
});
|
||||
const predicate = (url: URL) => url.host === integration.config.host;
|
||||
return { reader, predicate };
|
||||
});
|
||||
};
|
||||
|
||||
private readonly integration: BitbucketIntegration;
|
||||
private readonly deps: { treeResponseFactory: ReadTreeResponseFactory };
|
||||
|
||||
constructor(
|
||||
integration: BitbucketIntegration,
|
||||
logger: LoggerService,
|
||||
deps: { treeResponseFactory: ReadTreeResponseFactory },
|
||||
) {
|
||||
this.integration = integration;
|
||||
this.deps = deps;
|
||||
const { host, token, username, appPassword } = integration.config;
|
||||
const replacement =
|
||||
host === 'bitbucket.org' ? 'bitbucketCloud' : 'bitbucketServer';
|
||||
logger.warn(
|
||||
`[Deprecated] Please migrate from "integrations.bitbucket" to "integrations.${replacement}".`,
|
||||
);
|
||||
|
||||
if (!token && username && !appPassword) {
|
||||
throw new Error(
|
||||
`Bitbucket integration for '${host}' has configured a username but is missing a required appPassword.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async read(url: string): Promise<Buffer> {
|
||||
const response = await this.readUrl(url);
|
||||
return response.buffer();
|
||||
}
|
||||
|
||||
private getCredentials = async (options?: {
|
||||
token?: string;
|
||||
}): Promise<{ headers: Record<string, string> }> => {
|
||||
if (options?.token) {
|
||||
return {
|
||||
headers: {
|
||||
Authorization: `Bearer ${options.token}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return await getBitbucketRequestOptions(this.integration.config);
|
||||
};
|
||||
|
||||
async readUrl(
|
||||
url: string,
|
||||
options?: UrlReaderServiceReadUrlOptions,
|
||||
): Promise<UrlReaderServiceReadUrlResponse> {
|
||||
const { etag, lastModifiedAfter, signal } = options ?? {};
|
||||
const bitbucketUrl = getBitbucketFileFetchUrl(url, this.integration.config);
|
||||
const requestOptions = await this.getCredentials(options);
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(bitbucketUrl.toString(), {
|
||||
headers: {
|
||||
...requestOptions.headers,
|
||||
...(etag && { 'If-None-Match': etag }),
|
||||
...(lastModifiedAfter && {
|
||||
'If-Modified-Since': lastModifiedAfter.toUTCString(),
|
||||
}),
|
||||
},
|
||||
// TODO(freben): The signal cast is there because pre-3.x versions of
|
||||
// node-fetch have a very slightly deviating AbortSignal type signature.
|
||||
// The difference does not affect us in practice however. The cast can be
|
||||
// removed after we support ESM for CLI dependencies and migrate to
|
||||
// version 3 of node-fetch.
|
||||
// https://github.com/backstage/backstage/issues/8242
|
||||
...(signal && { signal: signal as any }),
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(`Unable to read ${url}, ${e}`);
|
||||
}
|
||||
|
||||
if (response.status === 304) {
|
||||
throw new NotModifiedError();
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
return ReadUrlResponseFactory.fromResponse(response);
|
||||
}
|
||||
|
||||
const message = `${url} could not be read as ${bitbucketUrl}, ${response.status} ${response.statusText}`;
|
||||
if (response.status === 404) {
|
||||
throw new NotFoundError(message);
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
async readTree(
|
||||
url: string,
|
||||
options?: UrlReaderServiceReadTreeOptions,
|
||||
): Promise<UrlReaderServiceReadTreeResponse> {
|
||||
const { filepath } = parseGitUrl(url);
|
||||
|
||||
const lastCommitShortHash = await this.getLastCommitShortHash(url);
|
||||
if (options?.etag && options.etag === lastCommitShortHash) {
|
||||
throw new NotModifiedError();
|
||||
}
|
||||
|
||||
const downloadUrl = await getBitbucketDownloadUrl(
|
||||
url,
|
||||
this.integration.config,
|
||||
);
|
||||
const archiveBitbucketResponse = await fetch(
|
||||
downloadUrl,
|
||||
getBitbucketRequestOptions(this.integration.config),
|
||||
);
|
||||
if (!archiveBitbucketResponse.ok) {
|
||||
const message = `Failed to read tree from ${url}, ${archiveBitbucketResponse.status} ${archiveBitbucketResponse.statusText}`;
|
||||
if (archiveBitbucketResponse.status === 404) {
|
||||
throw new NotFoundError(message);
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return await this.deps.treeResponseFactory.fromTarArchive({
|
||||
response: archiveBitbucketResponse,
|
||||
subpath: filepath,
|
||||
etag: lastCommitShortHash,
|
||||
filter: options?.filter,
|
||||
});
|
||||
}
|
||||
|
||||
async search(
|
||||
url: string,
|
||||
options?: UrlReaderServiceSearchOptions,
|
||||
): Promise<UrlReaderServiceSearchResponse> {
|
||||
const { filepath } = parseGitUrl(url);
|
||||
|
||||
// If it's a direct URL we use readUrl instead
|
||||
if (!filepath?.match(/[*?]/)) {
|
||||
try {
|
||||
const data = await this.readUrl(url, options);
|
||||
|
||||
return {
|
||||
files: [
|
||||
{
|
||||
url: url,
|
||||
content: data.buffer,
|
||||
lastModifiedAt: data.lastModifiedAt,
|
||||
},
|
||||
],
|
||||
etag: data.etag ?? '',
|
||||
};
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
if (error.name === 'NotFoundError') {
|
||||
return {
|
||||
files: [],
|
||||
etag: '',
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const matcher = new Minimatch(filepath);
|
||||
|
||||
// TODO(freben): For now, read the entire repo and filter through that. In
|
||||
// a future improvement, we could be smart and try to deduce that non-glob
|
||||
// prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used
|
||||
// to get just that part of the repo.
|
||||
const treeUrl = trimEnd(url.replace(filepath, ''), '/');
|
||||
|
||||
const tree = await this.readTree(treeUrl, {
|
||||
etag: options?.etag,
|
||||
filter: path => matcher.match(path),
|
||||
});
|
||||
const files = await tree.files();
|
||||
|
||||
return {
|
||||
etag: tree.etag,
|
||||
files: files.map(file => ({
|
||||
url: this.integration.resolveUrl({
|
||||
url: `/${file.path}`,
|
||||
base: url,
|
||||
}),
|
||||
content: file.content,
|
||||
lastModifiedAt: file.lastModifiedAt,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
toString() {
|
||||
const { host, token, username, appPassword } = this.integration.config;
|
||||
let authed = Boolean(token);
|
||||
if (!authed) {
|
||||
authed = Boolean(username && appPassword);
|
||||
}
|
||||
return `bitbucket{host=${host},authed=${authed}}`;
|
||||
}
|
||||
|
||||
private async getLastCommitShortHash(url: string): Promise<string> {
|
||||
const { resource, name: repoName, owner: project, ref } = parseGitUrl(url);
|
||||
|
||||
let branch = ref;
|
||||
if (!branch) {
|
||||
branch = await getBitbucketDefaultBranch(url, this.integration.config);
|
||||
}
|
||||
|
||||
const isHosted = resource === 'bitbucket.org';
|
||||
// Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp222
|
||||
const commitsApiUrl = isHosted
|
||||
? `${this.integration.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}`
|
||||
: `${this.integration.config.apiBaseUrl}/projects/${project}/repos/${repoName}/commits`;
|
||||
|
||||
const commitsResponse = await fetch(
|
||||
commitsApiUrl,
|
||||
getBitbucketRequestOptions(this.integration.config),
|
||||
);
|
||||
if (!commitsResponse.ok) {
|
||||
const message = `Failed to retrieve commits from ${commitsApiUrl}, ${commitsResponse.status} ${commitsResponse.statusText}`;
|
||||
if (commitsResponse.status === 404) {
|
||||
throw new NotFoundError(message);
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const commits = await commitsResponse.json();
|
||||
if (isHosted) {
|
||||
if (
|
||||
commits &&
|
||||
commits.values &&
|
||||
commits.values.length > 0 &&
|
||||
commits.values[0].hash
|
||||
) {
|
||||
return commits.values[0].hash.substring(0, 12);
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
commits &&
|
||||
commits.values &&
|
||||
commits.values.length > 0 &&
|
||||
commits.values[0].id
|
||||
) {
|
||||
return commits.values[0].id.substring(0, 12);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Failed to read response from ${commitsApiUrl}`);
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@ import { UrlReaderPredicateMux } from './UrlReaderPredicateMux';
|
||||
import { AzureUrlReader } from './AzureUrlReader';
|
||||
import { BitbucketCloudUrlReader } from './BitbucketCloudUrlReader';
|
||||
import { BitbucketServerUrlReader } from './BitbucketServerUrlReader';
|
||||
import { BitbucketUrlReader } from './BitbucketUrlReader';
|
||||
import { GerritUrlReader } from './GerritUrlReader';
|
||||
import { GithubUrlReader } from './GithubUrlReader';
|
||||
import { GitlabUrlReader } from './GitlabUrlReader';
|
||||
@@ -92,7 +91,6 @@ export class UrlReaders {
|
||||
AzureUrlReader.factory,
|
||||
BitbucketCloudUrlReader.factory,
|
||||
BitbucketServerUrlReader.factory,
|
||||
BitbucketUrlReader.factory,
|
||||
GerritUrlReader.factory,
|
||||
GithubUrlReader.factory,
|
||||
GiteaUrlReader.factory,
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
export { AzureUrlReader } from './AzureUrlReader';
|
||||
export { BitbucketCloudUrlReader } from './BitbucketCloudUrlReader';
|
||||
export { BitbucketUrlReader } from './BitbucketUrlReader';
|
||||
export { BitbucketServerUrlReader } from './BitbucketServerUrlReader';
|
||||
export { GerritUrlReader } from './GerritUrlReader';
|
||||
export { GithubUrlReader } from './GithubUrlReader';
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import { ScmIntegration, ScmIntegrationsGroup } from '@backstage/integration';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { scmIntegrationsApiRef } from '../src/ScmIntegrationsApi';
|
||||
import { scmIntegrationsApiRef } from '../src/api/ScmIntegrationsApi';
|
||||
|
||||
import { Content } from '@backstage/core-components';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
@@ -48,10 +48,6 @@ export const DevPage = () => {
|
||||
Azure
|
||||
</Typography>
|
||||
<Integrations group={integrations.azure} />
|
||||
<Typography paragraph variant="h2">
|
||||
Bitbucket
|
||||
</Typography>
|
||||
<Integrations group={integrations.bitbucket} />
|
||||
<Typography paragraph variant="h2">
|
||||
Bitbucket Cloud
|
||||
</Typography>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
import { createDevApp } from '@backstage/dev-utils';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { scmIntegrationsApiRef } from '../src/ScmIntegrationsApi';
|
||||
import { scmIntegrationsApiRef } from '../src/api/ScmIntegrationsApi';
|
||||
import { DevPage } from './DevPage';
|
||||
import { configApiRef, createApiFactory } from '@backstage/core-plugin-api';
|
||||
|
||||
|
||||
@@ -26,6 +26,6 @@ describe('scmIntegrationsApiRef', () => {
|
||||
|
||||
it('should be instantiated', () => {
|
||||
const i = ScmIntegrationsApi.fromConfig(new ConfigReader({}));
|
||||
expect(i.list().length).toBe(8); // The default ones
|
||||
expect(i.list().length).toBe(7); // The default ones
|
||||
});
|
||||
});
|
||||
|
||||
Vendored
-58
@@ -27,27 +27,6 @@ export interface Config {
|
||||
* @visibility frontend
|
||||
*/
|
||||
host: string;
|
||||
/**
|
||||
* Token used to authenticate requests.
|
||||
* @visibility secret
|
||||
* @deprecated Use `credentials` instead.
|
||||
*/
|
||||
token?: string;
|
||||
|
||||
/**
|
||||
* The credential to use for requests.
|
||||
*
|
||||
* If no credential is specified anonymous access is used.
|
||||
*
|
||||
* @deepVisibility secret
|
||||
* @deprecated Use `credentials` instead.
|
||||
*/
|
||||
credential?: {
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
tenantId?: string;
|
||||
personalAccessToken?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The credentials to use for requests. If multiple credentials are specified the first one that matches the organization is used.
|
||||
@@ -131,43 +110,6 @@ export interface Config {
|
||||
};
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Integration configuration for Bitbucket
|
||||
* @deprecated replaced by bitbucketCloud and bitbucketServer
|
||||
*/
|
||||
bitbucket?: Array<{
|
||||
/**
|
||||
* The hostname of the given Bitbucket instance
|
||||
* @visibility frontend
|
||||
*/
|
||||
host: string;
|
||||
/**
|
||||
* Token used to authenticate requests.
|
||||
* @visibility secret
|
||||
*/
|
||||
token?: string;
|
||||
/**
|
||||
* The base url for the Bitbucket API, for example https://api.bitbucket.org/2.0
|
||||
* @visibility frontend
|
||||
*/
|
||||
apiBaseUrl?: string;
|
||||
/**
|
||||
* The username to use for authenticated requests.
|
||||
* @visibility secret
|
||||
*/
|
||||
username?: string;
|
||||
/**
|
||||
* Bitbucket app password used to authenticate requests.
|
||||
* @visibility secret
|
||||
*/
|
||||
appPassword?: string;
|
||||
/**
|
||||
* PGP signing key for signing commits.
|
||||
* @visibility secret
|
||||
*/
|
||||
commitSigningKey?: string;
|
||||
}>;
|
||||
|
||||
/** Integration configuration for Bitbucket Cloud */
|
||||
bitbucketCloud?: Array<{
|
||||
/**
|
||||
|
||||
@@ -201,8 +201,6 @@ export class AzureIntegration implements ScmIntegration {
|
||||
// @public
|
||||
export type AzureIntegrationConfig = {
|
||||
host: string;
|
||||
token?: string;
|
||||
credential?: AzureDevOpsCredential;
|
||||
credentials?: AzureDevOpsCredential[];
|
||||
commitSigningKey?: string;
|
||||
};
|
||||
@@ -255,37 +253,6 @@ export type BitbucketCloudIntegrationConfig = {
|
||||
commitSigningKey?: string;
|
||||
};
|
||||
|
||||
// @public @deprecated
|
||||
export class BitbucketIntegration implements ScmIntegration {
|
||||
constructor(integrationConfig: BitbucketIntegrationConfig);
|
||||
// (undocumented)
|
||||
get config(): BitbucketIntegrationConfig;
|
||||
// (undocumented)
|
||||
static factory: ScmIntegrationsFactory<BitbucketIntegration>;
|
||||
// (undocumented)
|
||||
resolveEditUrl(url: string): string;
|
||||
// (undocumented)
|
||||
resolveUrl(options: {
|
||||
url: string;
|
||||
base: string;
|
||||
lineNumber?: number;
|
||||
}): string;
|
||||
// (undocumented)
|
||||
get title(): string;
|
||||
// (undocumented)
|
||||
get type(): string;
|
||||
}
|
||||
|
||||
// @public @deprecated
|
||||
export type BitbucketIntegrationConfig = {
|
||||
host: string;
|
||||
apiBaseUrl: string;
|
||||
token?: string;
|
||||
username?: string;
|
||||
appPassword?: string;
|
||||
commitSigningKey?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export class BitbucketServerIntegration implements ScmIntegration {
|
||||
constructor(integrationConfig: BitbucketServerIntegrationConfig);
|
||||
@@ -317,14 +284,6 @@ export type BitbucketServerIntegrationConfig = {
|
||||
commitSigningKey?: string;
|
||||
};
|
||||
|
||||
// @public @deprecated
|
||||
export function buildGerritGitilesArchiveUrl(
|
||||
config: GerritIntegrationConfig,
|
||||
project: string,
|
||||
branch: string,
|
||||
filePath: string,
|
||||
): string;
|
||||
|
||||
// @public
|
||||
export function buildGerritGitilesArchiveUrlFromLocation(
|
||||
config: GerritIntegrationConfig,
|
||||
@@ -426,14 +385,6 @@ export function getAzureDownloadUrl(url: string): string;
|
||||
// @public
|
||||
export function getAzureFileFetchUrl(url: string): string;
|
||||
|
||||
// @public @deprecated
|
||||
export function getAzureRequestOptions(
|
||||
config: AzureIntegrationConfig,
|
||||
additionalHeaders?: Record<string, string>,
|
||||
): Promise<{
|
||||
headers: Record<string, string>;
|
||||
}>;
|
||||
|
||||
// @public
|
||||
export function getBitbucketCloudDefaultBranch(
|
||||
url: string,
|
||||
@@ -465,31 +416,6 @@ export function getBitbucketCloudRequestOptions(
|
||||
headers: Record<string, string>;
|
||||
}>;
|
||||
|
||||
// @public @deprecated
|
||||
export function getBitbucketDefaultBranch(
|
||||
url: string,
|
||||
config: BitbucketIntegrationConfig,
|
||||
): Promise<string>;
|
||||
|
||||
// @public @deprecated
|
||||
export function getBitbucketDownloadUrl(
|
||||
url: string,
|
||||
config: BitbucketIntegrationConfig,
|
||||
): Promise<string>;
|
||||
|
||||
// @public @deprecated
|
||||
export function getBitbucketFileFetchUrl(
|
||||
url: string,
|
||||
config: BitbucketIntegrationConfig,
|
||||
): string;
|
||||
|
||||
// @public @deprecated
|
||||
export function getBitbucketRequestOptions(
|
||||
config: BitbucketIntegrationConfig,
|
||||
): {
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function getBitbucketServerDefaultBranch(
|
||||
url: string,
|
||||
@@ -579,14 +505,6 @@ export function getGithubFileFetchUrl(
|
||||
credentials: GithubCredentials,
|
||||
): string;
|
||||
|
||||
// @public @deprecated
|
||||
export function getGitHubRequestOptions(
|
||||
config: GithubIntegrationConfig,
|
||||
credentials: GithubCredentials,
|
||||
): {
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function getGitilesAuthenticationUrl(
|
||||
config: GerritIntegrationConfig,
|
||||
@@ -854,8 +772,6 @@ export interface IntegrationsByType {
|
||||
azure: ScmIntegrationsGroup<AzureIntegration>;
|
||||
// (undocumented)
|
||||
azureBlobStorage: ScmIntegrationsGroup<AzureBlobStorageIntergation>;
|
||||
// @deprecated (undocumented)
|
||||
bitbucket: ScmIntegrationsGroup<BitbucketIntegration>;
|
||||
// (undocumented)
|
||||
bitbucketCloud: ScmIntegrationsGroup<BitbucketCloudIntegration>;
|
||||
// (undocumented)
|
||||
@@ -874,16 +790,6 @@ export interface IntegrationsByType {
|
||||
harness: ScmIntegrationsGroup<HarnessIntegration>;
|
||||
}
|
||||
|
||||
// @public @deprecated
|
||||
export function parseGerritGitilesUrl(
|
||||
config: GerritIntegrationConfig,
|
||||
url: string,
|
||||
): {
|
||||
branch: string;
|
||||
filePath: string;
|
||||
project: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function parseGerritJsonResponse(response: Response): Promise<unknown>;
|
||||
|
||||
@@ -989,16 +895,6 @@ export function readBitbucketCloudIntegrationConfigs(
|
||||
configs: Config[],
|
||||
): BitbucketCloudIntegrationConfig[];
|
||||
|
||||
// @public @deprecated
|
||||
export function readBitbucketIntegrationConfig(
|
||||
config: Config,
|
||||
): BitbucketIntegrationConfig;
|
||||
|
||||
// @public @deprecated
|
||||
export function readBitbucketIntegrationConfigs(
|
||||
configs: Config[],
|
||||
): BitbucketIntegrationConfig[];
|
||||
|
||||
// @public
|
||||
export function readBitbucketServerIntegrationConfig(
|
||||
config: Config,
|
||||
@@ -1085,8 +981,6 @@ export interface ScmIntegrationRegistry
|
||||
azure: ScmIntegrationsGroup<AzureIntegration>;
|
||||
// (undocumented)
|
||||
azureBlobStorage: ScmIntegrationsGroup<AzureBlobStorageIntergation>;
|
||||
// @deprecated (undocumented)
|
||||
bitbucket: ScmIntegrationsGroup<BitbucketIntegration>;
|
||||
// (undocumented)
|
||||
bitbucketCloud: ScmIntegrationsGroup<BitbucketCloudIntegration>;
|
||||
// (undocumented)
|
||||
@@ -1120,8 +1014,6 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
|
||||
get azure(): ScmIntegrationsGroup<AzureIntegration>;
|
||||
// (undocumented)
|
||||
get azureBlobStorage(): ScmIntegrationsGroup<AzureBlobStorageIntergation>;
|
||||
// @deprecated (undocumented)
|
||||
get bitbucket(): ScmIntegrationsGroup<BitbucketIntegration>;
|
||||
// (undocumented)
|
||||
get bitbucketCloud(): ScmIntegrationsGroup<BitbucketCloudIntegration>;
|
||||
// (undocumented)
|
||||
|
||||
@@ -21,8 +21,6 @@ import {
|
||||
BitbucketCloudIntegration,
|
||||
BitbucketCloudIntegrationConfig,
|
||||
} from './bitbucketCloud';
|
||||
import { BitbucketIntegrationConfig } from './bitbucket';
|
||||
import { BitbucketIntegration } from './bitbucket/BitbucketIntegration';
|
||||
import {
|
||||
BitbucketServerIntegration,
|
||||
BitbucketServerIntegrationConfig,
|
||||
@@ -62,10 +60,6 @@ describe('ScmIntegrations', () => {
|
||||
host: 'azureblobstorage.local',
|
||||
} as AzureBlobStorageIntegrationConfig);
|
||||
|
||||
const bitbucket = new BitbucketIntegration({
|
||||
host: 'bitbucket.local',
|
||||
} as BitbucketIntegrationConfig);
|
||||
|
||||
const bitbucketCloud = new BitbucketCloudIntegration({
|
||||
host: 'bitbucket.org',
|
||||
} as BitbucketCloudIntegrationConfig);
|
||||
@@ -103,7 +97,6 @@ describe('ScmIntegrations', () => {
|
||||
awsCodeCommit: basicIntegrations([awsCodeCommit], item => item.config.host),
|
||||
azure: basicIntegrations([azure], item => item.config.host),
|
||||
azureBlobStorage: basicIntegrations([azureBlob], item => item.config.host),
|
||||
bitbucket: basicIntegrations([bitbucket], item => item.config.host),
|
||||
bitbucketCloud: basicIntegrations([bitbucketCloud], item => item.title),
|
||||
bitbucketServer: basicIntegrations(
|
||||
[bitbucketServer],
|
||||
@@ -126,7 +119,6 @@ describe('ScmIntegrations', () => {
|
||||
expect(i.azureBlobStorage.byUrl('https://azureblobstorage.local')).toBe(
|
||||
azureBlob,
|
||||
);
|
||||
expect(i.bitbucket.byUrl('https://bitbucket.local')).toBe(bitbucket);
|
||||
expect(i.bitbucketCloud.byUrl('https://bitbucket.org')).toBe(
|
||||
bitbucketCloud,
|
||||
);
|
||||
@@ -147,7 +139,6 @@ describe('ScmIntegrations', () => {
|
||||
awsCodeCommit,
|
||||
azure,
|
||||
azureBlob,
|
||||
bitbucket,
|
||||
bitbucketCloud,
|
||||
bitbucketServer,
|
||||
gerrit,
|
||||
@@ -166,7 +157,6 @@ describe('ScmIntegrations', () => {
|
||||
expect(i.azureBlobStorage.byUrl('https://azureblobstorage.local')).toBe(
|
||||
azureBlob,
|
||||
);
|
||||
expect(i.byUrl('https://bitbucket.local')).toBe(bitbucket);
|
||||
expect(i.byUrl('https://bitbucket.org')).toBe(bitbucketCloud);
|
||||
expect(i.byUrl('https://bitbucket-server.local')).toBe(bitbucketServer);
|
||||
expect(i.byUrl('https://gerrit.local')).toBe(gerrit);
|
||||
@@ -179,7 +169,6 @@ describe('ScmIntegrations', () => {
|
||||
expect(i.byHost('awscodecommit.local')).toBe(awsCodeCommit);
|
||||
expect(i.byHost('azure.local')).toBe(azure);
|
||||
expect(i.byHost('azureblobstorage.local')).toBe(azureBlob);
|
||||
expect(i.byHost('bitbucket.local')).toBe(bitbucket);
|
||||
expect(i.byHost('bitbucket.org')).toBe(bitbucketCloud);
|
||||
expect(i.byHost('bitbucket-server.local')).toBe(bitbucketServer);
|
||||
expect(i.byHost('gerrit.local')).toBe(gerrit);
|
||||
|
||||
@@ -19,7 +19,6 @@ import { AwsS3Integration } from './awsS3/AwsS3Integration';
|
||||
import { AwsCodeCommitIntegration } from './awsCodeCommit/AwsCodeCommitIntegration';
|
||||
import { AzureIntegration } from './azure/AzureIntegration';
|
||||
import { BitbucketCloudIntegration } from './bitbucketCloud/BitbucketCloudIntegration';
|
||||
import { BitbucketIntegration } from './bitbucket/BitbucketIntegration';
|
||||
import { BitbucketServerIntegration } from './bitbucketServer/BitbucketServerIntegration';
|
||||
import { GerritIntegration } from './gerrit/GerritIntegration';
|
||||
import { GithubIntegration } from './github/GithubIntegration';
|
||||
@@ -42,10 +41,6 @@ export interface IntegrationsByType {
|
||||
awsCodeCommit: ScmIntegrationsGroup<AwsCodeCommitIntegration>;
|
||||
azureBlobStorage: ScmIntegrationsGroup<AzureBlobStorageIntergation>;
|
||||
azure: ScmIntegrationsGroup<AzureIntegration>;
|
||||
/**
|
||||
* @deprecated in favor of `bitbucketCloud` and `bitbucketServer`
|
||||
*/
|
||||
bitbucket: ScmIntegrationsGroup<BitbucketIntegration>;
|
||||
bitbucketCloud: ScmIntegrationsGroup<BitbucketCloudIntegration>;
|
||||
bitbucketServer: ScmIntegrationsGroup<BitbucketServerIntegration>;
|
||||
gerrit: ScmIntegrationsGroup<GerritIntegration>;
|
||||
@@ -70,7 +65,6 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
|
||||
awsCodeCommit: AwsCodeCommitIntegration.factory({ config }),
|
||||
azureBlobStorage: AzureBlobStorageIntergation.factory({ config }),
|
||||
azure: AzureIntegration.factory({ config }),
|
||||
bitbucket: BitbucketIntegration.factory({ config }),
|
||||
bitbucketCloud: BitbucketCloudIntegration.factory({ config }),
|
||||
bitbucketServer: BitbucketServerIntegration.factory({ config }),
|
||||
gerrit: GerritIntegration.factory({ config }),
|
||||
@@ -102,13 +96,6 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
|
||||
return this.byType.azure;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated in favor of `bitbucketCloud()` and `bitbucketServer()`
|
||||
*/
|
||||
get bitbucket(): ScmIntegrationsGroup<BitbucketIntegration> {
|
||||
return this.byType.bitbucket;
|
||||
}
|
||||
|
||||
get bitbucketCloud(): ScmIntegrationsGroup<BitbucketCloudIntegration> {
|
||||
return this.byType.bitbucketCloud;
|
||||
}
|
||||
@@ -148,20 +135,10 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
|
||||
}
|
||||
|
||||
byUrl(url: string | URL): ScmIntegration | undefined {
|
||||
let candidates = Object.values(this.byType)
|
||||
const candidates = Object.values(this.byType)
|
||||
.map(i => i.byUrl(url))
|
||||
.filter(Boolean);
|
||||
|
||||
// Do not return deprecated integrations if there are other options
|
||||
if (candidates.length > 1) {
|
||||
const filteredCandidates = candidates.filter(
|
||||
x => !(x instanceof BitbucketIntegration),
|
||||
);
|
||||
if (filteredCandidates.length !== 0) {
|
||||
candidates = filteredCandidates;
|
||||
}
|
||||
}
|
||||
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
|
||||
@@ -276,50 +276,6 @@ describe('readAzureIntegrationConfig', () => {
|
||||
expect(output).toEqual({ host: 'dev.azure.com' });
|
||||
});
|
||||
|
||||
it('maps deprecated token to credentials', () => {
|
||||
const output = readAzureIntegrationConfig(
|
||||
buildConfig({
|
||||
host: 'dev.azure.com',
|
||||
token: 't',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(output).toEqual({
|
||||
host: 'dev.azure.com',
|
||||
credentials: [
|
||||
{
|
||||
kind: 'PersonalAccessToken',
|
||||
personalAccessToken: 't',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('maps deprecated credential to credentials', () => {
|
||||
const output = readAzureIntegrationConfig(
|
||||
buildConfig({
|
||||
host: 'dev.azure.com',
|
||||
credential: {
|
||||
clientId: 'id',
|
||||
clientSecret: 'secret',
|
||||
tenantId: 'tenantId',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(output).toEqual({
|
||||
host: 'dev.azure.com',
|
||||
credentials: [
|
||||
{
|
||||
kind: 'ClientSecret',
|
||||
clientId: 'id',
|
||||
clientSecret: 'secret',
|
||||
tenantId: 'tenantId',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects config when host is not valid', () => {
|
||||
expect(() =>
|
||||
readAzureIntegrationConfig(buildConfig({ ...valid, host: 7 })),
|
||||
|
||||
@@ -32,24 +32,6 @@ export type AzureIntegrationConfig = {
|
||||
*/
|
||||
host: string;
|
||||
|
||||
/**
|
||||
* The authorization token to use for requests.
|
||||
*
|
||||
* If no token is specified, anonymous access is used.
|
||||
*
|
||||
* @deprecated Use `credentials` instead.
|
||||
*/
|
||||
token?: string;
|
||||
|
||||
/**
|
||||
* The credential to use for requests.
|
||||
*
|
||||
* If no credential is specified anonymous access is used.
|
||||
*
|
||||
* @deprecated Use `credentials` instead.
|
||||
*/
|
||||
credential?: AzureDevOpsCredential;
|
||||
|
||||
/**
|
||||
* The credentials to use for requests. If multiple credentials are specified the first one that matches the organization is used.
|
||||
* If not organization matches the first credential without an organization is used.
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { getAzureRequestOptions } from './deprecated';
|
||||
import { DateTime } from 'luxon';
|
||||
import {
|
||||
AccessToken,
|
||||
ClientSecretCredential,
|
||||
ManagedIdentityCredential,
|
||||
} from '@azure/identity';
|
||||
|
||||
jest.mock('@azure/identity');
|
||||
|
||||
const MockedClientSecretCredential = ClientSecretCredential as jest.MockedClass<
|
||||
typeof ClientSecretCredential
|
||||
>;
|
||||
|
||||
const MockedManagedIdentityCredential =
|
||||
ManagedIdentityCredential as jest.MockedClass<
|
||||
typeof ManagedIdentityCredential
|
||||
>;
|
||||
|
||||
describe('azure core', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
MockedClientSecretCredential.prototype.getToken.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(),
|
||||
token: 'fake-client-secret-token',
|
||||
} as AccessToken),
|
||||
);
|
||||
MockedManagedIdentityCredential.prototype.getToken.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(),
|
||||
token: 'fake-managed-identity-token',
|
||||
} as AccessToken),
|
||||
);
|
||||
});
|
||||
|
||||
describe('getAzureRequestOptions', () => {
|
||||
it('should not add authorization header when not using token or credential', async () => {
|
||||
expect(await getAzureRequestOptions({ host: '' })).toEqual(
|
||||
expect.objectContaining({
|
||||
headers: expect.not.objectContaining({
|
||||
Authorization: expect.anything(),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should add authorization header when using a personal access token', async () => {
|
||||
const pat = '0123456789';
|
||||
const encoded = Buffer.from(`:${pat}`).toString('base64');
|
||||
expect(
|
||||
await getAzureRequestOptions({
|
||||
host: '',
|
||||
credentials: [
|
||||
{
|
||||
kind: 'PersonalAccessToken',
|
||||
personalAccessToken: pat,
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: `Basic ${encoded}`,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should add authorization header when using a client secret', async () => {
|
||||
expect(
|
||||
await getAzureRequestOptions({
|
||||
host: '',
|
||||
credentials: [
|
||||
{
|
||||
kind: 'ClientSecret',
|
||||
clientId: 'fake-id',
|
||||
clientSecret: 'fake-secret',
|
||||
tenantId: 'fake-tenant',
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer fake-client-secret-token',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should add authorization header when using a managed identity', async () => {
|
||||
expect(
|
||||
await getAzureRequestOptions({
|
||||
host: '',
|
||||
credentials: [
|
||||
{
|
||||
kind: 'ManagedIdentity',
|
||||
clientId: 'fake-id',
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer fake-managed-identity-token',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { AzureIntegrationConfig } from './config';
|
||||
import { CachedAzureDevOpsCredentialsProvider } from './CachedAzureDevOpsCredentialsProvider';
|
||||
|
||||
/**
|
||||
* Gets the request options necessary to make requests to a given provider.
|
||||
*
|
||||
* @param config - The relevant provider config
|
||||
* @param additionalHeaders - Additional headers for the request
|
||||
* @public
|
||||
* @deprecated Use {@link AzureDevOpsCredentialsProvider} instead.
|
||||
*/
|
||||
export async function getAzureRequestOptions(
|
||||
config: AzureIntegrationConfig,
|
||||
additionalHeaders?: Record<string, string>,
|
||||
): Promise<{ headers: Record<string, string> }> {
|
||||
const headers: Record<string, string> = additionalHeaders
|
||||
? { ...additionalHeaders }
|
||||
: {};
|
||||
|
||||
/*
|
||||
* Since we do not have a way to determine which organization the request is for,
|
||||
* we will use the first credential that does not have an organization specified.
|
||||
*/
|
||||
const credentialConfig = config.credentials?.filter(
|
||||
credential =>
|
||||
credential.organizations === undefined ||
|
||||
credential.organizations.length === 0,
|
||||
)[0];
|
||||
|
||||
if (credentialConfig) {
|
||||
const credentialsProvider =
|
||||
CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential(
|
||||
credentialConfig,
|
||||
);
|
||||
const credentials = await credentialsProvider.getCredentials();
|
||||
|
||||
return {
|
||||
headers: {
|
||||
...credentials?.headers,
|
||||
...headers,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { headers };
|
||||
}
|
||||
@@ -38,5 +38,3 @@ export {
|
||||
|
||||
export * from './types';
|
||||
export { DefaultAzureDevOpsCredentialsProvider } from './DefaultAzureDevOpsCredentialsProvider';
|
||||
|
||||
export * from './deprecated';
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { BitbucketIntegration } from './BitbucketIntegration';
|
||||
|
||||
describe('BitbucketIntegration', () => {
|
||||
describe('factory', () => {
|
||||
it('works', () => {
|
||||
const integrations = BitbucketIntegration.factory({
|
||||
config: new ConfigReader({
|
||||
integrations: {
|
||||
bitbucket: [
|
||||
{
|
||||
host: 'h.com',
|
||||
apiBaseUrl: 'a',
|
||||
token: 't',
|
||||
username: 'u',
|
||||
appPassword: 'p',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(integrations.list().length).toBe(2); // including default
|
||||
expect(integrations.list()[0].config.host).toBe('h.com');
|
||||
expect(integrations.list()[1].config.host).toBe('bitbucket.org');
|
||||
});
|
||||
|
||||
it('falls back to bitbucketCloud+bitbucketServer', () => {
|
||||
const integrations = BitbucketIntegration.factory({
|
||||
config: new ConfigReader({
|
||||
integrations: {
|
||||
bitbucketCloud: [
|
||||
{
|
||||
username: 'u',
|
||||
appPassword: 'p',
|
||||
},
|
||||
],
|
||||
bitbucketServer: [
|
||||
{
|
||||
host: 'h.com',
|
||||
apiBaseUrl: 'a',
|
||||
token: 't',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(integrations.list().length).toBe(2); // including default
|
||||
expect(integrations.list()[0].config.host).toBe('bitbucket.org');
|
||||
expect(integrations.list()[1].config.host).toBe('h.com');
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the basics', () => {
|
||||
const integration = new BitbucketIntegration({ host: 'h.com' } as any);
|
||||
expect(integration.type).toBe('bitbucket');
|
||||
expect(integration.title).toBe('h.com');
|
||||
});
|
||||
|
||||
it('resolves url line number correctly for Bitbucket Cloud', () => {
|
||||
const integration = new BitbucketIntegration({
|
||||
host: 'bitbucket.org',
|
||||
} as any);
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: './a.yaml',
|
||||
base: 'https://bitbucket.org/my-owner/my-project/src/master/README.md',
|
||||
lineNumber: 14,
|
||||
}),
|
||||
).toBe(
|
||||
'https://bitbucket.org/my-owner/my-project/src/master/a.yaml#lines-14',
|
||||
);
|
||||
});
|
||||
|
||||
it('resolves url line number correctly for Bitbucket Server', () => {
|
||||
const integration = new BitbucketIntegration({ host: 'h.com' } as any);
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: './a.yaml',
|
||||
base: 'https://bitbucket.org/my-owner/my-project/src/master/README.md',
|
||||
lineNumber: 14,
|
||||
}),
|
||||
).toBe('https://bitbucket.org/my-owner/my-project/src/master/a.yaml#14');
|
||||
});
|
||||
|
||||
it('resolve edit URL', () => {
|
||||
const integration = new BitbucketIntegration({ host: 'h.com' } as any);
|
||||
|
||||
expect(
|
||||
integration.resolveEditUrl(
|
||||
'https://bitbucket.org/my-owner/my-project/src/master/README.md',
|
||||
),
|
||||
).toBe(
|
||||
'https://bitbucket.org/my-owner/my-project/src/master/README.md?mode=edit&spa=0&at=master',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,99 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import { basicIntegrations, defaultScmResolveUrl } from '../helpers';
|
||||
import { ScmIntegration, ScmIntegrationsFactory } from '../types';
|
||||
import {
|
||||
BitbucketIntegrationConfig,
|
||||
readBitbucketIntegrationConfigs,
|
||||
} from './config';
|
||||
|
||||
/**
|
||||
* A Bitbucket based integration.
|
||||
*
|
||||
* @public
|
||||
* @deprecated replaced by the integrations bitbucketCloud and bitbucketServer.
|
||||
*/
|
||||
export class BitbucketIntegration implements ScmIntegration {
|
||||
static factory: ScmIntegrationsFactory<BitbucketIntegration> = ({
|
||||
config,
|
||||
}) => {
|
||||
const configs = readBitbucketIntegrationConfigs(
|
||||
config.getOptionalConfigArray('integrations.bitbucket') ?? [
|
||||
// if integrations.bitbucket was not used assume the use was migrated to the new configs
|
||||
// and backport for the deprecated integration to be usable for other parts of the system
|
||||
// until these got migrated
|
||||
...(config.getOptionalConfigArray('integrations.bitbucketCloud') ?? []),
|
||||
...(config.getOptionalConfigArray('integrations.bitbucketServer') ??
|
||||
[]),
|
||||
],
|
||||
);
|
||||
return basicIntegrations(
|
||||
configs.map(c => new BitbucketIntegration(c)),
|
||||
i => i.config.host,
|
||||
);
|
||||
};
|
||||
|
||||
constructor(private readonly integrationConfig: BitbucketIntegrationConfig) {}
|
||||
|
||||
get type(): string {
|
||||
return 'bitbucket';
|
||||
}
|
||||
|
||||
get title(): string {
|
||||
return this.integrationConfig.host;
|
||||
}
|
||||
|
||||
get config(): BitbucketIntegrationConfig {
|
||||
return this.integrationConfig;
|
||||
}
|
||||
|
||||
resolveUrl(options: {
|
||||
url: string;
|
||||
base: string;
|
||||
lineNumber?: number;
|
||||
}): string {
|
||||
const resolved = defaultScmResolveUrl(options);
|
||||
if (!options.lineNumber) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
const url = new URL(resolved);
|
||||
|
||||
if (this.integrationConfig.host === 'bitbucket.org') {
|
||||
// Bitbucket Cloud uses the syntax #lines-{start}[:{end}][,...]
|
||||
url.hash = `lines-${options.lineNumber}`;
|
||||
} else {
|
||||
// Bitbucket Server uses the syntax #{start}[-{end}][,...]
|
||||
url.hash = `${options.lineNumber}`;
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
resolveEditUrl(url: string): string {
|
||||
const urlData = parseGitUrl(url);
|
||||
const editUrl = new URL(url);
|
||||
|
||||
editUrl.searchParams.set('mode', 'edit');
|
||||
// TODO: Not sure what spa=0 does, at least bitbucket.org doesn't support it
|
||||
// but this is taken over from the initial implementation.
|
||||
editUrl.searchParams.set('spa', '0');
|
||||
editUrl.searchParams.set('at', urlData.ref);
|
||||
return editUrl.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config, ConfigReader } from '@backstage/config';
|
||||
import { loadConfigSchema } from '@backstage/config-loader';
|
||||
import {
|
||||
BitbucketIntegrationConfig,
|
||||
readBitbucketIntegrationConfig,
|
||||
readBitbucketIntegrationConfigs,
|
||||
} from './config';
|
||||
|
||||
describe('readBitbucketIntegrationConfig', () => {
|
||||
function buildConfig(data: Partial<BitbucketIntegrationConfig>): Config {
|
||||
return new ConfigReader(data);
|
||||
}
|
||||
|
||||
async function buildFrontendConfig(
|
||||
data: Partial<BitbucketIntegrationConfig>,
|
||||
): Promise<Config> {
|
||||
const fullSchema = await loadConfigSchema({
|
||||
dependencies: ['@backstage/integration'],
|
||||
});
|
||||
const serializedSchema = fullSchema.serialize() as {
|
||||
schemas: { value: { properties?: { integrations?: object } } }[];
|
||||
};
|
||||
const schema = await loadConfigSchema({
|
||||
serialized: {
|
||||
...serializedSchema, // only include schemas that apply to integrations
|
||||
schemas: serializedSchema.schemas.filter(
|
||||
s => s.value?.properties?.integrations,
|
||||
),
|
||||
},
|
||||
});
|
||||
const processed = schema.process(
|
||||
[{ data: { integrations: { bitbucket: [data] } }, context: 'app' }],
|
||||
{ visibility: ['frontend'] },
|
||||
);
|
||||
return new ConfigReader(
|
||||
(processed[0].data as any).integrations.bitbucket[0],
|
||||
);
|
||||
}
|
||||
|
||||
it('reads all values', () => {
|
||||
const output = readBitbucketIntegrationConfig(
|
||||
buildConfig({
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
token: 't\n\n\n',
|
||||
username: 'u',
|
||||
appPassword: '\n\n\np',
|
||||
}),
|
||||
);
|
||||
expect(output).toEqual({
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
token: 't',
|
||||
username: 'u',
|
||||
appPassword: 'p',
|
||||
});
|
||||
});
|
||||
|
||||
it('inserts the defaults if missing', () => {
|
||||
const output = readBitbucketIntegrationConfig(buildConfig({}));
|
||||
expect(output).toEqual(
|
||||
expect.objectContaining({
|
||||
host: 'bitbucket.org',
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects funky configs', () => {
|
||||
const valid: any = {
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
token: 't',
|
||||
username: 'u',
|
||||
appPassword: 'p',
|
||||
};
|
||||
expect(() =>
|
||||
readBitbucketIntegrationConfig(buildConfig({ ...valid, host: 7 })),
|
||||
).toThrow(/host/);
|
||||
expect(() =>
|
||||
readBitbucketIntegrationConfig(buildConfig({ ...valid, apiBaseUrl: 7 })),
|
||||
).toThrow(/apiBaseUrl/);
|
||||
expect(() =>
|
||||
readBitbucketIntegrationConfig(buildConfig({ ...valid, token: 7 })),
|
||||
).toThrow(/token/);
|
||||
expect(() =>
|
||||
readBitbucketIntegrationConfig(buildConfig({ ...valid, username: 7 })),
|
||||
).toThrow(/username/);
|
||||
expect(() =>
|
||||
readBitbucketIntegrationConfig(buildConfig({ ...valid, appPassword: 7 })),
|
||||
).toThrow(/appPassword/);
|
||||
});
|
||||
|
||||
it('works on the frontend', async () => {
|
||||
expect(
|
||||
readBitbucketIntegrationConfig(
|
||||
await buildFrontendConfig({
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
token: 't',
|
||||
username: 'u',
|
||||
appPassword: 'p',
|
||||
}),
|
||||
),
|
||||
).toEqual({
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('readBitbucketIntegrationConfigs', () => {
|
||||
function buildConfig(data: Partial<BitbucketIntegrationConfig>[]): Config[] {
|
||||
return data.map(item => new ConfigReader(item));
|
||||
}
|
||||
|
||||
it('reads all values', () => {
|
||||
const output = readBitbucketIntegrationConfigs(
|
||||
buildConfig([
|
||||
{
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
token: 't',
|
||||
username: 'u',
|
||||
appPassword: 'p',
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(output).toContainEqual({
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
token: 't',
|
||||
username: 'u',
|
||||
appPassword: 'p',
|
||||
});
|
||||
});
|
||||
|
||||
it('adds a default Bitbucket Cloud entry when missing', () => {
|
||||
const output = readBitbucketIntegrationConfigs(buildConfig([]));
|
||||
expect(output).toEqual([
|
||||
{
|
||||
host: 'bitbucket.org',
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('injects the correct Bitbucket Cloud API base URL when missing', () => {
|
||||
const output = readBitbucketIntegrationConfigs(
|
||||
buildConfig([{ host: 'bitbucket.org' }]),
|
||||
);
|
||||
expect(output).toEqual([
|
||||
{
|
||||
host: 'bitbucket.org',
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,136 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { trimEnd } from 'lodash';
|
||||
import { isValidHost } from '../helpers';
|
||||
|
||||
const BITBUCKET_HOST = 'bitbucket.org';
|
||||
const BITBUCKET_API_BASE_URL = 'https://api.bitbucket.org/2.0';
|
||||
|
||||
/**
|
||||
* The configuration parameters for a single Bitbucket API provider.
|
||||
*
|
||||
* @public
|
||||
* @deprecated bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer.
|
||||
*/
|
||||
export type BitbucketIntegrationConfig = {
|
||||
/**
|
||||
* The host of the target that this matches on, e.g. "bitbucket.org"
|
||||
*/
|
||||
host: string;
|
||||
|
||||
/**
|
||||
* The base URL of the API of this provider, e.g. "https://api.bitbucket.org/2.0",
|
||||
* with no trailing slash.
|
||||
*
|
||||
* Values omitted at the optional property at the app-config will be deduced
|
||||
* from the "host" value.
|
||||
*/
|
||||
apiBaseUrl: string;
|
||||
|
||||
/**
|
||||
* The authorization token to use for requests to a Bitbucket Server provider.
|
||||
*
|
||||
* See https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html
|
||||
*
|
||||
* If no token is specified, anonymous access is used.
|
||||
*/
|
||||
token?: string;
|
||||
|
||||
/**
|
||||
* The username to use for requests to Bitbucket Cloud (bitbucket.org).
|
||||
*/
|
||||
username?: string;
|
||||
|
||||
/**
|
||||
* Authentication with Bitbucket Cloud (bitbucket.org) is done using app passwords.
|
||||
*
|
||||
* See https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/
|
||||
*/
|
||||
appPassword?: string;
|
||||
|
||||
/**
|
||||
* Signing key for commits
|
||||
*/
|
||||
commitSigningKey?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads a single Bitbucket integration config.
|
||||
*
|
||||
* @param config - The config object of a single integration
|
||||
* @public
|
||||
* @deprecated bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer.
|
||||
*/
|
||||
export function readBitbucketIntegrationConfig(
|
||||
config: Config,
|
||||
): BitbucketIntegrationConfig {
|
||||
const host = config.getOptionalString('host') ?? BITBUCKET_HOST;
|
||||
let apiBaseUrl = config.getOptionalString('apiBaseUrl');
|
||||
const token = config.getOptionalString('token')?.trim();
|
||||
const username = config.getOptionalString('username');
|
||||
const appPassword = config.getOptionalString('appPassword')?.trim();
|
||||
|
||||
if (!isValidHost(host)) {
|
||||
throw new Error(
|
||||
`Invalid Bitbucket integration config, '${host}' is not a valid host`,
|
||||
);
|
||||
}
|
||||
|
||||
if (apiBaseUrl) {
|
||||
apiBaseUrl = trimEnd(apiBaseUrl, '/');
|
||||
} else if (host === BITBUCKET_HOST) {
|
||||
apiBaseUrl = BITBUCKET_API_BASE_URL;
|
||||
} else {
|
||||
apiBaseUrl = `https://${host}/rest/api/1.0`;
|
||||
}
|
||||
|
||||
return {
|
||||
host,
|
||||
apiBaseUrl,
|
||||
token,
|
||||
username,
|
||||
appPassword,
|
||||
commitSigningKey: config.getOptionalString('commitSigningKey'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a set of Bitbucket integration configs, and inserts some defaults for
|
||||
* public Bitbucket if not specified.
|
||||
*
|
||||
* @param configs - All of the integration config objects
|
||||
* @public
|
||||
* @deprecated bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer.
|
||||
*/
|
||||
export function readBitbucketIntegrationConfigs(
|
||||
configs: Config[],
|
||||
): BitbucketIntegrationConfig[] {
|
||||
// First read all the explicit integrations
|
||||
const result = configs.map(readBitbucketIntegrationConfig);
|
||||
|
||||
// If no explicit bitbucket.org integration was added, put one in the list as
|
||||
// a convenience
|
||||
if (!result.some(c => c.host === BITBUCKET_HOST)) {
|
||||
result.push({
|
||||
host: BITBUCKET_HOST,
|
||||
apiBaseUrl: BITBUCKET_API_BASE_URL,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,304 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { registerMswTestHooks } from '../helpers';
|
||||
import { BitbucketIntegrationConfig } from './config';
|
||||
import {
|
||||
getBitbucketDefaultBranch,
|
||||
getBitbucketDownloadUrl,
|
||||
getBitbucketFileFetchUrl,
|
||||
getBitbucketRequestOptions,
|
||||
} from './core';
|
||||
|
||||
describe('bitbucket core', () => {
|
||||
const worker = setupServer();
|
||||
registerMswTestHooks(worker);
|
||||
|
||||
describe('getBitbucketRequestOptions', () => {
|
||||
it('inserts a token when needed', () => {
|
||||
const withToken: BitbucketIntegrationConfig = {
|
||||
host: '',
|
||||
apiBaseUrl: '',
|
||||
token: 'A',
|
||||
};
|
||||
const withoutToken: BitbucketIntegrationConfig = {
|
||||
host: '',
|
||||
apiBaseUrl: '',
|
||||
};
|
||||
expect(
|
||||
(getBitbucketRequestOptions(withToken).headers as any).Authorization,
|
||||
).toEqual('Bearer A');
|
||||
expect(
|
||||
(getBitbucketRequestOptions(withoutToken).headers as any).Authorization,
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('insert basic auth when needed', () => {
|
||||
const withUsernameAndPassword: BitbucketIntegrationConfig = {
|
||||
host: '',
|
||||
apiBaseUrl: '',
|
||||
username: 'some-user',
|
||||
appPassword: 'my-secret',
|
||||
};
|
||||
const withoutUsernameAndPassword: BitbucketIntegrationConfig = {
|
||||
host: '',
|
||||
apiBaseUrl: '',
|
||||
};
|
||||
expect(
|
||||
(getBitbucketRequestOptions(withUsernameAndPassword).headers as any)
|
||||
.Authorization,
|
||||
).toEqual('Basic c29tZS11c2VyOm15LXNlY3JldA==');
|
||||
expect(
|
||||
(getBitbucketRequestOptions(withoutUsernameAndPassword).headers as any)
|
||||
.Authorization,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBitbucketFileFetchUrl', () => {
|
||||
it('rejects targets that do not look like URLs', () => {
|
||||
const config: BitbucketIntegrationConfig = { host: '', apiBaseUrl: '' };
|
||||
expect(() => getBitbucketFileFetchUrl('a/b', config)).toThrow(
|
||||
/Incorrect URL: a\/b/,
|
||||
);
|
||||
});
|
||||
|
||||
it('happy path for Bitbucket Cloud', () => {
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.org',
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
};
|
||||
expect(
|
||||
getBitbucketFileFetchUrl(
|
||||
'https://bitbucket.org/org-name/repo-name/src/master/templates/my-template.yaml',
|
||||
config,
|
||||
),
|
||||
).toEqual(
|
||||
'https://api.bitbucket.org/2.0/repositories/org-name/repo-name/src/master/templates/my-template.yaml',
|
||||
);
|
||||
});
|
||||
|
||||
it('happy path for Bitbucket Server', () => {
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://bitbucket.mycompany.net/rest/api/1.0',
|
||||
};
|
||||
expect(
|
||||
getBitbucketFileFetchUrl(
|
||||
'https://bitbucket.mycompany.net/projects/a/repos/b/browse/path/to/c.yaml',
|
||||
config,
|
||||
),
|
||||
).toEqual(
|
||||
'https://bitbucket.mycompany.net/rest/api/1.0/projects/a/repos/b/raw/path/to/c.yaml?at=',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBitbucketDownloadUrl', () => {
|
||||
it('add path param if a path is specified for Bitbucket Server', async () => {
|
||||
const defaultBranchResponse = {
|
||||
displayId: 'main',
|
||||
};
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json(defaultBranchResponse),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
|
||||
};
|
||||
const result = await getBitbucketDownloadUrl(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs',
|
||||
config,
|
||||
);
|
||||
expect(result).toEqual(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=tgz&at=main&prefix=backstage-mock&path=docs',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not double encode the filepath', async () => {
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
|
||||
};
|
||||
const result = await getBitbucketDownloadUrl(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/%2Fdocs?at=some-branch',
|
||||
config,
|
||||
);
|
||||
expect(result).toEqual(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=tgz&at=some-branch&prefix=backstage-mock&path=%2Fdocs',
|
||||
);
|
||||
});
|
||||
|
||||
it('do not add path param if no path is specified for Bitbucket Server', async () => {
|
||||
const defaultBranchResponse = {
|
||||
displayId: 'main',
|
||||
};
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json(defaultBranchResponse),
|
||||
),
|
||||
),
|
||||
);
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
|
||||
};
|
||||
const result = await getBitbucketDownloadUrl(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse',
|
||||
config,
|
||||
);
|
||||
|
||||
expect(result).toEqual(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=tgz&at=main&prefix=backstage-mock',
|
||||
);
|
||||
});
|
||||
|
||||
it('get by branch for Bitbucket Server', async () => {
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
|
||||
};
|
||||
const result = await getBitbucketDownloadUrl(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs?at=some-branch',
|
||||
config,
|
||||
);
|
||||
expect(result).toEqual(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=tgz&at=some-branch&prefix=backstage-mock&path=docs',
|
||||
);
|
||||
});
|
||||
|
||||
it('do not add path param for Bitbucket Cloud', async () => {
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.org',
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
};
|
||||
const result = await getBitbucketDownloadUrl(
|
||||
'https://bitbucket.org/backstage/mock/src/master',
|
||||
config,
|
||||
);
|
||||
expect(result).toEqual(
|
||||
'https://bitbucket.org/backstage/mock/get/master.tar.gz',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBitbucketDefaultBranch', () => {
|
||||
it('return default branch for Bitbucket Cloud', async () => {
|
||||
const repoInfoResponse = {
|
||||
mainbranch: {
|
||||
name: 'main',
|
||||
},
|
||||
};
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.org/2.0/repositories/backstage/mock',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json(repoInfoResponse),
|
||||
),
|
||||
),
|
||||
);
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.org',
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
};
|
||||
const defaultBranch = await getBitbucketDefaultBranch(
|
||||
'https://bitbucket.org/backstage/mock/src/main',
|
||||
config,
|
||||
);
|
||||
expect(defaultBranch).toEqual('main');
|
||||
});
|
||||
|
||||
it('return default branch for Bitbucket Server', async () => {
|
||||
const defaultBranchResponse = {
|
||||
displayId: 'main',
|
||||
};
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json(defaultBranchResponse),
|
||||
),
|
||||
),
|
||||
);
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
|
||||
};
|
||||
const defaultBranch = await getBitbucketDefaultBranch(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/README.md',
|
||||
config,
|
||||
);
|
||||
expect(defaultBranch).toEqual('main');
|
||||
});
|
||||
|
||||
it('return default branch for Bitbucket Server for bitbucket version 5.11', async () => {
|
||||
const defaultBranchResponse = {
|
||||
displayId: 'main',
|
||||
};
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(404),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json(defaultBranchResponse),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/branches/default',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json(defaultBranchResponse),
|
||||
),
|
||||
),
|
||||
);
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
|
||||
};
|
||||
const defaultBranch = await getBitbucketDefaultBranch(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/README.md',
|
||||
config,
|
||||
);
|
||||
expect(defaultBranch).toEqual('main');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,183 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fetch from 'cross-fetch';
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import { BitbucketIntegrationConfig } from './config';
|
||||
|
||||
/**
|
||||
* Given a URL pointing to a path on a provider, returns the default branch.
|
||||
*
|
||||
* @param url - A URL pointing to a path
|
||||
* @param config - The relevant provider config
|
||||
* @public
|
||||
* @deprecated no longer in use, bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer.
|
||||
*/
|
||||
export async function getBitbucketDefaultBranch(
|
||||
url: string,
|
||||
config: BitbucketIntegrationConfig,
|
||||
): Promise<string> {
|
||||
const { name: repoName, owner: project, resource } = parseGitUrl(url);
|
||||
|
||||
const isHosted = resource === 'bitbucket.org';
|
||||
// Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp184
|
||||
let branchUrl = isHosted
|
||||
? `${config.apiBaseUrl}/repositories/${project}/${repoName}`
|
||||
: `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/default-branch`;
|
||||
|
||||
let response = await fetch(branchUrl, getBitbucketRequestOptions(config));
|
||||
|
||||
if (response.status === 404 && !isHosted) {
|
||||
// First try the new format, and then if it gets specifically a 404 it should try the old format
|
||||
// (to support old Atlassian Bitbucket v5.11.1 format )
|
||||
branchUrl = `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/branches/default`;
|
||||
response = await fetch(branchUrl, getBitbucketRequestOptions(config));
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const message = `Failed to retrieve default branch from ${branchUrl}, ${response.status} ${response.statusText}`;
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
let defaultBranch;
|
||||
if (isHosted) {
|
||||
const repoInfo = await response.json();
|
||||
defaultBranch = repoInfo.mainbranch.name;
|
||||
} else {
|
||||
const { displayId } = await response.json();
|
||||
defaultBranch = displayId;
|
||||
}
|
||||
if (!defaultBranch) {
|
||||
throw new Error(
|
||||
`Failed to read default branch from ${branchUrl}. ` +
|
||||
`Response ${response.status} ${response.json()}`,
|
||||
);
|
||||
}
|
||||
return defaultBranch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a URL pointing to a path on a provider, returns a URL that is suitable
|
||||
* for downloading the subtree.
|
||||
*
|
||||
* @param url - A URL pointing to a path
|
||||
* @param config - The relevant provider config
|
||||
* @public
|
||||
* @deprecated no longer in use, bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer.
|
||||
*/
|
||||
export async function getBitbucketDownloadUrl(
|
||||
url: string,
|
||||
config: BitbucketIntegrationConfig,
|
||||
): Promise<string> {
|
||||
const {
|
||||
name: repoName,
|
||||
owner: project,
|
||||
ref,
|
||||
protocol,
|
||||
resource,
|
||||
filepath,
|
||||
} = parseGitUrl(url);
|
||||
|
||||
const isHosted = resource === 'bitbucket.org';
|
||||
|
||||
let branch = ref;
|
||||
if (!branch) {
|
||||
branch = await getBitbucketDefaultBranch(url, config);
|
||||
}
|
||||
// path will limit the downloaded content
|
||||
// /docs will only download the docs folder and everything below it
|
||||
// /docs/index.md will download the docs folder and everything below it
|
||||
const path = filepath
|
||||
? `&path=${encodeURIComponent(decodeURIComponent(filepath))}`
|
||||
: '';
|
||||
const archiveUrl = isHosted
|
||||
? `${protocol}://${resource}/${project}/${repoName}/get/${branch}.tar.gz`
|
||||
: `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/archive?format=tgz&at=${branch}&prefix=${project}-${repoName}${path}`;
|
||||
|
||||
return archiveUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a URL pointing to a file on a provider, returns a URL that is suitable
|
||||
* for fetching the contents of the data.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Converts
|
||||
* from: https://bitbucket.org/orgname/reponame/src/master/file.yaml
|
||||
* to: https://api.bitbucket.org/2.0/repositories/orgname/reponame/src/master/file.yaml
|
||||
*
|
||||
* @param url - A URL pointing to a file
|
||||
* @param config - The relevant provider config
|
||||
* @public
|
||||
* @deprecated no longer in use, bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer.
|
||||
*/
|
||||
export function getBitbucketFileFetchUrl(
|
||||
url: string,
|
||||
config: BitbucketIntegrationConfig,
|
||||
): string {
|
||||
try {
|
||||
const { owner, name, ref, filepathtype, filepath } = parseGitUrl(url);
|
||||
if (
|
||||
!owner ||
|
||||
!name ||
|
||||
(filepathtype !== 'browse' &&
|
||||
filepathtype !== 'raw' &&
|
||||
filepathtype !== 'src')
|
||||
) {
|
||||
throw new Error('Invalid Bitbucket URL or file path');
|
||||
}
|
||||
|
||||
const pathWithoutSlash = filepath.replace(/^\//, '');
|
||||
|
||||
if (config.host === 'bitbucket.org') {
|
||||
if (!ref) {
|
||||
throw new Error('Invalid Bitbucket URL or file path');
|
||||
}
|
||||
return `${config.apiBaseUrl}/repositories/${owner}/${name}/src/${ref}/${pathWithoutSlash}`;
|
||||
}
|
||||
return `${config.apiBaseUrl}/projects/${owner}/repos/${name}/raw/${pathWithoutSlash}?at=${ref}`;
|
||||
} catch (e) {
|
||||
throw new Error(`Incorrect URL: ${url}, ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request options necessary to make requests to a given provider.
|
||||
*
|
||||
* @param config - The relevant provider config
|
||||
* @public
|
||||
* @deprecated no longer in use, bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer.
|
||||
*/
|
||||
export function getBitbucketRequestOptions(
|
||||
config: BitbucketIntegrationConfig,
|
||||
): { headers: Record<string, string> } {
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
if (config.token) {
|
||||
headers.Authorization = `Bearer ${config.token}`;
|
||||
} else if (config.username && config.appPassword) {
|
||||
const buffer = Buffer.from(
|
||||
`${config.username}:${config.appPassword}`,
|
||||
'utf8',
|
||||
);
|
||||
headers.Authorization = `Basic ${buffer.toString('base64')}`;
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
};
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { BitbucketIntegration } from './BitbucketIntegration';
|
||||
export {
|
||||
readBitbucketIntegrationConfig,
|
||||
readBitbucketIntegrationConfigs,
|
||||
} from './config';
|
||||
export type { BitbucketIntegrationConfig } from './config';
|
||||
export {
|
||||
getBitbucketDefaultBranch,
|
||||
getBitbucketDownloadUrl,
|
||||
getBitbucketFileFetchUrl,
|
||||
getBitbucketRequestOptions,
|
||||
} from './core';
|
||||
@@ -20,7 +20,6 @@ import fetch from 'cross-fetch';
|
||||
import { registerMswTestHooks } from '../helpers';
|
||||
import { GerritIntegrationConfig } from './config';
|
||||
import {
|
||||
buildGerritGitilesArchiveUrl,
|
||||
buildGerritGitilesArchiveUrlFromLocation,
|
||||
buildGerritGitilesUrl,
|
||||
getGerritBranchApiUrl,
|
||||
@@ -28,7 +27,6 @@ import {
|
||||
getGerritRequestOptions,
|
||||
parseGerritJsonResponse,
|
||||
parseGitilesUrlRef,
|
||||
parseGerritGitilesUrl,
|
||||
getGerritFileContentsApiUrl,
|
||||
} from './core';
|
||||
|
||||
@@ -36,86 +34,6 @@ describe('gerrit core', () => {
|
||||
const worker = setupServer();
|
||||
registerMswTestHooks(worker);
|
||||
|
||||
describe('buildGerritGitilesArchiveUrl', () => {
|
||||
const config: GerritIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
baseUrl: 'https://gerrit.com',
|
||||
gitilesBaseUrl: 'https://gerrit.com/gitiles',
|
||||
};
|
||||
const configWithPath: GerritIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
baseUrl: 'https://gerrit.com/gerrit',
|
||||
gitilesBaseUrl: 'https://gerrit.com/gerrit/plugins/gitiles',
|
||||
};
|
||||
const configWithDedicatedGitiles: GerritIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
baseUrl: 'https://gerrit.com/gerrit',
|
||||
gitilesBaseUrl: 'https://dedicated-gitiles-server.com/gerrit/gitiles',
|
||||
};
|
||||
it('can create an archive url for a branch', () => {
|
||||
expect(buildGerritGitilesArchiveUrl(config, 'repo', 'dev', '')).toEqual(
|
||||
'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev.tar.gz',
|
||||
);
|
||||
|
||||
expect(buildGerritGitilesArchiveUrl(config, 'repo', 'dev', '/')).toEqual(
|
||||
'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev.tar.gz',
|
||||
);
|
||||
});
|
||||
it('can create an archive url for a specific directory', () => {
|
||||
expect(
|
||||
buildGerritGitilesArchiveUrl(config, 'repo', 'dev', 'docs'),
|
||||
).toEqual(
|
||||
'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz',
|
||||
);
|
||||
});
|
||||
it('can create an authenticated url when auth is enabled', () => {
|
||||
const authConfig = {
|
||||
...config,
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
};
|
||||
expect(
|
||||
buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'),
|
||||
).toEqual(
|
||||
'https://gerrit.com/a/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz',
|
||||
);
|
||||
});
|
||||
it('can create an authenticated url when auth is enabled and an url-path is used', () => {
|
||||
const authConfig = {
|
||||
...configWithPath,
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
};
|
||||
expect(
|
||||
buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'),
|
||||
).toEqual(
|
||||
'https://gerrit.com/gerrit/a/plugins/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz',
|
||||
);
|
||||
});
|
||||
it('Cannot build an authenticated url when a dedicated Gitiles server is used', () => {
|
||||
const authConfig = {
|
||||
...configWithDedicatedGitiles,
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
};
|
||||
expect(() =>
|
||||
buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'),
|
||||
).toThrow(
|
||||
'Since the baseUrl (Gerrit) is not part of the gitilesBaseUrl, an authentication URL could not be constructed.',
|
||||
);
|
||||
});
|
||||
it('Build a non-authenticated url when a dedicated Gitiles server is used', () => {
|
||||
const authConfig = {
|
||||
...configWithDedicatedGitiles,
|
||||
};
|
||||
expect(
|
||||
buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'),
|
||||
).toEqual(
|
||||
'https://dedicated-gitiles-server.com/gerrit/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildGerritGitilesArchiveUrlFromLocation', () => {
|
||||
const config: GerritIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
@@ -238,6 +156,7 @@ describe('gerrit core', () => {
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseGitilesUrlRef', () => {
|
||||
const config: GerritIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
@@ -360,64 +279,6 @@ describe('gerrit core', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('parseGerritGitilesUrl', () => {
|
||||
it('can parse a valid gitiles urls.', () => {
|
||||
const config: GerritIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
gitilesBaseUrl: 'https://gerrit.com/gitiles',
|
||||
};
|
||||
const { branch, filePath, project } = parseGerritGitilesUrl(
|
||||
config,
|
||||
'https://gerrit.com/gitiles/web/project/+/refs/heads/master/README.md',
|
||||
);
|
||||
expect(project).toEqual('web/project');
|
||||
expect(branch).toEqual('master');
|
||||
expect(filePath).toEqual('README.md');
|
||||
|
||||
const { filePath: rootPath } = parseGerritGitilesUrl(
|
||||
config,
|
||||
'https://gerrit.com/gitiles/web/project/+/refs/heads/master',
|
||||
);
|
||||
expect(rootPath).toEqual('/');
|
||||
});
|
||||
it('can parse a valid authenticated gitiles url.', () => {
|
||||
const config: GerritIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
gitilesBaseUrl: 'https://gerrit.com/gitiles',
|
||||
};
|
||||
const { branch, filePath, project } = parseGerritGitilesUrl(
|
||||
config,
|
||||
'https://gerrit.com/a/gitiles/web/project/+/refs/heads/master/README.md',
|
||||
);
|
||||
expect(project).toEqual('web/project');
|
||||
expect(branch).toEqual('master');
|
||||
expect(filePath).toEqual('README.md');
|
||||
|
||||
const { filePath: rootPath } = parseGerritGitilesUrl(
|
||||
config,
|
||||
'https://gerrit.com/gitiles/web/project/+/refs/heads/master',
|
||||
);
|
||||
expect(rootPath).toEqual('/');
|
||||
});
|
||||
it('throws on incorrect gitiles urls.', () => {
|
||||
const config: GerritIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
gitilesBaseUrl: 'https://gerrit.com',
|
||||
};
|
||||
expect(() =>
|
||||
parseGerritGitilesUrl(
|
||||
config,
|
||||
'https://gerrit.com/+/refs/heads/master/README.md',
|
||||
),
|
||||
).toThrow(/project/);
|
||||
expect(() =>
|
||||
parseGerritGitilesUrl(
|
||||
config,
|
||||
'https://gerrit.com/web/project/+/refs/changes/1/11/master/README.md',
|
||||
),
|
||||
).toThrow(/branch/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGerritBranchApiUrl', () => {
|
||||
it('can create an url for anonymous access.', () => {
|
||||
|
||||
@@ -18,70 +18,6 @@ import { GerritIntegrationConfig } from './config';
|
||||
|
||||
const GERRIT_BODY_PREFIX = ")]}'";
|
||||
|
||||
/**
|
||||
* Parse a Gitiles URL and return branch, file path and project.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Gerrit only handles code reviews so it does not have a native way to browse
|
||||
* or showing the content of gits. Image if Github only had the "pull requests"
|
||||
* tab.
|
||||
*
|
||||
* Any source code browsing is instead handled by optional services outside
|
||||
* Gerrit. The url format chosen for the Gerrit url reader is the one used by
|
||||
* the Gitiles project. Gerrit will work perfectly with Backstage without
|
||||
* having Gitiles installed but there are some places in the Backstage GUI
|
||||
* with links to the url used by the url reader. These will not work unless
|
||||
* the urls point to an actual Gitiles installation.
|
||||
*
|
||||
* Gitiles url:
|
||||
* https://g.com/optional_path/\{project\}/+/refs/heads/\{branch\}/\{filePath\}
|
||||
* https://g.com/a/optional_path/\{project\}/+/refs/heads/\{branch\}/\{filePath\}
|
||||
*
|
||||
*
|
||||
* @param url - An URL pointing to a file stored in git.
|
||||
* @public
|
||||
* @deprecated `parseGerritGitilesUrl` is deprecated. Use
|
||||
* {@link parseGitilesUrlRef} instead.
|
||||
*/
|
||||
export function parseGerritGitilesUrl(
|
||||
config: GerritIntegrationConfig,
|
||||
url: string,
|
||||
): { branch: string; filePath: string; project: string } {
|
||||
const baseUrlParse = new URL(config.gitilesBaseUrl!);
|
||||
const urlParse = new URL(url);
|
||||
|
||||
// Remove the gerrit authentication prefix '/a/' from the url
|
||||
// In case of the gitilesBaseUrl is https://review.gerrit.com/plugins/gitiles
|
||||
// and the url provided is https://review.gerrit.com/a/plugins/gitiles/...
|
||||
// remove the prefix only if the pathname start with '/a/'
|
||||
const urlPath = urlParse.pathname
|
||||
.substring(urlParse.pathname.startsWith('/a/') ? 2 : 0)
|
||||
.replace(baseUrlParse.pathname, '');
|
||||
|
||||
const parts = urlPath.split('/').filter(p => !!p);
|
||||
|
||||
const projectEndIndex = parts.indexOf('+');
|
||||
|
||||
if (projectEndIndex <= 0) {
|
||||
throw new Error(`Unable to parse project from url: ${url}`);
|
||||
}
|
||||
const project = trimStart(parts.slice(0, projectEndIndex).join('/'), '/');
|
||||
|
||||
const branchIndex = parts.indexOf('heads');
|
||||
if (branchIndex <= 0) {
|
||||
throw new Error(`Unable to parse branch from url: ${url}`);
|
||||
}
|
||||
const branch = parts[branchIndex + 1];
|
||||
const filePath = parts.slice(branchIndex + 2).join('/');
|
||||
|
||||
return {
|
||||
branch,
|
||||
filePath: filePath === '' ? '/' : filePath,
|
||||
project,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses Gitiles urls and returns the following:
|
||||
*
|
||||
@@ -231,30 +167,6 @@ export function buildGerritEditUrl(
|
||||
)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a Gerrit Gitiles archive url that targets a specific branch and path
|
||||
*
|
||||
* @param config - A Gerrit provider config.
|
||||
* @param project - The name of the git project
|
||||
* @param branch - The branch we will target.
|
||||
* @param filePath - The absolute file path.
|
||||
* @public
|
||||
* @deprecated `buildGerritGitilesArchiveUrl` is deprecated. Use
|
||||
* {@link buildGerritGitilesArchiveUrlFromLocation} instead.
|
||||
*/
|
||||
export function buildGerritGitilesArchiveUrl(
|
||||
config: GerritIntegrationConfig,
|
||||
project: string,
|
||||
branch: string,
|
||||
filePath: string,
|
||||
): string {
|
||||
const archiveName =
|
||||
filePath === '/' || filePath === '' ? '.tar.gz' : `/${filePath}.tar.gz`;
|
||||
return `${getGitilesAuthenticationUrl(
|
||||
config,
|
||||
)}/${project}/+archive/refs/heads/${branch}${archiveName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a Gerrit Gitiles archive url from a Gitiles url.
|
||||
*
|
||||
@@ -350,11 +262,15 @@ export function getGerritBranchApiUrl(
|
||||
config: GerritIntegrationConfig,
|
||||
url: string,
|
||||
) {
|
||||
const { branch, project } = parseGerritGitilesUrl(config, url);
|
||||
const { ref, refType, project } = parseGitilesUrlRef(config, url);
|
||||
|
||||
if (refType !== 'branch') {
|
||||
throw new Error(`Unsupported gitiles ref type: ${refType}`);
|
||||
}
|
||||
|
||||
return `${config.baseUrl}${getAuthenticationPrefix(
|
||||
config,
|
||||
)}projects/${encodeURIComponent(project)}/branches/${branch}`;
|
||||
)}projects/${encodeURIComponent(project)}/branches/${ref}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -367,7 +283,7 @@ export function getGerritCloneRepoUrl(
|
||||
config: GerritIntegrationConfig,
|
||||
url: string,
|
||||
) {
|
||||
const { project } = parseGerritGitilesUrl(config, url);
|
||||
const { project } = parseGitilesUrlRef(config, url);
|
||||
|
||||
return `${config.cloneUrl}${getAuthenticationPrefix(config)}${project}`;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ export {
|
||||
readGerritIntegrationConfigs,
|
||||
} from './config';
|
||||
export {
|
||||
buildGerritGitilesArchiveUrl,
|
||||
buildGerritGitilesArchiveUrlFromLocation,
|
||||
getGitilesAuthenticationUrl,
|
||||
getGerritBranchApiUrl,
|
||||
@@ -28,7 +27,6 @@ export {
|
||||
getGerritProjectsApiUrl,
|
||||
getGerritRequestOptions,
|
||||
parseGerritJsonResponse,
|
||||
parseGerritGitilesUrl,
|
||||
parseGitilesUrlRef,
|
||||
} from './core';
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { GithubIntegrationConfig } from './config';
|
||||
import { getGithubFileFetchUrl, getGitHubRequestOptions } from './core';
|
||||
import { getGithubFileFetchUrl } from './core';
|
||||
import { GithubCredentials } from './types';
|
||||
|
||||
describe('github core', () => {
|
||||
@@ -35,28 +35,6 @@ describe('github core', () => {
|
||||
type: 'token',
|
||||
};
|
||||
|
||||
describe('getGitHubRequestOptions', () => {
|
||||
it('inserts a token when needed', () => {
|
||||
const withToken: GithubIntegrationConfig = {
|
||||
host: '',
|
||||
rawBaseUrl: '',
|
||||
token: 'A',
|
||||
};
|
||||
const withoutToken: GithubIntegrationConfig = {
|
||||
host: '',
|
||||
rawBaseUrl: '',
|
||||
};
|
||||
expect(
|
||||
(getGitHubRequestOptions(withToken, appCredentials).headers as any)
|
||||
.Authorization,
|
||||
).toEqual('token A');
|
||||
expect(
|
||||
(getGitHubRequestOptions(withoutToken, noCredentials).headers as any)
|
||||
.Authorization,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGithubFileFetchUrl', () => {
|
||||
it('rejects targets that do not look like URLs', () => {
|
||||
const config: GithubIntegrationConfig = { host: '', apiBaseUrl: '' };
|
||||
|
||||
@@ -63,30 +63,6 @@ export function getGithubFileFetchUrl(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request options necessary to make requests to a given provider.
|
||||
*
|
||||
* @deprecated This function is no longer used internally
|
||||
* @param config - The relevant provider config
|
||||
* @public
|
||||
*/
|
||||
export function getGitHubRequestOptions(
|
||||
config: GithubIntegrationConfig,
|
||||
credentials: GithubCredentials,
|
||||
): { headers: Record<string, string> } {
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
if (chooseEndpoint(config, credentials) === 'api') {
|
||||
headers.Accept = 'application/vnd.github.v3.raw';
|
||||
}
|
||||
|
||||
if (credentials.token) {
|
||||
headers.Authorization = `token ${credentials.token}`;
|
||||
}
|
||||
|
||||
return { headers };
|
||||
}
|
||||
|
||||
export function chooseEndpoint(
|
||||
config: GithubIntegrationConfig,
|
||||
credentials: GithubCredentials,
|
||||
|
||||
@@ -19,7 +19,7 @@ export {
|
||||
readGithubIntegrationConfigs,
|
||||
} from './config';
|
||||
export type { GithubAppConfig, GithubIntegrationConfig } from './config';
|
||||
export { getGithubFileFetchUrl, getGitHubRequestOptions } from './core';
|
||||
export { getGithubFileFetchUrl } from './core';
|
||||
export { DefaultGithubCredentialsProvider } from './DefaultGithubCredentialsProvider';
|
||||
export {
|
||||
GithubAppCredentialsMux,
|
||||
|
||||
@@ -24,7 +24,6 @@ export * from './awsS3';
|
||||
export * from './awsCodeCommit';
|
||||
export * from './azureBlobStorage';
|
||||
export * from './azure';
|
||||
export * from './bitbucket';
|
||||
export * from './bitbucketCloud';
|
||||
export * from './bitbucketServer';
|
||||
export * from './gerrit';
|
||||
|
||||
@@ -19,7 +19,6 @@ import { AwsS3Integration } from './awsS3/AwsS3Integration';
|
||||
import { AwsCodeCommitIntegration } from './awsCodeCommit';
|
||||
import { AzureIntegration } from './azure/AzureIntegration';
|
||||
import { BitbucketCloudIntegration } from './bitbucketCloud/BitbucketCloudIntegration';
|
||||
import { BitbucketIntegration } from './bitbucket/BitbucketIntegration';
|
||||
import { BitbucketServerIntegration } from './bitbucketServer/BitbucketServerIntegration';
|
||||
import { GerritIntegration } from './gerrit/GerritIntegration';
|
||||
import { GithubIntegration } from './github/GithubIntegration';
|
||||
@@ -39,10 +38,6 @@ export interface ScmIntegrationRegistry
|
||||
awsCodeCommit: ScmIntegrationsGroup<AwsCodeCommitIntegration>;
|
||||
azureBlobStorage: ScmIntegrationsGroup<AzureBlobStorageIntergation>;
|
||||
azure: ScmIntegrationsGroup<AzureIntegration>;
|
||||
/**
|
||||
* @deprecated in favor of `bitbucketCloud` and `bitbucketServer`
|
||||
*/
|
||||
bitbucket: ScmIntegrationsGroup<BitbucketIntegration>;
|
||||
bitbucketCloud: ScmIntegrationsGroup<BitbucketCloudIntegration>;
|
||||
bitbucketServer: ScmIntegrationsGroup<BitbucketServerIntegration>;
|
||||
gerrit: ScmIntegrationsGroup<GerritIntegration>;
|
||||
|
||||
Reference in New Issue
Block a user