Merge pull request #27771 from solimant/23819_scaffolder_openapi

Move Scaffolder API to OpenAPI
This commit is contained in:
Ben Lambert
2025-07-29 14:36:47 +02:00
committed by GitHub
147 changed files with 7540 additions and 992 deletions
-3
View File
@@ -79,7 +79,6 @@
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.61",
"@microsoft/fetch-event-source": "^2.0.1",
"@react-hookz/web": "^24.0.0",
"@rjsf/core": "5.23.2",
"@rjsf/material-ui": "5.23.2",
@@ -100,7 +99,6 @@
"react-use": "^17.2.4",
"react-window": "^1.8.10",
"yaml": "^2.0.0",
"zen-observable": "^0.10.0",
"zod": "^3.22.4",
"zod-to-json-schema": "^3.20.4"
},
@@ -120,7 +118,6 @@
"@types/react": "^18.0.0",
"@types/react-resizable": "^3.0.8",
"@types/react-window": "^1.8.8",
"msw": "^1.0.0",
"react": "^18.0.2",
"react-dom": "^18.0.2",
"react-router-dom": "^6.3.0",
+19 -85
View File
@@ -11,47 +11,43 @@ import { createScaffolderFieldExtension as createScaffolderFieldExtension_2 } fr
import { createScaffolderLayout as createScaffolderLayout_2 } from '@backstage/plugin-scaffolder-react';
import { CustomFieldExtensionSchema as CustomFieldExtensionSchema_2 } from '@backstage/plugin-scaffolder-react';
import { CustomFieldValidator as CustomFieldValidator_2 } from '@backstage/plugin-scaffolder-react';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { ExternalRouteRef } from '@backstage/core-plugin-api';
import { FetchApi } from '@backstage/core-plugin-api';
import { FieldExtensionComponent as FieldExtensionComponent_2 } from '@backstage/plugin-scaffolder-react';
import { FieldExtensionComponentProps as FieldExtensionComponentProps_2 } from '@backstage/plugin-scaffolder-react';
import { FieldExtensionOptions as FieldExtensionOptions_2 } from '@backstage/plugin-scaffolder-react';
import { FieldSchema as FieldSchema_2 } from '@backstage/plugin-scaffolder-react';
import { FieldValidation } from '@rjsf/utils';
import { FormProps } from '@backstage/plugin-scaffolder-react';
import { IdentityApi } from '@backstage/core-plugin-api';
import { JSX as JSX_2 } from 'react/jsx-runtime';
import { LayoutOptions as LayoutOptions_2 } from '@backstage/plugin-scaffolder-react';
import { LayoutTemplate as LayoutTemplate_2 } from '@backstage/plugin-scaffolder-react';
import { ListActionsResponse as ListActionsResponse_2 } from '@backstage/plugin-scaffolder-react';
import { ListTemplatingExtensionsResponse } from '@backstage/plugin-scaffolder-react';
import { LogEvent as LogEvent_2 } from '@backstage/plugin-scaffolder-react';
import { Observable } from '@backstage/types';
import { ListActionsResponse as ListActionsResponse_2 } from '@backstage/plugin-scaffolder-common';
import { LogEvent as LogEvent_2 } from '@backstage/plugin-scaffolder-common';
import { PathParams } from '@backstage/core-plugin-api';
import { PropsWithChildren } from 'react';
import { ReactNode } from 'react';
import { ReviewStepProps } from '@backstage/plugin-scaffolder-react';
import { RouteRef } from '@backstage/core-plugin-api';
import { ScaffolderApi as ScaffolderApi_2 } from '@backstage/plugin-scaffolder-react';
import { ScaffolderDryRunOptions as ScaffolderDryRunOptions_2 } from '@backstage/plugin-scaffolder-react';
import { ScaffolderDryRunResponse as ScaffolderDryRunResponse_2 } from '@backstage/plugin-scaffolder-react';
import { ScaffolderGetIntegrationsListOptions as ScaffolderGetIntegrationsListOptions_2 } from '@backstage/plugin-scaffolder-react';
import { ScaffolderGetIntegrationsListResponse as ScaffolderGetIntegrationsListResponse_2 } from '@backstage/plugin-scaffolder-react';
import { ScaffolderOutputLink } from '@backstage/plugin-scaffolder-react';
import { ScaffolderScaffoldOptions as ScaffolderScaffoldOptions_2 } from '@backstage/plugin-scaffolder-react';
import { ScaffolderScaffoldResponse as ScaffolderScaffoldResponse_2 } from '@backstage/plugin-scaffolder-react';
import { ScaffolderStreamLogsOptions as ScaffolderStreamLogsOptions_2 } from '@backstage/plugin-scaffolder-react';
import { ScaffolderTask as ScaffolderTask_2 } from '@backstage/plugin-scaffolder-react';
import { ScaffolderApi as ScaffolderApi_2 } from '@backstage/plugin-scaffolder-common';
import { ScaffolderClient as ScaffolderClient_2 } from '@backstage/plugin-scaffolder-common';
import { ScaffolderDryRunOptions as ScaffolderDryRunOptions_2 } from '@backstage/plugin-scaffolder-common';
import { ScaffolderDryRunResponse as ScaffolderDryRunResponse_2 } from '@backstage/plugin-scaffolder-common';
import { ScaffolderGetIntegrationsListOptions as ScaffolderGetIntegrationsListOptions_2 } from '@backstage/plugin-scaffolder-common';
import { ScaffolderGetIntegrationsListResponse as ScaffolderGetIntegrationsListResponse_2 } from '@backstage/plugin-scaffolder-common';
import { ScaffolderOutputLink } from '@backstage/plugin-scaffolder-common';
import { ScaffolderScaffoldOptions as ScaffolderScaffoldOptions_2 } from '@backstage/plugin-scaffolder-common';
import { ScaffolderScaffoldResponse as ScaffolderScaffoldResponse_2 } from '@backstage/plugin-scaffolder-common';
import { ScaffolderStreamLogsOptions as ScaffolderStreamLogsOptions_2 } from '@backstage/plugin-scaffolder-common';
import { ScaffolderTask as ScaffolderTask_2 } from '@backstage/plugin-scaffolder-common';
import { ScaffolderTaskOutput as ScaffolderTaskOutput_2 } from '@backstage/plugin-scaffolder-react';
import { ScaffolderTaskStatus as ScaffolderTaskStatus_2 } from '@backstage/plugin-scaffolder-react';
import { ScaffolderTaskOutput as ScaffolderTaskOutput_3 } from '@backstage/plugin-scaffolder-common';
import { ScaffolderTaskStatus as ScaffolderTaskStatus_2 } from '@backstage/plugin-scaffolder-common';
import { ScaffolderUseTemplateSecrets as ScaffolderUseTemplateSecrets_2 } from '@backstage/plugin-scaffolder-react';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { SubRouteRef } from '@backstage/core-plugin-api';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { TemplateGroupFilter } from '@backstage/plugin-scaffolder-react';
import { TemplateListPageProps } from '@backstage/plugin-scaffolder/alpha';
import { TemplateParameterSchema as TemplateParameterSchema_2 } from '@backstage/plugin-scaffolder-react';
import { TemplateParameterSchema as TemplateParameterSchema_2 } from '@backstage/plugin-scaffolder-common';
import { TemplateWizardPageProps } from '@backstage/plugin-scaffolder/alpha';
import { z } from 'zod';
@@ -505,70 +501,8 @@ export type ScaffolderApi = ScaffolderApi_2;
// @public @deprecated (undocumented)
export const scaffolderApiRef: ApiRef<ScaffolderApi_2>;
// @public
export class ScaffolderClient implements ScaffolderApi_2 {
constructor(options: {
discoveryApi: DiscoveryApi;
fetchApi: FetchApi;
identityApi?: IdentityApi;
scmIntegrationsApi: ScmIntegrationRegistry;
useLongPollingLogs?: boolean;
});
// (undocumented)
autocomplete({
token,
resource,
provider,
context,
}: {
token: string;
provider: string;
resource: string;
context?: Record<string, string>;
}): Promise<{
results: {
title?: string;
id: string;
}[];
}>;
// (undocumented)
cancelTask(taskId: string): Promise<void>;
// (undocumented)
dryRun(
options: ScaffolderDryRunOptions_2,
): Promise<ScaffolderDryRunResponse_2>;
// (undocumented)
getIntegrationsList(
options: ScaffolderGetIntegrationsListOptions_2,
): Promise<ScaffolderGetIntegrationsListResponse_2>;
// (undocumented)
getTask(taskId: string): Promise<ScaffolderTask_2>;
// (undocumented)
getTemplateParameterSchema(
templateRef: string,
): Promise<TemplateParameterSchema_2>;
// (undocumented)
listActions(): Promise<ListActionsResponse_2>;
// (undocumented)
listTasks(options: {
filterByOwnership: 'owned' | 'all';
limit?: number;
offset?: number;
}): Promise<{
tasks: ScaffolderTask_2[];
totalTasks?: number;
}>;
// (undocumented)
listTemplatingExtensions(): Promise<ListTemplatingExtensionsResponse>;
// (undocumented)
retry?(taskId: string): Promise<void>;
// (undocumented)
scaffold(
options: ScaffolderScaffoldOptions_2,
): Promise<ScaffolderScaffoldResponse_2>;
// (undocumented)
streamLogs(options: ScaffolderStreamLogsOptions_2): Observable<LogEvent_2>;
}
// @public @deprecated
export class ScaffolderClient extends ScaffolderClient_2 {}
// @public @deprecated (undocumented)
export type ScaffolderDryRunOptions = ScaffolderDryRunOptions_2;
@@ -644,7 +578,7 @@ export type ScaffolderStreamLogsOptions = ScaffolderStreamLogsOptions_2;
export type ScaffolderTask = ScaffolderTask_2;
// @public @deprecated (undocumented)
export type ScaffolderTaskOutput = ScaffolderTaskOutput_2;
export type ScaffolderTaskOutput = ScaffolderTaskOutput_3;
// @public @deprecated (undocumented)
export type ScaffolderTaskStatus = ScaffolderTaskStatus_2;
@@ -118,7 +118,7 @@ async function downloadDirectoryContents(
directoryContents: {
path: string;
base64Content: string;
executable: boolean;
executable?: boolean;
}[],
name: string,
) {
-428
View File
@@ -1,428 +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/core-app-api';
import { ScmIntegrations } from '@backstage/integration';
import { MockFetchApi, registerMswTestHooks } from '@backstage/test-utils';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { ScaffolderClient } from './api';
import { fetchEventSource } from '@microsoft/fetch-event-source';
jest.mock('@microsoft/fetch-event-source');
const mockFetchEventSource = fetchEventSource as jest.MockedFunction<
typeof fetchEventSource
>;
const server = setupServer();
describe('api', () => {
registerMswTestHooks(server);
const mockBaseUrl = 'http://backstage/api';
const discoveryApi = { getBaseUrl: async () => mockBaseUrl };
const fetchApi = new MockFetchApi();
const identityApi = {
getBackstageIdentity: jest.fn(),
getProfileInfo: jest.fn(),
getCredentials: jest.fn(),
signOut: jest.fn(),
};
const scmIntegrationsApi = ScmIntegrations.fromConfig(
new ConfigReader({
integrations: {
github: [
{
host: 'hello.com',
},
],
},
}),
);
let apiClient: ScaffolderClient;
beforeEach(() => {
apiClient = new ScaffolderClient({
scmIntegrationsApi,
discoveryApi,
fetchApi,
identityApi,
});
jest.restoreAllMocks();
identityApi.getBackstageIdentity.mockReturnValue({});
});
it('should return default and custom integrations', async () => {
const allowedHosts = [
'hello.com',
'gitlab.com',
'github.com',
'dev.azure.com',
'bitbucket.org',
];
const { integrations } = await apiClient.getIntegrationsList({
allowedHosts,
});
integrations.forEach(integration =>
expect(allowedHosts).toContain(integration.host),
);
});
describe('streamEvents', () => {
describe('eventsource', () => {
it('should work', async () => {
mockFetchEventSource.mockImplementation(async (_url, options) => {
const { onopen, onmessage } = options;
await Promise.resolve();
await onopen?.({ ok: true } as Response);
await Promise.resolve();
onmessage?.({
id: '',
event: 'log',
data: '{"id":1,"taskId":"a-random-id","type":"log","createdAt":"","body":{"message":"My log message"}}',
});
await Promise.resolve();
onmessage?.({
id: '',
event: 'completion',
data: '{"id":2,"taskId":"a-random-id","type":"completion","createdAt":"","body":{"message":"Finished!"}}',
});
});
const next = jest.fn();
await new Promise<void>(complete => {
apiClient
.streamLogs({ taskId: 'a-random-task-id' })
.subscribe({ next, complete });
});
expect(mockFetchEventSource).toHaveBeenCalledWith(
'http://backstage/api/v2/tasks/a-random-task-id/eventstream',
{
fetch: fetchApi.fetch,
onmessage: expect.any(Function),
onerror: expect.any(Function),
signal: expect.any(AbortSignal),
},
);
expect(next).toHaveBeenCalledTimes(2);
expect(next).toHaveBeenCalledWith({
id: 1,
taskId: 'a-random-id',
type: 'log',
createdAt: '',
body: { message: 'My log message' },
});
expect(next).toHaveBeenCalledWith({
id: 2,
taskId: 'a-random-id',
type: 'completion',
createdAt: '',
body: { message: 'Finished!' },
});
});
});
describe('longPolling', () => {
beforeEach(() => {
apiClient = new ScaffolderClient({
scmIntegrationsApi,
discoveryApi,
fetchApi,
identityApi,
useLongPollingLogs: true,
});
});
it('should work', async () => {
server.use(
rest.get(
`${mockBaseUrl}/v2/tasks/:taskId/events`,
(req, res, ctx) => {
const { taskId } = req.params;
const after = req.url.searchParams.get('after');
if (taskId === 'a-random-task-id') {
if (!after) {
return res(
ctx.json([
{
id: 1,
taskId: 'a-random-id',
type: 'log',
createdAt: '',
body: { message: 'My log message' },
},
]),
);
} else if (after === '1') {
return res(
ctx.json([
{
id: 2,
taskId: 'a-random-id',
type: 'completion',
createdAt: '',
body: { message: 'Finished!' },
},
]),
);
}
}
return res(ctx.status(500));
},
),
);
const next = jest.fn();
await new Promise<void>(complete =>
apiClient
.streamLogs({ taskId: 'a-random-task-id' })
.subscribe({ next, complete }),
);
expect(next).toHaveBeenCalledTimes(2);
expect(next).toHaveBeenCalledWith({
id: 1,
taskId: 'a-random-id',
type: 'log',
createdAt: '',
body: { message: 'My log message' },
});
expect(next).toHaveBeenCalledWith({
id: 2,
taskId: 'a-random-id',
type: 'completion',
createdAt: '',
body: { message: 'Finished!' },
});
});
it('should unsubscribe', async () => {
expect.assertions(3);
server.use(
rest.get(
`${mockBaseUrl}/v2/tasks/:taskId/events`,
(req, res, ctx) => {
const { taskId } = req.params;
const after = req.url.searchParams.get('after');
// use assertion to make sure it is not called after unsubscribing
expect(after).toBe(null);
if (taskId === 'a-random-task-id') {
return res(
ctx.json([
{
id: 1,
taskId: 'a-random-id',
type: 'log',
createdAt: '',
body: { message: 'My log message' },
},
]),
);
}
return res(ctx.status(500));
},
),
);
const next = jest.fn();
await new Promise<void>(complete => {
const subscription = apiClient
.streamLogs({ taskId: 'a-random-task-id' })
.subscribe({
next: (...args) => {
next(...args);
subscription.unsubscribe();
complete();
},
});
});
expect(next).toHaveBeenCalledTimes(1);
expect(next).toHaveBeenCalledWith({
id: 1,
taskId: 'a-random-id',
type: 'log',
createdAt: '',
body: { message: 'My log message' },
});
});
it('should continue after error', async () => {
const called = jest.fn();
server.use(
rest.get(
`${mockBaseUrl}/v2/tasks/:taskId/events`,
(_req, res, ctx) => {
called();
if (called.mock.calls.length > 1) {
return res(
ctx.json([
{
id: 2,
taskId: 'a-random-id',
type: 'completion',
createdAt: '',
body: { message: 'Finished!' },
},
]),
);
}
return res(ctx.status(500));
},
),
);
const next = jest.fn();
await new Promise<void>(complete =>
apiClient
.streamLogs({ taskId: 'a-random-task-id' })
.subscribe({ next, complete }),
);
expect(called).toHaveBeenCalledTimes(2);
expect(next).toHaveBeenCalledTimes(1);
expect(next).toHaveBeenCalledWith({
id: 2,
taskId: 'a-random-id',
type: 'completion',
createdAt: '',
body: { message: 'Finished!' },
});
});
});
});
describe('listTasks', () => {
it('should list all tasks', async () => {
server.use(
rest.get(`${mockBaseUrl}/v2/tasks`, (req, res, ctx) => {
const createdBy = req.url.searchParams.get('createdBy');
if (createdBy) {
return res(
ctx.json([
{
createdBy,
},
]),
);
}
return res(
ctx.json([
{
createdBy: null,
},
{
createdBy: null,
},
]),
);
}),
);
const result = await apiClient.listTasks({ filterByOwnership: 'all' });
expect(result).toHaveLength(2);
});
it('should list tasks with limit and offset', async () => {
server.use(
rest.get(
`${mockBaseUrl}/v2/tasks?limit=5&offset=0`,
(_req, res, ctx) => {
return res(
ctx.json([
{
createdBy: null,
},
{
createdBy: null,
},
]),
);
},
),
);
const result = await apiClient.listTasks({
filterByOwnership: 'all',
limit: 5,
offset: 0,
});
expect(result).toHaveLength(2);
});
it('should list task using the current user as owner', async () => {
server.use(
rest.get(`${mockBaseUrl}/v2/tasks`, (req, res, ctx) => {
const createdBy = req.url.searchParams.get('createdBy');
if (createdBy) {
return res(
ctx.json({
tasks: [
{
createdBy,
},
],
}),
);
}
return res(
ctx.json({
tasks: [
{
createdBy: null,
},
{
createdBy: null,
},
],
}),
);
}),
);
identityApi.getBackstageIdentity.mockResolvedValueOnce({
userEntityRef: 'user:default/foo',
});
const result = await apiClient.listTasks({ filterByOwnership: 'owned' });
expect(identityApi.getBackstageIdentity).toHaveBeenCalled();
expect(result.tasks).toHaveLength(1);
});
});
});
+3 -380
View File
@@ -14,389 +14,12 @@
* limitations under the License.
*/
import { parseEntityRef } from '@backstage/catalog-model';
import {
DiscoveryApi,
FetchApi,
IdentityApi,
} from '@backstage/core-plugin-api';
import { ResponseError } from '@backstage/errors';
import { ScmIntegrationRegistry } from '@backstage/integration';
import {
ListActionsResponse,
ListTemplatingExtensionsResponse,
LogEvent,
ScaffolderApi,
ScaffolderDryRunOptions,
ScaffolderDryRunResponse,
ScaffolderGetIntegrationsListOptions,
ScaffolderGetIntegrationsListResponse,
ScaffolderScaffoldOptions,
ScaffolderScaffoldResponse,
ScaffolderStreamLogsOptions,
ScaffolderTask,
TemplateParameterSchema,
} from '@backstage/plugin-scaffolder-react';
import { Observable } from '@backstage/types';
import {
EventSourceMessage,
fetchEventSource,
} from '@microsoft/fetch-event-source';
import { default as qs, default as queryString } from 'qs';
import ObservableImpl from 'zen-observable';
import { ScaffolderClient as _ScaffolderClient } from '@backstage/plugin-scaffolder-common';
/**
* An API to interact with the scaffolder backend.
*
* @public
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderClient} instead as this has now been moved.
*/
export class ScaffolderClient implements ScaffolderApi {
private readonly discoveryApi: DiscoveryApi;
private readonly scmIntegrationsApi: ScmIntegrationRegistry;
private readonly fetchApi: FetchApi;
private readonly identityApi?: IdentityApi;
private readonly useLongPollingLogs: boolean;
constructor(options: {
discoveryApi: DiscoveryApi;
fetchApi: FetchApi;
identityApi?: IdentityApi;
scmIntegrationsApi: ScmIntegrationRegistry;
useLongPollingLogs?: boolean;
}) {
this.discoveryApi = options.discoveryApi;
this.fetchApi = options.fetchApi ?? { fetch };
this.scmIntegrationsApi = options.scmIntegrationsApi;
this.useLongPollingLogs = options.useLongPollingLogs ?? false;
this.identityApi = options.identityApi;
}
async listTasks(options: {
filterByOwnership: 'owned' | 'all';
limit?: number;
offset?: number;
}): Promise<{ tasks: ScaffolderTask[]; totalTasks?: number }> {
if (!this.identityApi) {
throw new Error(
'IdentityApi is not available in the ScaffolderClient, please pass through the IdentityApi to the ScaffolderClient constructor in order to use the listTasks method',
);
}
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
const { userEntityRef } = await this.identityApi.getBackstageIdentity();
const query = queryString.stringify({
createdBy:
options.filterByOwnership === 'owned' ? userEntityRef : undefined,
limit: options.limit,
offset: options.offset,
});
const response = await this.fetchApi.fetch(`${baseUrl}/v2/tasks?${query}`);
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
return await response.json();
}
async getIntegrationsList(
options: ScaffolderGetIntegrationsListOptions,
): Promise<ScaffolderGetIntegrationsListResponse> {
const integrations = [
...this.scmIntegrationsApi.azure.list(),
...this.scmIntegrationsApi.bitbucket
.list()
.filter(
item =>
!this.scmIntegrationsApi.bitbucketCloud.byHost(item.config.host) &&
!this.scmIntegrationsApi.bitbucketServer.byHost(item.config.host),
),
...this.scmIntegrationsApi.bitbucketCloud.list(),
...this.scmIntegrationsApi.bitbucketServer.list(),
...this.scmIntegrationsApi.gerrit.list(),
...this.scmIntegrationsApi.gitea.list(),
...this.scmIntegrationsApi.github.list(),
...this.scmIntegrationsApi.gitlab.list(),
]
.map(c => ({ type: c.type, title: c.title, host: c.config.host }))
.filter(c => options.allowedHosts.includes(c.host));
return {
integrations,
};
}
async getTemplateParameterSchema(
templateRef: string,
): Promise<TemplateParameterSchema> {
const { namespace, kind, name } = parseEntityRef(templateRef, {
defaultKind: 'template',
});
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
const templatePath = [namespace, kind, name]
.map(s => encodeURIComponent(s))
.join('/');
const url = `${baseUrl}/v2/templates/${templatePath}/parameter-schema`;
const response = await this.fetchApi.fetch(url);
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
const schema: TemplateParameterSchema = await response.json();
return schema;
}
async scaffold(
options: ScaffolderScaffoldOptions,
): Promise<ScaffolderScaffoldResponse> {
const { templateRef, values, secrets = {} } = options;
const url = `${await this.discoveryApi.getBaseUrl('scaffolder')}/v2/tasks`;
const response = await this.fetchApi.fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
templateRef,
values: { ...values },
secrets,
}),
});
if (response.status !== 201) {
const status = `${response.status} ${response.statusText}`;
const body = await response.text();
throw new Error(`Backend request failed, ${status} ${body.trim()}`);
}
const { id } = (await response.json()) as { id: string };
return { taskId: id };
}
async getTask(taskId: string): Promise<ScaffolderTask> {
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
const url = `${baseUrl}/v2/tasks/${encodeURIComponent(taskId)}`;
const response = await this.fetchApi.fetch(url);
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
return await response.json();
}
streamLogs(options: ScaffolderStreamLogsOptions): Observable<LogEvent> {
if (this.useLongPollingLogs) {
return this.streamLogsPolling(options);
}
return this.streamLogsEventStream(options);
}
async dryRun(
options: ScaffolderDryRunOptions,
): Promise<ScaffolderDryRunResponse> {
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
const response = await this.fetchApi.fetch(`${baseUrl}/v2/dry-run`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
template: options.template,
values: options.values,
secrets: options.secrets,
directoryContents: options.directoryContents,
}),
});
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
return response.json();
}
private streamLogsEventStream({
isTaskRecoverable,
taskId,
after,
}: ScaffolderStreamLogsOptions): Observable<LogEvent> {
return new ObservableImpl(subscriber => {
const params = new URLSearchParams();
if (after !== undefined) {
params.set('after', String(Number(after)));
}
this.discoveryApi.getBaseUrl('scaffolder').then(
baseUrl => {
const url = `${baseUrl}/v2/tasks/${encodeURIComponent(
taskId,
)}/eventstream`;
const processEvent = (event: any) => {
if (event.data) {
try {
subscriber.next(JSON.parse(event.data));
} catch (ex) {
subscriber.error(ex);
}
}
};
const ctrl = new AbortController();
void fetchEventSource(url, {
fetch: this.fetchApi.fetch,
signal: ctrl.signal,
onmessage(e: EventSourceMessage) {
if (e.event === 'log') {
processEvent(e);
return;
} else if (e.event === 'completion' && !isTaskRecoverable) {
processEvent(e);
subscriber.complete();
ctrl.abort();
return;
}
processEvent(e);
},
onerror(err) {
subscriber.error(err);
},
});
},
error => {
subscriber.error(error);
},
);
});
}
private streamLogsPolling({
taskId,
after: inputAfter,
}: {
taskId: string;
after?: number;
}): Observable<LogEvent> {
let after = inputAfter;
return new ObservableImpl(subscriber => {
this.discoveryApi.getBaseUrl('scaffolder').then(async baseUrl => {
while (!subscriber.closed) {
const url = `${baseUrl}/v2/tasks/${encodeURIComponent(
taskId,
)}/events?${qs.stringify({ after })}`;
const response = await this.fetchApi.fetch(url);
if (!response.ok) {
// wait for one second to not run into an
await new Promise(resolve => setTimeout(resolve, 1000));
continue;
}
const logs = (await response.json()) as LogEvent[];
for (const event of logs) {
after = Number(event.id);
subscriber.next(event);
if (event.type === 'completion') {
subscriber.complete();
return;
}
}
}
});
});
}
async listActions(): Promise<ListActionsResponse> {
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
const response = await this.fetchApi.fetch(`${baseUrl}/v2/actions`);
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
return await response.json();
}
async listTemplatingExtensions(): Promise<ListTemplatingExtensionsResponse> {
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
const response = await this.fetchApi.fetch(
`${baseUrl}/v2/templating-extensions`,
);
if (!response.ok) {
throw ResponseError.fromResponse(response);
}
return response.json();
}
async cancelTask(taskId: string): Promise<void> {
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
const url = `${baseUrl}/v2/tasks/${encodeURIComponent(taskId)}/cancel`;
const response = await this.fetchApi.fetch(url, {
method: 'POST',
});
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
return await response.json();
}
async retry?(taskId: string): Promise<void> {
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
const url = `${baseUrl}/v2/tasks/${encodeURIComponent(taskId)}/retry`;
const response = await this.fetchApi.fetch(url, {
method: 'POST',
});
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
return await response.json();
}
async autocomplete({
token,
resource,
provider,
context,
}: {
token: string;
provider: string;
resource: string;
context?: Record<string, string>;
}): Promise<{ results: { title?: string; id: string }[] }> {
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
const url = `${baseUrl}/v2/autocomplete/${provider}/${resource}`;
const response = await this.fetchApi.fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
token,
context: context ?? {},
}),
});
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
const { results } = await response.json();
return { results };
}
}
export class ScaffolderClient extends _ScaffolderClient {}
@@ -85,6 +85,7 @@ export const BitbucketRepoPicker = (
scaffolderApi
.autocomplete({
context: {},
token: accessToken,
resource: 'workspaces',
provider: 'bitbucket-cloud',
+32 -29
View File
@@ -21,22 +21,8 @@
*/
import {
createScaffolderFieldExtension as createScaffolderFieldExtensionTemp,
ScaffolderFieldExtensions as ScaffolderFieldExtensionsTemp,
useTemplateSecrets as useTemplateSecretsTemp,
scaffolderApiRef as scaffolderApiRefTemp,
createScaffolderLayout as createScaffolderLayoutTemp,
ScaffolderLayouts as ScaffolderLayoutsTemp,
type LayoutOptions as LayoutOptionsTemp,
type LayoutTemplate as LayoutTemplateTemp,
type ScaffolderApi as ScaffolderApiTemp,
type ScaffolderUseTemplateSecrets as ScaffolderUseTemplateSecretsTemp,
type TemplateParameterSchema as TemplateParameterSchemaTemp,
type CustomFieldExtensionSchema as CustomFieldExtensionSchemaTemp,
type CustomFieldValidator as CustomFieldValidatorTemp,
type FieldExtensionOptions as FieldExtensionOptionsTemp,
type FieldExtensionComponentProps as FieldExtensionComponentPropsTemp,
type FieldExtensionComponent as FieldExtensionComponentTemp,
type ListActionsResponse as ListActionsResponseTemp,
type LogEvent as LogEventTemp,
type ScaffolderDryRunOptions as ScaffolderDryRunOptionsTemp,
@@ -50,6 +36,23 @@ import {
type ScaffolderTask as ScaffolderTaskTemp,
type ScaffolderTaskOutput as ScaffolderTaskOutputTemp,
type ScaffolderTaskStatus as ScaffolderTaskStatusTemp,
} from '@backstage/plugin-scaffolder-common';
import {
createScaffolderFieldExtension as createScaffolderFieldExtensionTemp,
ScaffolderFieldExtensions as ScaffolderFieldExtensionsTemp,
useTemplateSecrets as useTemplateSecretsTemp,
scaffolderApiRef as scaffolderApiRefTemp,
createScaffolderLayout as createScaffolderLayoutTemp,
ScaffolderLayouts as ScaffolderLayoutsTemp,
type LayoutOptions as LayoutOptionsTemp,
type LayoutTemplate as LayoutTemplateTemp,
type ScaffolderUseTemplateSecrets as ScaffolderUseTemplateSecretsTemp,
type CustomFieldExtensionSchema as CustomFieldExtensionSchemaTemp,
type CustomFieldValidator as CustomFieldValidatorTemp,
type FieldExtensionOptions as FieldExtensionOptionsTemp,
type FieldExtensionComponentProps as FieldExtensionComponentPropsTemp,
type FieldExtensionComponent as FieldExtensionComponentTemp,
} from '@backstage/plugin-scaffolder-react';
import { rootRouteRef as rootRouteRefTemp } from './routes';
@@ -82,7 +85,7 @@ export const useTemplateSecrets = useTemplateSecretsTemp;
export const scaffolderApiRef = scaffolderApiRefTemp;
/**
* @public
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderApi} instead as this has now been moved.
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderApi} instead as this has now been moved.
*/
export type ScaffolderApi = ScaffolderApiTemp;
/**
@@ -92,7 +95,7 @@ export type ScaffolderApi = ScaffolderApiTemp;
export type ScaffolderUseTemplateSecrets = ScaffolderUseTemplateSecretsTemp;
/**
* @public
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#TemplateParameterSchema} instead as this has now been moved.
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#TemplateParameterSchema} instead as this has now been moved.
*/
export type TemplateParameterSchema = TemplateParameterSchemaTemp;
/**
@@ -127,69 +130,69 @@ export type FieldExtensionComponent<_TReturnValue, _TInputProps> =
FieldExtensionComponentTemp<_TReturnValue, _TInputProps>;
/**
* @public
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ListActionsResponse} instead as this has now been moved.
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ListActionsResponse} instead as this has now been moved.
*/
export type ListActionsResponse = ListActionsResponseTemp;
/**
* @public
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#LogEvent} instead as this has now been moved.
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#LogEvent} instead as this has now been moved.
*/
export type LogEvent = LogEventTemp;
/**
* @public
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderDryRunOptions} instead as this has now been moved.
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderDryRunOptions} instead as this has now been moved.
*/
export type ScaffolderDryRunOptions = ScaffolderDryRunOptionsTemp;
/**
* @public
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderDryRunResponse} instead as this has now been moved.
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderDryRunResponse} instead as this has now been moved.
*/
export type ScaffolderDryRunResponse = ScaffolderDryRunResponseTemp;
/**
* @public
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderGetIntegrationsListOptions} instead as this has now been moved.
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderGetIntegrationsListOptions} instead as this has now been moved.
*/
export type ScaffolderGetIntegrationsListOptions =
ScaffolderGetIntegrationsListOptionsTemp;
/**
* @public
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderGetIntegrationsListResponse} instead as this has now been moved.
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderGetIntegrationsListResponse} instead as this has now been moved.
*/
export type ScaffolderGetIntegrationsListResponse =
ScaffolderGetIntegrationsListResponseTemp;
/**
* @public
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderOutputlink} instead as this has now been moved.
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderOutputlink} instead as this has now been moved.
*/
export type ScaffolderOutputlink = ScaffolderOutputLinkTemp;
/**
* @public
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderScaffoldOptions} instead as this has now been moved.
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderScaffoldOptions} instead as this has now been moved.
*/
export type ScaffolderScaffoldOptions = ScaffolderScaffoldOptionsTemp;
/**
* @public
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderScaffoldResponse} instead as this has now been moved.
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderScaffoldResponse} instead as this has now been moved.
*/
export type ScaffolderScaffoldResponse = ScaffolderScaffoldResponseTemp;
/**
* @public
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderStreamLogsOptions} instead as this has now been moved.
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderStreamLogsOptions} instead as this has now been moved.
*/
export type ScaffolderStreamLogsOptions = ScaffolderStreamLogsOptionsTemp;
/**
* @public
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderTask} instead as this has now been moved.
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderTask} instead as this has now been moved.
*/
export type ScaffolderTask = ScaffolderTaskTemp;
/**
* @public
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderTaskOutput} instead as this has now been moved.
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderTaskOutput} instead as this has now been moved.
*/
export type ScaffolderTaskOutput = ScaffolderTaskOutputTemp;
/**
* @public
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderTaskStatus} instead as this has now been moved.
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderTaskStatus} instead as this has now been moved.
*/
export type ScaffolderTaskStatus = ScaffolderTaskStatusTemp;
/**