Merge branch 'master' into lintMod
This commit is contained in:
@@ -73,4 +73,19 @@ describe('ScmIntegrations', () => {
|
||||
expect(i.byHost('github.local')).toBe(github);
|
||||
expect(i.byHost('gitlab.local')).toBe(gitlab);
|
||||
});
|
||||
|
||||
it('can resolveUrl using fallback', () => {
|
||||
expect(
|
||||
i.resolveUrl({
|
||||
url: '../b.yaml',
|
||||
base: 'https://no-matching-integration.com/x/a.yaml',
|
||||
}),
|
||||
).toBe('https://no-matching-integration.com/b.yaml');
|
||||
expect(
|
||||
i.resolveUrl({
|
||||
url: 'https://absolute.com/path',
|
||||
base: 'https://no-matching-integration.com/x/a.yaml',
|
||||
}),
|
||||
).toBe('https://absolute.com/path');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -81,4 +81,13 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
|
||||
.map(i => i.byHost(host))
|
||||
.find(Boolean);
|
||||
}
|
||||
|
||||
resolveUrl(options: { url: string; base: string }): string {
|
||||
const resolve = this.byUrl(options.base)?.resolveUrl;
|
||||
if (!resolve) {
|
||||
return new URL(options.url, options.base).toString();
|
||||
}
|
||||
|
||||
return resolve(options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,4 +41,52 @@ describe('AzureIntegration', () => {
|
||||
expect(integration.type).toBe('azure');
|
||||
expect(integration.title).toBe('h.com');
|
||||
});
|
||||
|
||||
describe('resolveUrl', () => {
|
||||
it('works for valid urls', () => {
|
||||
const integration = new AzureIntegration({
|
||||
host: 'dev.azure.com',
|
||||
} as any);
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: '../a.yaml',
|
||||
base:
|
||||
'https://dev.azure.com/organization/project/_git/repository?path=%2Ffolder%2Fcatalog-info.yaml',
|
||||
}),
|
||||
).toBe(
|
||||
'https://dev.azure.com/organization/project/_git/repository?path=%2Fa.yaml',
|
||||
);
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: './a.yaml',
|
||||
base: 'https://dev.azure.com/organization/project/_git/repository',
|
||||
}),
|
||||
).toBe(
|
||||
'https://dev.azure.com/organization/project/_git/repository?path=%2Fa.yaml',
|
||||
);
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: 'https://absolute.com/path',
|
||||
base:
|
||||
'https://dev.azure.com/organization/project/_git/repository?path=%2Fcatalog-info.yaml',
|
||||
}),
|
||||
).toBe('https://absolute.com/path');
|
||||
});
|
||||
|
||||
it('falls back to regular URL resolution if not in a repo', () => {
|
||||
const integration = new AzureIntegration({
|
||||
host: 'dev.azure.com',
|
||||
} as any);
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: './test',
|
||||
base: 'https://dev.azure.com/organization/project/_git',
|
||||
}),
|
||||
).toBe('https://dev.azure.com/organization/project/test');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import { basicIntegrations } from '../helpers';
|
||||
import { ScmIntegration, ScmIntegrationsFactory } from '../types';
|
||||
import { AzureIntegrationConfig, readAzureIntegrationConfigs } from './config';
|
||||
@@ -42,4 +43,39 @@ export class AzureIntegration implements ScmIntegration {
|
||||
get config(): AzureIntegrationConfig {
|
||||
return this.integrationConfig;
|
||||
}
|
||||
|
||||
/*
|
||||
* Azure repo URLs on the form with a `path` query param are treated specially.
|
||||
*
|
||||
* Example base URL: https://dev.azure.com/organization/project/_git/repository?path=%2Fcatalog-info.yaml
|
||||
*/
|
||||
resolveUrl(options: { url: string; base: string }): string {
|
||||
const { url, base } = options;
|
||||
|
||||
// If we can parse the url, it is absolute - then return it verbatim
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new URL(url);
|
||||
return url;
|
||||
} catch {
|
||||
// Ignore intentionally - looks like a relative path
|
||||
}
|
||||
|
||||
const parsed = parseGitUrl(base);
|
||||
const { organization, owner, name, filepath } = parsed;
|
||||
|
||||
// If not an actual file path within a repo, treat the URL as raw
|
||||
if (!organization || !owner || !name) {
|
||||
return new URL(url, base).toString();
|
||||
}
|
||||
|
||||
const path = filepath?.replace(/^\//, '') || '';
|
||||
const mockBaseUrl = new URL(`https://a.com/${path}`);
|
||||
const updatedPath = new URL(url, mockBaseUrl).pathname;
|
||||
|
||||
const newUrl = new URL(base);
|
||||
newUrl.searchParams.set('path', updatedPath);
|
||||
|
||||
return newUrl.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ describe('bitbucket core', () => {
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
|
||||
@@ -250,5 +251,40 @@ describe('bitbucket core', () => {
|
||||
);
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,11 +32,19 @@ export async function getBitbucketDefaultBranch(
|
||||
|
||||
const isHosted = resource === 'bitbucket.org';
|
||||
// Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp184
|
||||
const branchUrl = isHosted
|
||||
let branchUrl = isHosted
|
||||
? `${config.apiBaseUrl}/repositories/${project}/${repoName}`
|
||||
: `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/default-branch`;
|
||||
|
||||
const response = await fetch(branchUrl, getBitbucketRequestOptions(config));
|
||||
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);
|
||||
|
||||
@@ -49,23 +49,23 @@ describe('gitlab core', () => {
|
||||
{
|
||||
config: configWithNoToken,
|
||||
url:
|
||||
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml',
|
||||
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path%20with%20spaces/to/file.yaml',
|
||||
result:
|
||||
'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch',
|
||||
'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%20with%20spaces%2Fto%2Ffile.yaml/raw?ref=branch',
|
||||
},
|
||||
{
|
||||
config: configWithToken,
|
||||
url:
|
||||
'https://gitlab.example.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml',
|
||||
'https://gitlab.example.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path%20with%20spaces/to/file.yaml',
|
||||
result:
|
||||
'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch',
|
||||
'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%20with%20spaces%2Fto%2Ffile.yaml/raw?ref=branch',
|
||||
},
|
||||
{
|
||||
config: configWithNoToken,
|
||||
url:
|
||||
'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/to/file.yaml', // Repo not in subgroup
|
||||
'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path%20with%20spaces/to/file.yaml', // Repo not in subgroup
|
||||
result:
|
||||
'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch',
|
||||
'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%20with%20spaces%2Fto%2Ffile.yaml/raw?ref=branch',
|
||||
},
|
||||
// Raw URLs
|
||||
{
|
||||
|
||||
@@ -109,7 +109,7 @@ export function buildProjectUrl(target: string, projectID: Number): URL {
|
||||
'/api/v4/projects',
|
||||
projectID,
|
||||
'repository/files',
|
||||
encodeURIComponent(filePath.join('/')),
|
||||
encodeURIComponent(decodeURIComponent(filePath.join('/'))),
|
||||
'raw',
|
||||
].join('/');
|
||||
url.search = `?ref=${branch}`;
|
||||
|
||||
@@ -34,6 +34,18 @@ export interface ScmIntegration {
|
||||
* differentiate between different integrations.
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* Works like the two-argument form of the URL constructor, resolving an
|
||||
* absolute or relative URL in relation to a base URL.
|
||||
*
|
||||
* If this method is not implemented, the URL constructor is used instead for
|
||||
* URLs that match this integration.
|
||||
*
|
||||
* @param options.url The (absolute or relative) URL or path to resolve
|
||||
* @param options.base The base URL onto which this resolution happens
|
||||
*/
|
||||
resolveUrl?(options: { url: string; base: string }): string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,6 +81,15 @@ export interface ScmIntegrationRegistry
|
||||
bitbucket: ScmIntegrationsGroup<BitbucketIntegration>;
|
||||
github: ScmIntegrationsGroup<GitHubIntegration>;
|
||||
gitlab: ScmIntegrationsGroup<GitLabIntegration>;
|
||||
|
||||
/**
|
||||
* Works like the two-argument form of the URL constructor, resolving an
|
||||
* absolute or relative URL in relation to a base URL.
|
||||
*
|
||||
* @param options.url The (absolute or relative) URL or path to resolve
|
||||
* @param options.base The base URL onto which this resolution happens
|
||||
*/
|
||||
resolveUrl(options: { url: string; base: string }): string;
|
||||
}
|
||||
|
||||
export type ScmIntegrationsFactory<T extends ScmIntegration> = (options: {
|
||||
|
||||
Reference in New Issue
Block a user