Merge pull request #14056 from atoko/atoko/gitea/scmintegration
Gitea SCM integration / URL Reader
This commit is contained in:
@@ -21,6 +21,7 @@ import { Duration } from 'luxon';
|
||||
import { ErrorRequestHandler } from 'express';
|
||||
import express from 'express';
|
||||
import { GerritIntegration } from '@backstage/integration';
|
||||
import { GiteaIntegration } from '@backstage/integration';
|
||||
import { GithubCredentialsProvider } from '@backstage/integration';
|
||||
import { GithubIntegration } from '@backstage/integration';
|
||||
import { GitLabIntegration } from '@backstage/integration';
|
||||
@@ -406,6 +407,23 @@ export class Git {
|
||||
resolveRef(options: { dir: string; ref: string }): Promise<string>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class GiteaUrlReader implements UrlReader {
|
||||
constructor(integration: GiteaIntegration);
|
||||
// (undocumented)
|
||||
static factory: ReaderFactory;
|
||||
// (undocumented)
|
||||
read(url: string): Promise<Buffer>;
|
||||
// (undocumented)
|
||||
readTree(): Promise<ReadTreeResponse>;
|
||||
// (undocumented)
|
||||
readUrl(url: string, options?: ReadUrlOptions): Promise<ReadUrlResponse>;
|
||||
// (undocumented)
|
||||
search(): Promise<SearchResponse>;
|
||||
// (undocumented)
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class GithubUrlReader implements UrlReader {
|
||||
constructor(
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Copyright 2022 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 { setupRequestMockHandlers } from '@backstage/backend-test-utils';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { GiteaIntegration, readGiteaConfig } from '@backstage/integration';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { getVoidLogger } from '../logging';
|
||||
import { UrlReaderPredicateTuple } from './types';
|
||||
import { DefaultReadTreeResponseFactory } from './tree';
|
||||
import getRawBody from 'raw-body';
|
||||
import { GiteaUrlReader } from './GiteaUrlReader';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
|
||||
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
|
||||
config: new ConfigReader({}),
|
||||
});
|
||||
|
||||
jest.mock('../scm', () => ({
|
||||
Git: {
|
||||
fromAuth: () => ({
|
||||
clone: jest.fn(() => Promise.resolve({})),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
const giteaProcessor = new GiteaUrlReader(
|
||||
new GiteaIntegration(
|
||||
readGiteaConfig(
|
||||
new ConfigReader({
|
||||
host: 'gitea.com',
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => {
|
||||
return GiteaUrlReader.factory({
|
||||
config: new ConfigReader(config),
|
||||
logger: getVoidLogger(),
|
||||
treeResponseFactory,
|
||||
});
|
||||
};
|
||||
|
||||
describe('GiteaUrlReader', () => {
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('reader factory', () => {
|
||||
it('creates a reader.', () => {
|
||||
const readers = createReader({
|
||||
integrations: {
|
||||
gitea: [{ host: 'gitea.com' }],
|
||||
},
|
||||
});
|
||||
expect(readers).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not create a default entry.', () => {
|
||||
const readers = createReader({
|
||||
integrations: {},
|
||||
});
|
||||
expect(readers).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('predicates', () => {
|
||||
it('returns true for the configured host', () => {
|
||||
const readers = createReader({
|
||||
integrations: {
|
||||
gitea: [{ host: 'gitea.com' }],
|
||||
},
|
||||
});
|
||||
const predicate = readers[0].predicate;
|
||||
|
||||
expect(predicate(new URL('https://gitea.com/path'))).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for a different host.', () => {
|
||||
const readers = createReader({
|
||||
integrations: {
|
||||
gitea: [{ host: 'gitea.com' }],
|
||||
},
|
||||
});
|
||||
const predicate = readers[0].predicate;
|
||||
|
||||
expect(predicate(new URL('https://github.com/path'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readUrl', () => {
|
||||
const responseBuffer = Buffer.from('Apache License');
|
||||
const giteaApiResponse = (content: any) => {
|
||||
return JSON.stringify({
|
||||
encoding: 'base64',
|
||||
content: Buffer.from(content).toString('base64'),
|
||||
});
|
||||
};
|
||||
|
||||
it('should be able to read file contents as buffer', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://gitea.com/api/v1/repos/owner/project/contents/LICENSE',
|
||||
(req, res, ctx) => {
|
||||
// Test utils prefers matching URL directly but it is part of Gitea's API
|
||||
if (req.url.searchParams.get('ref') === 'branch2') {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.body(giteaApiResponse(responseBuffer.toString())),
|
||||
);
|
||||
}
|
||||
|
||||
return res(ctx.status(500));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const result = await giteaProcessor.readUrl(
|
||||
'https://gitea.com/owner/project/src/branch/branch2/LICENSE',
|
||||
);
|
||||
const buffer = await result.buffer();
|
||||
expect(buffer.toString()).toBe(responseBuffer.toString());
|
||||
});
|
||||
|
||||
it('should be able to read file contents as stream', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://gitea.com/api/v1/repos/owner/project/contents/LICENSE',
|
||||
(req, res, ctx) => {
|
||||
if (req.url.searchParams.get('ref') === 'branch2') {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.body(giteaApiResponse(responseBuffer.toString())),
|
||||
);
|
||||
}
|
||||
|
||||
return res(ctx.status(500));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const result = await giteaProcessor.readUrl(
|
||||
'https://gitea.com/owner/project/src/branch/branch2/LICENSE',
|
||||
);
|
||||
const fromStream = await getRawBody(result.stream!());
|
||||
expect(fromStream.toString()).toBe(responseBuffer.toString());
|
||||
});
|
||||
|
||||
it('should raise NotFoundError on 404.', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://gitea.com/api/v1/repos/owner/project/contents/LICENSE',
|
||||
(_, res, ctx) => {
|
||||
return res(ctx.status(404, 'File not found.'));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
giteaProcessor.readUrl(
|
||||
'https://gitea.com/owner/project/src/branch/branch2/LICENSE',
|
||||
),
|
||||
).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
|
||||
it('should throw an error on non 404 errors.', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://gitea.com/api/v1/repos/owner/project/contents/LICENSE',
|
||||
(_, res, ctx) => {
|
||||
return res(ctx.status(500, 'Error!!!'));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
giteaProcessor.readUrl(
|
||||
'https://gitea.com/owner/project/src/branch/branch2/LICENSE',
|
||||
),
|
||||
).rejects.toThrow(
|
||||
'https://gitea.com/owner/project/src/branch/branch2/LICENSE could not be read as https://gitea.com/api/v1/repos/owner/project/contents/LICENSE?ref=branch2, 500 Error!!!',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2022 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 {
|
||||
getGiteaRequestOptions,
|
||||
getGiteaFileContentsUrl,
|
||||
GiteaIntegration,
|
||||
ScmIntegrations,
|
||||
} from '@backstage/integration';
|
||||
import { ReadUrlOptions, ReadUrlResponse } from './types';
|
||||
import {
|
||||
ReaderFactory,
|
||||
ReadTreeResponse,
|
||||
SearchResponse,
|
||||
UrlReader,
|
||||
} from './types';
|
||||
import fetch, { Response } from 'node-fetch';
|
||||
import { ReadUrlResponseFactory } from './ReadUrlResponseFactory';
|
||||
import {
|
||||
AuthenticationError,
|
||||
NotFoundError,
|
||||
NotModifiedError,
|
||||
} from '@backstage/errors';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
/**
|
||||
* Implements a {@link UrlReader} for the Gitea v1 api.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class GiteaUrlReader implements UrlReader {
|
||||
static factory: ReaderFactory = ({ config }) => {
|
||||
return ScmIntegrations.fromConfig(config)
|
||||
.gitea.list()
|
||||
.map(integration => {
|
||||
const reader = new GiteaUrlReader(integration);
|
||||
const predicate = (url: URL) => {
|
||||
return url.host === integration.config.host;
|
||||
};
|
||||
return { reader, predicate };
|
||||
});
|
||||
};
|
||||
|
||||
constructor(private readonly integration: GiteaIntegration) {}
|
||||
|
||||
async read(url: string): Promise<Buffer> {
|
||||
const response = await this.readUrl(url);
|
||||
return response.buffer();
|
||||
}
|
||||
|
||||
async readUrl(
|
||||
url: string,
|
||||
options?: ReadUrlOptions,
|
||||
): Promise<ReadUrlResponse> {
|
||||
let response: Response;
|
||||
const blobUrl = getGiteaFileContentsUrl(this.integration.config, url);
|
||||
|
||||
try {
|
||||
response = await fetch(blobUrl, {
|
||||
method: 'GET',
|
||||
...getGiteaRequestOptions(this.integration.config),
|
||||
signal: options?.signal as any,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(`Unable to read ${blobUrl}, ${e}`);
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
// Gitea returns an object with the file contents encoded, not the file itself
|
||||
const { encoding, content } = await response.json();
|
||||
|
||||
if (encoding === 'base64') {
|
||||
return ReadUrlResponseFactory.fromReadable(
|
||||
Readable.from(Buffer.from(content, 'base64')),
|
||||
{
|
||||
etag: response.headers.get('ETag') ?? undefined,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`Unknown encoding: ${encoding}`);
|
||||
}
|
||||
|
||||
const message = `${url} could not be read as ${blobUrl}, ${response.status} ${response.statusText}`;
|
||||
if (response.status === 404) {
|
||||
throw new NotFoundError(message);
|
||||
}
|
||||
|
||||
if (response.status === 304) {
|
||||
throw new NotModifiedError();
|
||||
}
|
||||
|
||||
if (response.status === 403) {
|
||||
throw new AuthenticationError();
|
||||
}
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
readTree(): Promise<ReadTreeResponse> {
|
||||
throw new Error('GiteaUrlReader readTree not implemented.');
|
||||
}
|
||||
search(): Promise<SearchResponse> {
|
||||
throw new Error('GiteaUrlReader search not implemented.');
|
||||
}
|
||||
|
||||
toString() {
|
||||
const { host } = this.integration.config;
|
||||
return `gitea{host=${host},authed=${Boolean(
|
||||
this.integration.config.password,
|
||||
)}}`;
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import { DefaultReadTreeResponseFactory } from './tree';
|
||||
import { FetchUrlReader } from './FetchUrlReader';
|
||||
import { GoogleGcsUrlReader } from './GoogleGcsUrlReader';
|
||||
import { AwsS3UrlReader } from './AwsS3UrlReader';
|
||||
import { GiteaUrlReader } from './GiteaUrlReader';
|
||||
|
||||
/**
|
||||
* Creation options for {@link UrlReaders}.
|
||||
@@ -89,6 +90,7 @@ export class UrlReaders {
|
||||
BitbucketUrlReader.factory,
|
||||
GerritUrlReader.factory,
|
||||
GithubUrlReader.factory,
|
||||
GiteaUrlReader.factory,
|
||||
GitlabUrlReader.factory,
|
||||
GoogleGcsUrlReader.factory,
|
||||
AwsS3UrlReader.factory,
|
||||
|
||||
@@ -21,6 +21,7 @@ export { BitbucketServerUrlReader } from './BitbucketServerUrlReader';
|
||||
export { GerritUrlReader } from './GerritUrlReader';
|
||||
export { GithubUrlReader } from './GithubUrlReader';
|
||||
export { GitlabUrlReader } from './GitlabUrlReader';
|
||||
export { GiteaUrlReader } from './GiteaUrlReader';
|
||||
export { AwsS3UrlReader } from './AwsS3UrlReader';
|
||||
export { FetchUrlReader } from './FetchUrlReader';
|
||||
export { ReadUrlResponseFactory } from './ReadUrlResponseFactory';
|
||||
|
||||
@@ -323,6 +323,17 @@ export function getGerritRequestOptions(config: GerritIntegrationConfig): {
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function getGiteaFileContentsUrl(
|
||||
config: GiteaIntegrationConfig,
|
||||
url: string,
|
||||
): string;
|
||||
|
||||
// @public
|
||||
export function getGiteaRequestOptions(config: GiteaIntegrationConfig): {
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export const getGitHubFileFetchUrl: typeof getGithubFileFetchUrl;
|
||||
|
||||
@@ -357,6 +368,35 @@ export function getGitLabRequestOptions(config: GitLabIntegrationConfig): {
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export class GiteaIntegration implements ScmIntegration {
|
||||
constructor(config: GiteaIntegrationConfig);
|
||||
// (undocumented)
|
||||
readonly config: GiteaIntegrationConfig;
|
||||
// (undocumented)
|
||||
static factory: ScmIntegrationsFactory<GiteaIntegration>;
|
||||
// (undocumented)
|
||||
resolveEditUrl(url: string): string;
|
||||
// (undocumented)
|
||||
resolveUrl(options: {
|
||||
url: string;
|
||||
base: string;
|
||||
lineNumber?: number | undefined;
|
||||
}): string;
|
||||
// (undocumented)
|
||||
get title(): string;
|
||||
// (undocumented)
|
||||
get type(): string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type GiteaIntegrationConfig = {
|
||||
host: string;
|
||||
baseUrl?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type GithubAppConfig = {
|
||||
appId: number;
|
||||
@@ -488,6 +528,8 @@ export interface IntegrationsByType {
|
||||
// (undocumented)
|
||||
gerrit: ScmIntegrationsGroup<GerritIntegration>;
|
||||
// (undocumented)
|
||||
gitea: ScmIntegrationsGroup<GiteaIntegration>;
|
||||
// (undocumented)
|
||||
github: ScmIntegrationsGroup<GithubIntegration>;
|
||||
// (undocumented)
|
||||
gitlab: ScmIntegrationsGroup<GitLabIntegration>;
|
||||
@@ -566,6 +608,9 @@ export function readGerritIntegrationConfigs(
|
||||
configs: Config[],
|
||||
): GerritIntegrationConfig[];
|
||||
|
||||
// @public
|
||||
export function readGiteaConfig(config: Config): GiteaIntegrationConfig;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export const readGitHubIntegrationConfig: typeof readGithubIntegrationConfig;
|
||||
|
||||
@@ -640,6 +685,8 @@ export interface ScmIntegrationRegistry
|
||||
// (undocumented)
|
||||
gerrit: ScmIntegrationsGroup<GerritIntegration>;
|
||||
// (undocumented)
|
||||
gitea: ScmIntegrationsGroup<GiteaIntegration>;
|
||||
// (undocumented)
|
||||
github: ScmIntegrationsGroup<GithubIntegration>;
|
||||
// (undocumented)
|
||||
gitlab: ScmIntegrationsGroup<GitLabIntegration>;
|
||||
@@ -673,6 +720,8 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
|
||||
// (undocumented)
|
||||
get gerrit(): ScmIntegrationsGroup<GerritIntegration>;
|
||||
// (undocumented)
|
||||
get gitea(): ScmIntegrationsGroup<GiteaIntegration>;
|
||||
// (undocumented)
|
||||
get github(): ScmIntegrationsGroup<GithubIntegration>;
|
||||
// (undocumented)
|
||||
get gitlab(): ScmIntegrationsGroup<GitLabIntegration>;
|
||||
|
||||
Vendored
+26
@@ -281,5 +281,31 @@ export interface Config {
|
||||
*/
|
||||
externalId?: string;
|
||||
}>;
|
||||
|
||||
/** Integration configuration for Gitea */
|
||||
gitea?: Array<{
|
||||
/**
|
||||
* The hostname of the given Gitea instance
|
||||
* @visibility frontend
|
||||
*/
|
||||
host: string;
|
||||
/**
|
||||
* The base url for the Gitea instance.
|
||||
* @visibility frontend
|
||||
*/
|
||||
baseUrl?: string;
|
||||
|
||||
/**
|
||||
* The username to use for authenticated requests.
|
||||
* @visibility secret
|
||||
*/
|
||||
username?: string;
|
||||
/**
|
||||
* Gitea password used to authenticate requests. This can be either a password
|
||||
* or a generated access token.
|
||||
* @visibility secret
|
||||
*/
|
||||
password?: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import { GitLabIntegrationConfig } from './gitlab';
|
||||
import { GitLabIntegration } from './gitlab/GitLabIntegration';
|
||||
import { basicIntegrations } from './helpers';
|
||||
import { ScmIntegrations } from './ScmIntegrations';
|
||||
import { GiteaIntegration, GiteaIntegrationConfig } from './gitea';
|
||||
|
||||
describe('ScmIntegrations', () => {
|
||||
const awsS3 = new AwsS3Integration({
|
||||
@@ -69,6 +70,10 @@ describe('ScmIntegrations', () => {
|
||||
host: 'gitlab.local',
|
||||
} as GitLabIntegrationConfig);
|
||||
|
||||
const gitea = new GiteaIntegration({
|
||||
host: 'gitea.local',
|
||||
} as GiteaIntegrationConfig);
|
||||
|
||||
const i = new ScmIntegrations({
|
||||
awsS3: basicIntegrations([awsS3], item => item.config.host),
|
||||
azure: basicIntegrations([azure], item => item.config.host),
|
||||
@@ -81,6 +86,7 @@ describe('ScmIntegrations', () => {
|
||||
gerrit: basicIntegrations([gerrit], item => item.config.host),
|
||||
github: basicIntegrations([github], item => item.config.host),
|
||||
gitlab: basicIntegrations([gitlab], item => item.config.host),
|
||||
gitea: basicIntegrations([gitea], item => item.config.host),
|
||||
});
|
||||
|
||||
it('can get the specifics', () => {
|
||||
@@ -96,6 +102,7 @@ describe('ScmIntegrations', () => {
|
||||
expect(i.gerrit.byUrl('https://gerrit.local')).toBe(gerrit);
|
||||
expect(i.github.byUrl('https://github.local')).toBe(github);
|
||||
expect(i.gitlab.byUrl('https://gitlab.local')).toBe(gitlab);
|
||||
expect(i.gitea.byUrl('https://gitea.local')).toBe(gitea);
|
||||
});
|
||||
|
||||
it('can list', () => {
|
||||
@@ -109,6 +116,7 @@ describe('ScmIntegrations', () => {
|
||||
gerrit,
|
||||
github,
|
||||
gitlab,
|
||||
gitea,
|
||||
]),
|
||||
);
|
||||
});
|
||||
@@ -122,6 +130,7 @@ describe('ScmIntegrations', () => {
|
||||
expect(i.byUrl('https://gerrit.local')).toBe(gerrit);
|
||||
expect(i.byUrl('https://github.local')).toBe(github);
|
||||
expect(i.byUrl('https://gitlab.local')).toBe(gitlab);
|
||||
expect(i.byUrl('https://gitea.local')).toBe(gitea);
|
||||
|
||||
expect(i.byHost('awss3.local')).toBe(awsS3);
|
||||
expect(i.byHost('azure.local')).toBe(azure);
|
||||
@@ -131,6 +140,7 @@ describe('ScmIntegrations', () => {
|
||||
expect(i.byHost('gerrit.local')).toBe(gerrit);
|
||||
expect(i.byHost('github.local')).toBe(github);
|
||||
expect(i.byHost('gitlab.local')).toBe(gitlab);
|
||||
expect(i.byHost('gitea.local')).toBe(gitea);
|
||||
});
|
||||
|
||||
it('can resolveUrl using fallback', () => {
|
||||
|
||||
@@ -26,6 +26,7 @@ import { GitLabIntegration } from './gitlab/GitLabIntegration';
|
||||
import { defaultScmResolveUrl } from './helpers';
|
||||
import { ScmIntegration, ScmIntegrationsGroup } from './types';
|
||||
import { ScmIntegrationRegistry } from './registry';
|
||||
import { GiteaIntegration } from './gitea';
|
||||
|
||||
/**
|
||||
* The set of supported integrations.
|
||||
@@ -44,6 +45,7 @@ export interface IntegrationsByType {
|
||||
gerrit: ScmIntegrationsGroup<GerritIntegration>;
|
||||
github: ScmIntegrationsGroup<GithubIntegration>;
|
||||
gitlab: ScmIntegrationsGroup<GitLabIntegration>;
|
||||
gitea: ScmIntegrationsGroup<GiteaIntegration>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,6 +66,7 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
|
||||
gerrit: GerritIntegration.factory({ config }),
|
||||
github: GithubIntegration.factory({ config }),
|
||||
gitlab: GitLabIntegration.factory({ config }),
|
||||
gitea: GiteaIntegration.factory({ config }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -106,6 +109,10 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
|
||||
return this.byType.gitlab;
|
||||
}
|
||||
|
||||
get gitea(): ScmIntegrationsGroup<GiteaIntegration> {
|
||||
return this.byType.gitea;
|
||||
}
|
||||
|
||||
list(): ScmIntegration[] {
|
||||
return Object.values(this.byType).flatMap(
|
||||
i => i.list() as ScmIntegration[],
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright 2022 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 { GiteaIntegration } from './GiteaIntegration';
|
||||
|
||||
describe('GiteaIntegration', () => {
|
||||
it('has a working factory', () => {
|
||||
const integrations = GiteaIntegration.factory({
|
||||
config: new ConfigReader({
|
||||
integrations: {
|
||||
gitea: [
|
||||
{
|
||||
host: 'gitea.example.com',
|
||||
username: 'git',
|
||||
baseUrl: 'https://gitea.example.com/route',
|
||||
password: '1234',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(integrations.list().length).toBe(1);
|
||||
expect(integrations.list()[0].config.host).toBe('gitea.example.com');
|
||||
expect(integrations.list()[0].config.baseUrl).toBe(
|
||||
'https://gitea.example.com/route',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the basics', () => {
|
||||
const integration = new GiteaIntegration({
|
||||
host: 'gitea.example.com',
|
||||
});
|
||||
expect(integration.type).toBe('gitea');
|
||||
expect(integration.title).toBe('gitea.example.com');
|
||||
});
|
||||
|
||||
describe('resolveUrl', () => {
|
||||
it('works for valid urls, ignoring line number', () => {
|
||||
const integration = new GiteaIntegration({
|
||||
host: 'gitea.example.com',
|
||||
});
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: 'https://gitea.example.com/catalog-info.yaml',
|
||||
base: 'https://gitea.example.com/catalog-info.yaml',
|
||||
lineNumber: 9,
|
||||
}),
|
||||
).toBe('https://gitea.example.com/catalog-info.yaml');
|
||||
});
|
||||
|
||||
it('handles line numbers', () => {
|
||||
const integration = new GiteaIntegration({
|
||||
host: 'gitea.example.com',
|
||||
});
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: '',
|
||||
base: 'https://gitea.example.com/catalog-info.yaml#4',
|
||||
lineNumber: 9,
|
||||
}),
|
||||
).toBe('https://gitea.example.com/catalog-info.yaml#L9');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolves with a relative url', () => {
|
||||
it('works for valid urls', () => {
|
||||
const integration = new GiteaIntegration({
|
||||
host: 'gitea.example.com',
|
||||
});
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: './skeleton',
|
||||
base: 'https://gitea.example.com/git/plugins/repo/+/refs/heads/master/template.yaml',
|
||||
}),
|
||||
).toBe(
|
||||
'https://gitea.example.com/git/plugins/repo/+/refs/heads/master/skeleton',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolves with an absolute url', () => {
|
||||
it('works for valid urls', () => {
|
||||
const integration = new GiteaIntegration({
|
||||
host: 'gitea.example.com',
|
||||
});
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: '/catalog-info.yaml',
|
||||
base: 'https://gitea.example.com/git/repo/+/refs/heads/master/',
|
||||
}),
|
||||
).toBe(
|
||||
'https://gitea.example.com/git/repo/+/refs/heads/master/catalog-info.yaml',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('resolve edit URL', () => {
|
||||
const integration = new GiteaIntegration({
|
||||
host: 'gitea.example.com',
|
||||
});
|
||||
|
||||
expect(
|
||||
integration.resolveEditUrl(
|
||||
'https://gitea.example.com/owner/repo/src/branch/branch_name/path/to/c.yaml',
|
||||
),
|
||||
).toBe(
|
||||
'https://gitea.example.com/owner/repo/_edit/branch_name/path/to/c.yaml',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2022 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 { basicIntegrations, defaultScmResolveUrl } from '../helpers';
|
||||
import { ScmIntegration, ScmIntegrationsFactory } from '../types';
|
||||
import { GiteaIntegrationConfig, readGiteaConfig } from './config';
|
||||
import { getGiteaEditContentsUrl } from './core';
|
||||
|
||||
/**
|
||||
* A Gitea based integration.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class GiteaIntegration implements ScmIntegration {
|
||||
static factory: ScmIntegrationsFactory<GiteaIntegration> = ({ config }) => {
|
||||
const configs = config.getOptionalConfigArray('integrations.gitea') ?? [];
|
||||
const giteaConfigs = configs.map(c => readGiteaConfig(c));
|
||||
|
||||
return basicIntegrations(
|
||||
giteaConfigs.map(c => new GiteaIntegration(c)),
|
||||
(gitea: GiteaIntegration) => gitea.config.host,
|
||||
);
|
||||
};
|
||||
|
||||
constructor(readonly config: GiteaIntegrationConfig) {}
|
||||
|
||||
get type(): string {
|
||||
return 'gitea';
|
||||
}
|
||||
|
||||
get title(): string {
|
||||
return this.config.host;
|
||||
}
|
||||
|
||||
resolveUrl(options: {
|
||||
url: string;
|
||||
base: string;
|
||||
lineNumber?: number | undefined;
|
||||
}): string {
|
||||
return defaultScmResolveUrl(options);
|
||||
}
|
||||
|
||||
resolveEditUrl(url: string): string {
|
||||
return getGiteaEditContentsUrl(this.config, url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2022 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 { GiteaIntegrationConfig, readGiteaConfig } from './config';
|
||||
|
||||
describe('readGiteaConfig', () => {
|
||||
function buildConfig(data: Partial<GiteaIntegrationConfig>): Config {
|
||||
return new ConfigReader(data);
|
||||
}
|
||||
|
||||
async function buildFrontendConfig(
|
||||
data: Partial<GiteaIntegrationConfig>,
|
||||
): 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: { gitea: [data] } }, context: 'app' }],
|
||||
{ visibility: ['frontend'] },
|
||||
);
|
||||
return new ConfigReader((processed[0].data as any).integrations.gitea[0]);
|
||||
}
|
||||
|
||||
it('reads all values', () => {
|
||||
const output = readGiteaConfig(
|
||||
buildConfig({
|
||||
host: 'a.com',
|
||||
baseUrl: 'https://a.com/route/api',
|
||||
username: 'u',
|
||||
password: 'p',
|
||||
}),
|
||||
);
|
||||
expect(output).toEqual({
|
||||
host: 'a.com',
|
||||
baseUrl: 'https://a.com/route/api',
|
||||
username: 'u',
|
||||
password: 'p',
|
||||
});
|
||||
});
|
||||
|
||||
it('can create a default value if the API base URL is missing', () => {
|
||||
const output = readGiteaConfig(
|
||||
buildConfig({
|
||||
host: 'a.com',
|
||||
}),
|
||||
);
|
||||
expect(output).toEqual({
|
||||
host: 'a.com',
|
||||
baseUrl: 'https://a.com',
|
||||
username: undefined,
|
||||
password: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects funky configs', () => {
|
||||
const valid: any = {
|
||||
host: 'a.com',
|
||||
};
|
||||
expect(() => readGiteaConfig(buildConfig({ ...valid, host: 2 }))).toThrow(
|
||||
/host/,
|
||||
);
|
||||
expect(() =>
|
||||
readGiteaConfig(buildConfig({ ...valid, baseUrl: 2 })),
|
||||
).toThrow(/baseUrl/);
|
||||
});
|
||||
|
||||
it('works on the frontend', async () => {
|
||||
expect(
|
||||
readGiteaConfig(
|
||||
await buildFrontendConfig({
|
||||
host: 'a.com',
|
||||
baseUrl: 'https://a.com/route',
|
||||
username: 'u',
|
||||
password: 'p',
|
||||
}),
|
||||
),
|
||||
).toEqual({
|
||||
host: 'a.com',
|
||||
baseUrl: 'https://a.com/route',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { trimEnd } from 'lodash';
|
||||
import { isValidHost } from '../helpers';
|
||||
|
||||
/**
|
||||
* The configuration for a single Gitea integration.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type GiteaIntegrationConfig = {
|
||||
/**
|
||||
* The host of the target that this matches on, e.g. "gitea.website.com"
|
||||
*/
|
||||
host: string;
|
||||
/**
|
||||
* The optional base URL of the Gitea instance. It is assumed that https
|
||||
* is used and that the base path is "/" on the host. If that is not the
|
||||
* case set the complete base url to the gitea instance, e.g.
|
||||
* "https://gitea.website.com/". This is the url that you would open
|
||||
* in a browser.
|
||||
*/
|
||||
baseUrl?: string;
|
||||
/**
|
||||
* The username to use for requests to gitea.
|
||||
*/
|
||||
username?: string;
|
||||
|
||||
/**
|
||||
* The password or http token to use for authentication.
|
||||
*/
|
||||
password?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a location config block for use in GiteaIntegration
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function readGiteaConfig(config: Config): GiteaIntegrationConfig {
|
||||
const host = config.getString('host');
|
||||
let baseUrl = config.getOptionalString('baseUrl');
|
||||
const username = config.getOptionalString('username');
|
||||
const password = config.getOptionalString('password');
|
||||
|
||||
if (!isValidHost(host)) {
|
||||
throw new Error(
|
||||
`Invalid Gitea integration config, '${host}' is not a valid host`,
|
||||
);
|
||||
}
|
||||
|
||||
if (baseUrl) {
|
||||
baseUrl = trimEnd(baseUrl, '/');
|
||||
} else {
|
||||
baseUrl = `https://${host}`;
|
||||
}
|
||||
|
||||
return {
|
||||
host,
|
||||
baseUrl,
|
||||
username,
|
||||
password,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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 { setupServer } from 'msw/node';
|
||||
import { setupRequestMockHandlers } from '@backstage/test-utils';
|
||||
import { GiteaIntegrationConfig } from './config';
|
||||
import {
|
||||
getGiteaEditContentsUrl,
|
||||
getGiteaFileContentsUrl,
|
||||
getGiteaRequestOptions,
|
||||
} from './core';
|
||||
|
||||
describe('gitea core', () => {
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
|
||||
describe('getGiteaFileContentsUrl', () => {
|
||||
it('can create an url from arguments', () => {
|
||||
const config: GiteaIntegrationConfig = {
|
||||
host: 'gitea.com',
|
||||
};
|
||||
expect(
|
||||
getGiteaFileContentsUrl(
|
||||
config,
|
||||
'https://gitea.com/a/b/src/branch/branch_name/path/to/c.yaml',
|
||||
),
|
||||
).toEqual(
|
||||
'https://gitea.com/api/v1/repos/a/b/contents/path/to/c.yaml?ref=branch_name',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGiteaEditContentsUrl', () => {
|
||||
it('can create an url from arguments', () => {
|
||||
const config: GiteaIntegrationConfig = {
|
||||
host: 'gitea.example.com',
|
||||
};
|
||||
expect(
|
||||
getGiteaEditContentsUrl(
|
||||
config,
|
||||
'https://gitea.example.com/owner/repo/src/branch/branch_name/path/to/c.yaml',
|
||||
),
|
||||
).toEqual(
|
||||
'https://gitea.example.com/owner/repo/_edit/branch_name/path/to/c.yaml',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGerritRequestOptions', () => {
|
||||
it('adds token header when only a password is specified', () => {
|
||||
const authRequest: GiteaIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
password: 'P',
|
||||
};
|
||||
const anonymousRequest: GiteaIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
};
|
||||
expect(
|
||||
(getGiteaRequestOptions(authRequest).headers as any).Authorization,
|
||||
).toEqual('token P');
|
||||
expect(getGiteaRequestOptions(anonymousRequest).headers).toBeUndefined();
|
||||
});
|
||||
|
||||
it('adds basic auth when username and password are specified', () => {
|
||||
const authRequest: GiteaIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
username: 'username',
|
||||
password: 'P',
|
||||
};
|
||||
|
||||
const basicAuthentication = `basic ${Buffer.from(
|
||||
`${authRequest.username}:${authRequest.password}`,
|
||||
).toString('base64')}`;
|
||||
|
||||
expect(
|
||||
(getGiteaRequestOptions(authRequest).headers as any).Authorization,
|
||||
).toEqual(basicAuthentication);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2022 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 { GiteaIntegrationConfig } from './config';
|
||||
|
||||
/**
|
||||
* Given a URL pointing to a file, returns a URL
|
||||
* for editing the contents of the data.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Converts
|
||||
* from: https://gitea.com/a/b/src/branchname/path/to/c.yaml
|
||||
* or: https://gitea.com/a/b/_edit/branchname/path/to/c.yaml
|
||||
*
|
||||
* @param url - A URL pointing to a file
|
||||
* @param config - The relevant provider config
|
||||
* @public
|
||||
*/
|
||||
export function getGiteaEditContentsUrl(
|
||||
config: GiteaIntegrationConfig,
|
||||
url: string,
|
||||
) {
|
||||
try {
|
||||
const baseUrl = config.baseUrl ?? `https://${config.host}`;
|
||||
const [_blank, owner, name, _src, _branch, ref, ...path] = url
|
||||
.replace(baseUrl, '')
|
||||
.split('/');
|
||||
const pathWithoutSlash = path.join('/').replace(/^\//, '');
|
||||
return `${baseUrl}/${owner}/${name}/_edit/${ref}/${pathWithoutSlash}`;
|
||||
} catch (e) {
|
||||
throw new Error(`Incorrect URL: ${url}, ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a URL pointing to a file, returns an api URL
|
||||
* for fetching the contents of the data.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Converts
|
||||
* from: https://gitea.com/a/b/src/branch/branchname/path/to/c.yaml
|
||||
* to: https://gitea.com/api/v1/repos/a/b/contents/path/to/c.yaml?ref=branchname
|
||||
*
|
||||
* @param url - A URL pointing to a file
|
||||
* @param config - The relevant provider config
|
||||
* @public
|
||||
*/
|
||||
export function getGiteaFileContentsUrl(
|
||||
config: GiteaIntegrationConfig,
|
||||
url: string,
|
||||
) {
|
||||
try {
|
||||
const baseUrl = config.baseUrl ?? `https://${config.host}`;
|
||||
const [_blank, owner, name, _src, _branch, ref, ...path] = url
|
||||
.replace(baseUrl, '')
|
||||
.split('/');
|
||||
const pathWithoutSlash = path.join('/').replace(/^\//, '');
|
||||
|
||||
return `${baseUrl}/api/v1/repos/${owner}/${name}/contents/${pathWithoutSlash}?ref=${ref}`;
|
||||
} catch (e) {
|
||||
throw new Error(`Incorrect URL: ${url}, ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return request headers for a Gitea provider.
|
||||
*
|
||||
* @param config - A Gitea provider config
|
||||
* @public
|
||||
*/
|
||||
export function getGiteaRequestOptions(config: GiteaIntegrationConfig): {
|
||||
headers?: Record<string, string>;
|
||||
} {
|
||||
const headers: Record<string, string> = {};
|
||||
const { username, password } = config;
|
||||
|
||||
if (!password) {
|
||||
return headers;
|
||||
}
|
||||
|
||||
if (username) {
|
||||
headers.Authorization = `basic ${Buffer.from(
|
||||
`${username}:${password}`,
|
||||
).toString('base64')}`;
|
||||
} else {
|
||||
headers.Authorization = `token ${password}`;
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2022 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 { GiteaIntegration } from './GiteaIntegration';
|
||||
export { getGiteaRequestOptions, getGiteaFileContentsUrl } from './core';
|
||||
export { readGiteaConfig } from './config';
|
||||
export type { GiteaIntegrationConfig } from './config';
|
||||
@@ -26,6 +26,7 @@ export * from './bitbucket';
|
||||
export * from './bitbucketCloud';
|
||||
export * from './bitbucketServer';
|
||||
export * from './gerrit';
|
||||
export * from './gitea';
|
||||
export * from './github';
|
||||
export * from './gitlab';
|
||||
export * from './googleGcs';
|
||||
|
||||
@@ -23,6 +23,7 @@ import { BitbucketServerIntegration } from './bitbucketServer/BitbucketServerInt
|
||||
import { GerritIntegration } from './gerrit/GerritIntegration';
|
||||
import { GithubIntegration } from './github/GithubIntegration';
|
||||
import { GitLabIntegration } from './gitlab/GitLabIntegration';
|
||||
import { GiteaIntegration } from './gitea/GiteaIntegration';
|
||||
|
||||
/**
|
||||
* Holds all registered SCM integrations, of all types.
|
||||
@@ -42,7 +43,7 @@ export interface ScmIntegrationRegistry
|
||||
gerrit: ScmIntegrationsGroup<GerritIntegration>;
|
||||
github: ScmIntegrationsGroup<GithubIntegration>;
|
||||
gitlab: ScmIntegrationsGroup<GitLabIntegration>;
|
||||
|
||||
gitea: ScmIntegrationsGroup<GiteaIntegration>;
|
||||
/**
|
||||
* Resolves an absolute or relative URL in relation to a base URL.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user