Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions workspaces/x2a/packages/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import {
X2APage,
x2aPluginTranslations,
RepoAuthenticationExtension,
X2ARepoUrlPickerExtension,
} from '@red-hat-developer-hub/backstage-plugin-x2a';
import {
bitbucketAuthApiRef,
Expand Down Expand Up @@ -160,6 +161,7 @@ const routes = (
<Route path="/create" element={<ScaffolderPage />}>
<ScaffolderFieldExtensions>
<RepoAuthenticationExtension />
<X2ARepoUrlPickerExtension />
</ScaffolderFieldExtensions>
</Route>
</FlatRoutes>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,11 @@ spec:
description: |
The Owner should be your SCM (Source Code Management) username. The repository name should be a name that already exists in your SCM account and contains a Chef cookbook or directory to convert.
type: string
ui:field: RepoUrlPicker
# HACK: X2ARepoUrlPicker wraps the default RepoUrlPicker to
# fix a Bitbucket Cloud/Server type mismatch in Backstage
# scaffolder. Replace with RepoUrlPicker once upstream is fixed.
# See https://redhat.atlassian.net/browse/FLPATH-4033
ui:field: X2ARepoUrlPicker
ui:options:
requestUserCredentials:
secretsKey: SRC_USER_OAUTH_TOKEN
Expand Down Expand Up @@ -177,7 +181,11 @@ spec:
description: |
The Owner should be your SCM (Source Code Management) username. The repository name should be a name that already exists in your SCM account. It will be populated by converted Ansible sources and intermediary artifacts.
type: string
ui:field: RepoUrlPicker
# HACK: X2ARepoUrlPicker wraps the default RepoUrlPicker to
# fix a Bitbucket Cloud/Server type mismatch in Backstage
# scaffolder. Replace with RepoUrlPicker once upstream is fixed.
# See https://redhat.atlassian.net/browse/FLPATH-4033
ui:field: X2ARepoUrlPicker
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A brief comment that this is a workaround for BitBucker will be helpful in the future.

ui:options:
requestUserCredentials:
secretsKey: TGT_USER_OAUTH_TOKEN
Expand Down
4 changes: 3 additions & 1 deletion workspaces/x2a/plugins/x2a/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@
},
"dependencies": {
"@backstage/catalog-model": "^1.7.6",
"@backstage/core-app-api": "^1.19.2",
"@backstage/core-components": "^0.18.3",
"@backstage/core-plugin-api": "^1.12.0",
"@backstage/integration-react": "^1.2.12",
"@backstage/plugin-catalog": "^1.32.0",
"@backstage/plugin-permission-react": "^0.4.38",
"@backstage/plugin-scaffolder": "^1.34.3",
"@backstage/plugin-scaffolder-react": "^1.19.3",
"@backstage/theme": "^0.7.0",
"@backstage/ui": "^0.9.1",
Expand All @@ -60,7 +63,6 @@
"devDependencies": {
"@backstage/cli": "^0.34.5",
"@backstage/config": "^1.3.6",
"@backstage/core-app-api": "^1.19.2",
"@backstage/dev-utils": "^1.1.17",
"@backstage/frontend-plugin-api": "^0.14.1",
"@backstage/test-utils": "^1.7.13",
Expand Down
3 changes: 3 additions & 0 deletions workspaces/x2a/plugins/x2a/report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,9 @@ readonly empty: string;
// @public
export const x2aPluginTranslations: TranslationResource<"plugin.x2a">;

// @public
export const X2ARepoUrlPickerExtension: FieldExtensionComponent<string, {}>;

// (No @packageDocumentation comment for this package)

```
7 changes: 6 additions & 1 deletion workspaces/x2a/plugins/x2a/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { x2APlugin, X2APage, RepoAuthenticationExtension } from './plugin';
export {
x2APlugin,
X2APage,
RepoAuthenticationExtension,
X2ARepoUrlPickerExtension,
} from './plugin';
export { x2aPluginTranslations, x2aPluginTranslationRef } from './translations';
export {
useTranslation as useX2ATranslation,
Expand Down
22 changes: 21 additions & 1 deletion workspaces/x2a/plugins/x2a/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ import {
import { createScaffolderFieldExtension } from '@backstage/plugin-scaffolder-react';

import { rootRouteRef } from './routes';
import { RepoAuthentication, repoAuthenticationValidation } from './scaffolder';
import {
RepoAuthentication,
repoAuthenticationValidation,
RepoUrlPickerWithBitbucketFix,
} from './scaffolder';
import { repoPickerValidation } from '@backstage/plugin-scaffolder';

/** @public */
export const x2APlugin = createPlugin({
Expand All @@ -47,3 +52,18 @@ export const RepoAuthenticationExtension = x2APlugin.provide(
validation: repoAuthenticationValidation,
}),
);

/**
* Scaffolder field extension that wraps the built-in RepoUrlPicker with a fix
* for Bitbucket Cloud/Server type resolution. Use `ui:field: X2ARepoUrlPicker`
* in templates instead of `RepoUrlPicker` when bitbucket.org is an allowed host.
*
* @public
*/
export const X2ARepoUrlPickerExtension = x2APlugin.provide(
createScaffolderFieldExtension({
component: RepoUrlPickerWithBitbucketFix,
name: 'X2ARepoUrlPicker',
validation: repoPickerValidation,
}),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright Red Hat, Inc.
*
* 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 type { ScmIntegration } from '@backstage/integration';
import { wrapIntegrationsApi } from './RepoUrlPickerWithBitbucketFix';

function createFakeApi(integrationsByHost: Record<string, ScmIntegration>) {
return {
list: jest.fn(() => Object.values(integrationsByHost)),
byUrl: jest.fn(),
byHost: jest.fn((host: string) => integrationsByHost[host]),
resolveUrl: jest.fn(),
resolveEditUrl: jest.fn(),
} as any;
}

function makeIntegration(type: string, host: string): ScmIntegration {
return {
type,
title: `${type} - ${host}`,
byUrl: jest.fn(),
resolveUrl: jest.fn(),
resolveEditUrl: jest.fn(),
} as unknown as ScmIntegration;
}

describe('wrapIntegrationsApi', () => {
it('remaps bitbucketCloud type to bitbucket', () => {
const integration = makeIntegration('bitbucketCloud', 'bitbucket.org');
const api = createFakeApi({ 'bitbucket.org': integration });
const wrapped = wrapIntegrationsApi(api);

const result = wrapped.byHost('bitbucket.org');

expect(result).toBeDefined();
expect(result!.type).toBe('bitbucket');
});

it('remaps bitbucketServer type to bitbucket', () => {
const integration = makeIntegration(
'bitbucketServer',
'bitbucket.mycompany.com',
);
const api = createFakeApi({ 'bitbucket.mycompany.com': integration });
const wrapped = wrapIntegrationsApi(api);

const result = wrapped.byHost('bitbucket.mycompany.com');

expect(result).toBeDefined();
expect(result!.type).toBe('bitbucket');
});

it('preserves non-type properties on remapped integrations', () => {
const integration = makeIntegration('bitbucketCloud', 'bitbucket.org');
const api = createFakeApi({ 'bitbucket.org': integration });
const wrapped = wrapIntegrationsApi(api);

const result = wrapped.byHost('bitbucket.org')!;

expect(result.title).toBe('bitbucketCloud - bitbucket.org');
expect(result.resolveUrl).toBe(integration.resolveUrl);
expect(result.resolveEditUrl).toBe(integration.resolveEditUrl);
});

it('does not remap github integrations', () => {
const integration = makeIntegration('github', 'github.com');
const api = createFakeApi({ 'github.com': integration });
const wrapped = wrapIntegrationsApi(api);

const result = wrapped.byHost('github.com');

expect(result).toBeDefined();
expect(result!.type).toBe('github');
});

it('does not remap gitlab integrations', () => {
const integration = makeIntegration('gitlab', 'gitlab.com');
const api = createFakeApi({ 'gitlab.com': integration });
const wrapped = wrapIntegrationsApi(api);

const result = wrapped.byHost('gitlab.com');

expect(result!.type).toBe('gitlab');
});

it('returns undefined for unknown hosts', () => {
const api = createFakeApi({});
const wrapped = wrapIntegrationsApi(api);

const result = wrapped.byHost('unknown.example.com');

expect(result).toBeUndefined();
});

it('delegates non-byHost properties to the original api', () => {
const integration = makeIntegration('github', 'github.com');
const api = createFakeApi({ 'github.com': integration });
const wrapped = wrapIntegrationsApi(api);

const result = wrapped.list();

expect(api.list).toHaveBeenCalled();
expect(result).toEqual([integration]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Copyright Red Hat, Inc.
*
* 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.
*/

// HACK: Backstage scaffolder >=1.36.1 (commit 527cf88) updated validation.ts
// to check for "bitbucketCloud"/"bitbucketServer" types, but RepoUrlPicker.tsx
// still only checks hostType === "bitbucket". Since
// integrationApi.byHost("bitbucket.org") returns a BitbucketCloudIntegration
// (type "bitbucketCloud"), the BitbucketRepoPicker (workspace/project fields)
// never renders. This wrapper fixes the type mismatch by intercepting the
// scmIntegrationsApi within the component's React context.
//
// Remove once upstream fixes RepoUrlPicker to handle the new type strings.
// See https://redhat.atlassian.net/browse/FLPATH-4033

import { createElement, useMemo } from 'react';
import {
type ApiHolder,
type ApiRef,
getComponentData,
useApi,
useApiHolder,
} from '@backstage/core-plugin-api';
import { ApiProvider } from '@backstage/core-app-api';
import { scmIntegrationsApiRef } from '@backstage/integration-react';
import {
type FieldExtensionComponentProps,
type FieldExtensionOptions,
} from '@backstage/plugin-scaffolder-react';
import { RepoUrlPickerFieldExtension } from '@backstage/plugin-scaffolder';

type IntegrationsApi =
typeof scmIntegrationsApiRef extends ApiRef<infer T> ? T : never;

// Extract the original RepoUrlPicker component from the field extension's
// attached component data. This avoids importing from internal dist paths
// which are blocked by the package's "exports" field.
const FIELD_EXTENSION_KEY = 'scaffolder.extensions.field.v1';
const fieldExtensionData = getComponentData<FieldExtensionOptions>(
createElement(RepoUrlPickerFieldExtension),
FIELD_EXTENSION_KEY,
);

if (!fieldExtensionData?.component) {
throw new Error(
'Failed to extract RepoUrlPicker component from RepoUrlPickerFieldExtension. ' +
'The Backstage scaffolder field extension data key may have changed.',
);
}

const OriginalRepoUrlPicker = fieldExtensionData.component;

const BITBUCKET_TYPES = new Set(['bitbucketCloud', 'bitbucketServer']);

/**
* Wraps the IntegrationsApi so that byHost() remaps "bitbucketCloud" and
* "bitbucketServer" to "bitbucket", making the upstream RepoUrlPicker render
* the BitbucketRepoPicker component.
*/
/** @internal Exported for testing only. */
export function wrapIntegrationsApi(api: IntegrationsApi): IntegrationsApi {
return new Proxy(api, {
get(target, prop, receiver) {
if (prop !== 'byHost') {
return Reflect.get(target, prop, receiver);
}

return (host: string) => {
const integration = target.byHost(host);
if (!integration || !BITBUCKET_TYPES.has(integration.type)) {
return integration;
}

// TODO: Remove this type remapping once upstream decouples

Check warning on line 86 in workspaces/x2a/plugins/x2a/src/scaffolder/RepoUrlPickerWithBitbucketFix.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZ2Rex6qsFKdQ1yTjFEL&open=AZ2Rex6qsFKdQ1yTjFEL&pullRequest=2768
// "bitbucketCloud" / "bitbucketServer" in RepoUrlPicker.
// See https://redhat.atlassian.net/browse/FLPATH-4033
return new Proxy(integration, {
get(innerTarget, innerProp, innerReceiver) {
if (innerProp === 'type') {
return 'bitbucket';
}
Comment on lines +91 to +93
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we assume this part will change / be removed once the bug is resolved, correct? as they may decouple bitbucketCloud and bitbucketServer

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yuup

return Reflect.get(innerTarget, innerProp, innerReceiver);
},
});
};
},
});
}

/**
* Creates an ApiHolder that intercepts scmIntegrationsApiRef to return a
* wrapped version, delegating all other API lookups to the parent holder.
*/
function createWrappedApiHolder(
parentHolder: ApiHolder,
wrappedIntegrations: IntegrationsApi,
): ApiHolder {
return {
get<T>(ref: ApiRef<T>): T | undefined {
if (ref === scmIntegrationsApiRef) {
return wrappedIntegrations as unknown as T;
}
return parentHolder.get(ref);
},
};
}

/**
* Wrapper around Backstage's RepoUrlPicker that fixes the Bitbucket Cloud/Server
* type mismatch. Provides a scoped API context override so the inner
* RepoUrlPicker sees "bitbucket" as the host type and renders the
* BitbucketRepoPicker with workspace/project fields.
*/
export function RepoUrlPickerWithBitbucketFix(
props: FieldExtensionComponentProps<string>,
) {
const parentHolder = useApiHolder();
const integrationsApi = useApi(scmIntegrationsApiRef);

const wrappedHolder = useMemo(
() =>
createWrappedApiHolder(
parentHolder,
wrapIntegrationsApi(integrationsApi),
),
[parentHolder, integrationsApi],
);

return (
<ApiProvider apis={wrappedHolder}>
<OriginalRepoUrlPicker
{...(props as FieldExtensionComponentProps<unknown>)}
/>
</ApiProvider>
);
}
1 change: 1 addition & 0 deletions workspaces/x2a/plugins/x2a/src/scaffolder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
* See the License for the specific language governing permissions and limitations under the License.
*/
export * from './RepoAuthentication';
export { RepoUrlPickerWithBitbucketFix } from './RepoUrlPickerWithBitbucketFix';
Loading
Loading