diff --git a/.changeset/cool-ways-battle.md b/.changeset/cool-ways-battle.md new file mode 100644 index 0000000000..383e2879c0 --- /dev/null +++ b/.changeset/cool-ways-battle.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-scaffolder-common': patch +--- + +**DEPRECATED** - The `TaskSpec.metadata` and `TaskSpec.baseUrl` has been deprecated in favour of the new `TaskSpec.templateInfo`. +The `baseUrl` is now found on the `templateInfo` object, and the name can be inferred from the `templateInfo.entityRef` property. + +Usages of `TaskSpec.metadata.name` or `ctx.metadata.name` in Actions should migrate to using `parseEntityRef(taskSpec.templateInfo.entityRef)` to get the parsed entity triplet. + +Usages of `ctx.baseUrl` in Actions should migrate to using `ctx.templateInfo.baseUrl` instead. diff --git a/.changeset/purple-camels-accept.md b/.changeset/purple-camels-accept.md new file mode 100644 index 0000000000..fe3da66a74 --- /dev/null +++ b/.changeset/purple-camels-accept.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +'@backstage/plugin-scaffolder-backend-module-rails': patch +--- + +Migrated over from the deprecated `spec.metadata` to `spec.templateInfo` for the `name` and the `baseUrl` of the template. diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts index bba51e3f01..2c1ecd06f9 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts @@ -89,7 +89,10 @@ describe('fetch:cookiecutter', () => { help: 'me', }, }, - baseUrl: 'somebase', + templateInfo: { + entityRef: 'template:default/cookiecutter', + baseUrl: 'somebase', + }, workspacePath: mockTmpDir, logger: getVoidLogger(), logStream: new PassThrough(), @@ -149,7 +152,7 @@ describe('fetch:cookiecutter', () => { expect.objectContaining({ reader: mockReader, integrations, - baseUrl: mockContext.baseUrl, + baseUrl: mockContext.templateInfo?.baseUrl, fetchUrl: mockContext.input.url, outputPath: join( mockTmpDir, diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts index c4d7f1e3e8..0455f01989 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts @@ -222,7 +222,7 @@ export function createFetchCookiecutterAction(options: { await fetchContents({ reader, integrations, - baseUrl: ctx.baseUrl, + baseUrl: ctx.templateInfo?.baseUrl, fetchUrl: ctx.input.url, outputPath: templateContentsDir, }); diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts index 0c7552f72f..0a6a996aa8 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts @@ -62,7 +62,10 @@ describe('fetch:rails', () => { help: 'me', }, }, - baseUrl: 'somebase', + templateInfo: { + baseUrl: 'somebase', + entityRef: 'template:default/myTemplate', + }, workspacePath: mockTmpDir, logger: getVoidLogger(), logStream: new PassThrough(), @@ -100,7 +103,7 @@ describe('fetch:rails', () => { expect(fetchContents).toHaveBeenCalledWith({ reader: mockReader, integrations, - baseUrl: mockContext.baseUrl, + baseUrl: mockContext.templateInfo.baseUrl, fetchUrl: mockContext.input.url, outputPath: resolvePath(mockContext.workspacePath), }); diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts index 2865361a8b..132929ee1c 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts @@ -170,7 +170,7 @@ export function createFetchRailsAction(options: { await fetchContents({ reader, integrations, - baseUrl: ctx.baseUrl, + baseUrl: ctx.templateInfo?.baseUrl, fetchUrl: ctx.input.url, outputPath: workDir, }); diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index b4db89b8eb..14b422048d 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -29,6 +29,7 @@ import { SpawnOptionsWithoutStdio } from 'child_process'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { TaskSpecV1beta2 } from '@backstage/plugin-scaffolder-common'; import { TaskSpecV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; import { TemplateMetadata } from '@backstage/plugin-scaffolder-common'; import { UrlReader } from '@backstage/backend-common'; import { Writable } from 'stream'; @@ -46,6 +47,7 @@ export type ActionContext = { output(name: string, value: JsonValue): void; createTemporaryDirectory(): Promise; metadata?: TemplateMetadata; + templateInfo?: TemplateInfo; }; // @public diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts index 9cac6d5ab8..17f49a5bd4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts @@ -59,7 +59,7 @@ export function createFetchPlainAction(options: { await fetchContents({ reader, integrations, - baseUrl: ctx.baseUrl, + baseUrl: ctx.templateInfo?.baseUrl, fetchUrl: ctx.input.url, outputPath, }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts index 2f9bfd3796..8f641fe2e2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts @@ -75,7 +75,10 @@ describe('fetch:template', () => { const logger = getVoidLogger(); const mockContext = (inputPatch: Partial = {}) => ({ - baseUrl: 'base-url', + templateInfo: { + baseUrl: 'base-url', + entityRef: 'template:default/test-template', + }, input: { url: './skeleton', targetPath: './target', @@ -199,7 +202,7 @@ describe('fetch:template', () => { it('uses fetchContents to retrieve the template content', () => { expect(mockFetchContents).toHaveBeenCalledWith( expect.objectContaining({ - baseUrl: context.baseUrl, + baseUrl: context.templateInfo?.baseUrl, fetchUrl: context.input.url, }), ); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts index 08dfc46caf..a8ba9ac19f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts @@ -135,7 +135,7 @@ export function createFetchTemplateAction(options: { await fetchContents({ reader, integrations, - baseUrl: ctx.baseUrl, + baseUrl: ctx.templateInfo?.baseUrl, fetchUrl: ctx.input.url, outputPath: templateDir, }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts index 6008e8183f..b2227426b8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts @@ -19,10 +19,12 @@ import { Writable } from 'stream'; import { JsonValue, JsonObject } from '@backstage/types'; import { Schema } from 'jsonschema'; import { TaskSecrets, TemplateMetadata } from '../tasks/types'; +import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; export type ActionContext = { /** * Base URL for the location of the task spec, typically the url of the source entity file. + * @deprecated please use templateInfo.baseUrl instead */ baseUrl?: string; @@ -38,7 +40,11 @@ export type ActionContext = { */ createTemporaryDirectory(): Promise; + /** + * @deprecated please use templateInfo instead + */ metadata?: TemplateMetadata; + templateInfo?: TemplateInfo; }; export type TemplateAction = { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.test.ts index 9b05c43831..9f4135b3d1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.test.ts @@ -94,7 +94,7 @@ describe('LegacyWorkflowRunner', () => { }); it('should pass metadata through', async () => { - const templateName = 'template name'; + const entityRef = `template:default/templateName`; const task = createMockTaskWithSpec({ apiVersion: 'backstage.io/v1beta2', steps: [ @@ -107,13 +107,13 @@ describe('LegacyWorkflowRunner', () => { ], output: {}, values: {}, - metadata: { name: templateName }, + templateInfo: { entityRef }, }); await runner.execute(task); - expect(fakeActionHandler.mock.calls[0][0].metadata).toEqual({ - name: templateName, + expect(fakeActionHandler.mock.calls[0][0].templateInfo).toEqual({ + entityRef, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts index 86e719c828..e525e6bd14 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts @@ -225,13 +225,8 @@ export class HandlebarsWorkflowRunner implements WorkflowRunner { input: JSON.stringify(input, null, 2), }); - if (!task.spec.metadata) { - console.warn( - 'DEPRECATION NOTICE: metadata is undefined. metadata will be required in the future.', - ); - } - await action.handler({ + // deprecated in favor of templateInfo.baseUrl baseUrl: task.spec.baseUrl, logger: taskLogger, logStream: stream, @@ -248,7 +243,9 @@ export class HandlebarsWorkflowRunner implements WorkflowRunner { output(name: string, value: JsonValue) { stepOutputs[name] = value; }, + // deprecated in favor of templateInfo metadata: task.spec.metadata, + templateInfo: task.spec.templateInfo, }); // Remove all temporary directories that were created when executing the action diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index f46d969eb8..b66c69ee17 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -163,7 +163,7 @@ describe('DefaultWorkflowRunner', () => { }); it('should pass metadata through', async () => { - const templateName = 'template name'; + const entityRef = `template:default/templateName`; const task = createMockTaskWithSpec({ apiVersion: 'scaffolder.backstage.io/v1beta3', parameters: {}, @@ -176,13 +176,13 @@ describe('DefaultWorkflowRunner', () => { input: { foo: 1 }, }, ], - metadata: { name: templateName }, + templateInfo: { entityRef }, }); await runner.execute(task); - expect(fakeActionHandler.mock.calls[0][0].metadata).toEqual({ - name: templateName, + expect(fakeActionHandler.mock.calls[0][0].templateInfo).toEqual({ + entityRef, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 5d39d0bc00..b1177ca49c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -259,6 +259,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { const stepOutput: { [outputName: string]: JsonValue } = {}; await action.handler({ + // deprecated in favourof templateInfo.baseUrl baseUrl: task.spec.baseUrl, input, secrets: task.secrets ?? {}, @@ -275,7 +276,9 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { output(name: string, value: JsonValue) { stepOutput[name] = value; }, + // deprecated in favour of templateInfo metadata: task.spec.metadata, + templateInfo: task.spec.templateInfo, }); // Remove all temporary directories that were created when executing the action diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index b2978441bd..8a3645cf31 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -28,6 +28,8 @@ import { ScmIntegrations } from '@backstage/integration'; import { TemplateEntityV1beta2, TemplateEntityV1beta3, + TaskSpecV1beta3, + TaskSpecV1beta2, } from '@backstage/plugin-scaffolder-common'; import express from 'express'; import Router from 'express-promise-router'; @@ -186,12 +188,15 @@ export async function createRouter( }) .post('/v2/tasks', async (req, res) => { const templateName: string = req.body.templateName; + const { kind, namespace } = { kind: 'template', namespace: 'default' }; const values = req.body.values; const token = getBearerToken(req.headers.authorization); const template = await findTemplate({ catalogApi: catalogClient, entityRef: { name: templateName, + kind, + namespace, }, token: getBearerToken(req.headers.authorization), }); @@ -219,32 +224,40 @@ export async function createRouter( const baseUrl = getEntityBaseUrl(template); + const baseTaskSpec = { + baseUrl, + steps: template.spec.steps.map((step, index) => ({ + ...step, + id: step.id ?? `step-${index + 1}`, + name: step.name ?? step.action, + })), + output: template.spec.output ?? {}, + + // deprecated in favour of templateInfo + metadata: { name: template.metadata?.name }, + + templateInfo: { + entityRef: stringifyEntityRef({ + kind, + namespace, + name: template.metadata?.name, + }), + baseUrl, + }, + }; + taskSpec = template.apiVersion === 'backstage.io/v1beta2' - ? { + ? ({ + ...baseTaskSpec, apiVersion: template.apiVersion, - baseUrl, values, - steps: template.spec.steps.map((step, index) => ({ - ...step, - id: step.id ?? `step-${index + 1}`, - name: step.name ?? step.action, - })), - output: template.spec.output ?? {}, - metadata: { name: template.metadata?.name }, - } - : { + } as TaskSpecV1beta2) + : ({ + ...baseTaskSpec, apiVersion: template.apiVersion, - baseUrl, parameters: values, - steps: template.spec.steps.map((step, index) => ({ - ...step, - id: step.id ?? `step-${index + 1}`, - name: step.name ?? step.action, - })), - output: template.spec.output ?? {}, - metadata: { name: template.metadata?.name }, - }; + } as TaskSpecV1beta3); } else { throw new InputError( `Unsupported apiVersion field in schema entity, ${ @@ -256,8 +269,6 @@ export async function createRouter( const result = await taskBroker.dispatch(taskSpec, { ...req.body.secrets, backstageToken: token, - // This is deprecated, but we need to support it for now if people are running their own task broker. - token: token, }); res.status(201).json({ id: result.taskId }); diff --git a/plugins/scaffolder-common/api-report.md b/plugins/scaffolder-common/api-report.md index 8d2324a2dd..1872aa3ed8 100644 --- a/plugins/scaffolder-common/api-report.md +++ b/plugins/scaffolder-common/api-report.md @@ -15,9 +15,9 @@ export type TaskSpec = TaskSpecV1beta2 | TaskSpecV1beta3; export interface TaskSpecV1beta2 { // (undocumented) apiVersion: 'backstage.io/v1beta2'; - // (undocumented) + // @deprecated (undocumented) baseUrl?: string; - // (undocumented) + // @deprecated (undocumented) metadata?: TemplateMetadata; // (undocumented) output: { @@ -26,6 +26,8 @@ export interface TaskSpecV1beta2 { // (undocumented) steps: TaskStep[]; // (undocumented) + templateInfo?: TemplateInfo; + // (undocumented) values: JsonObject; } @@ -33,9 +35,9 @@ export interface TaskSpecV1beta2 { export interface TaskSpecV1beta3 { // (undocumented) apiVersion: 'scaffolder.backstage.io/v1beta3'; - // (undocumented) + // @deprecated (undocumented) baseUrl?: string; - // (undocumented) + // @deprecated (undocumented) metadata?: TemplateMetadata; // (undocumented) output: { @@ -45,6 +47,8 @@ export interface TaskSpecV1beta3 { parameters: JsonObject; // (undocumented) steps: TaskStep[]; + // (undocumented) + templateInfo?: TemplateInfo; } // @public @@ -116,6 +120,12 @@ export interface TemplateEntityV1beta3 extends Entity { export const templateEntityV1beta3Validator: KindValidator; // @public +export type TemplateInfo = { + entityRef: string; + baseUrl?: string; +}; + +// @public @deprecated export type TemplateMetadata = { name: string; }; diff --git a/plugins/scaffolder-common/src/TaskSpec.ts b/plugins/scaffolder-common/src/TaskSpec.ts index 1cbe41b28c..2c7eb26d5b 100644 --- a/plugins/scaffolder-common/src/TaskSpec.ts +++ b/plugins/scaffolder-common/src/TaskSpec.ts @@ -21,11 +21,23 @@ import { JsonValue, JsonObject } from '@backstage/types'; * stored in the database. * * @public + * @deprecated use templateInfo on the spec instead */ export type TemplateMetadata = { name: string; }; +/** + * Information about a template that is stored on a task specification. + * Includes a stringified entityRef, and the baseUrl which is usually the relative path of the template definition + * + * @public + */ +export type TemplateInfo = { + entityRef: string; + baseUrl?: string; +}; + /** * An individual step of a scaffolder task, as stored in the database. * @@ -49,11 +61,14 @@ export interface TaskStep { */ export interface TaskSpecV1beta2 { apiVersion: 'backstage.io/v1beta2'; + /** @deprecated use templateInfo.baseUrl instead */ baseUrl?: string; values: JsonObject; steps: TaskStep[]; output: { [name: string]: string }; + /** @deprecated use templateInfo instead */ metadata?: TemplateMetadata; + templateInfo?: TemplateInfo; } /** @@ -64,11 +79,14 @@ export interface TaskSpecV1beta2 { */ export interface TaskSpecV1beta3 { apiVersion: 'scaffolder.backstage.io/v1beta3'; + /** @deprecated use templateInfo.baseUrl instead */ baseUrl?: string; parameters: JsonObject; steps: TaskStep[]; output: { [name: string]: JsonValue }; + /** @deprecated use templateInfo instead */ metadata?: TemplateMetadata; + templateInfo?: TemplateInfo; } /** diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx index 2f49f40643..a642660bac 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import { parseEntityRef } from '@backstage/catalog-model'; import { Content, ErrorPage, @@ -291,8 +292,9 @@ export const TaskPage = ({ loadingText }: TaskPageProps) => { const { output } = taskStream; const handleStartOver = () => { - if (!taskStream.task || !taskStream.task?.spec.metadata?.name) { + if (!taskStream.task || !taskStream.task?.spec.templateInfo?.entityRef) { navigate(generatePath(rootLink())); + return; } const formData = @@ -300,13 +302,17 @@ export const TaskPage = ({ loadingText }: TaskPageProps) => { ? taskStream.task!.spec.values : taskStream.task!.spec.parameters; + const { name } = parseEntityRef( + taskStream.task!.spec.templateInfo?.entityRef, + ); + navigate( generatePath( `${rootLink()}/templates/:templateName?${qs.stringify({ formData: JSON.stringify(formData), })}`, { - templateName: taskStream.task!.spec.metadata!.name, + templateName: name, }, ), );