diff --git a/.changeset/yellow-falcons-whisper.md b/.changeset/yellow-falcons-whisper.md new file mode 100644 index 0000000000..9168b32c27 --- /dev/null +++ b/.changeset/yellow-falcons-whisper.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder-common': patch +--- + +Expose template metadata to custom action handler in Scaffolder. diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index 07c4891a6c..e578918cc0 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -91,6 +91,8 @@ argument. It looks like the following: - `createTemporaryDirectory` a function to call to give you a temporary directory somewhere on the runner so you can store some files there rather than polluting the `workspacePath` +- `ctx.metadata` - an object containing a `name` field, indicating the template + name. More metadata fields may be added later. ### Registering Custom Actions diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 3f9b77a3a3..4b27b938c9 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -41,6 +41,7 @@ export type ActionContext = { input: Input; output(name: string, value: JsonValue): void; createTemporaryDirectory(): Promise; + metadata?: TemplateMetadata; }; // Warning: (ae-missing-release-tag) "CatalogEntityClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -432,6 +433,8 @@ export interface TaskSpecV1beta2 { // (undocumented) baseUrl?: string; // (undocumented) + metadata?: TemplateMetadata; + // (undocumented) output: { [name: string]: string; }; @@ -454,6 +457,8 @@ export interface TaskSpecV1beta3 { // (undocumented) baseUrl?: string; // (undocumented) + metadata?: TemplateMetadata; + // (undocumented) output: { [name: string]: JsonValue; }; @@ -558,4 +563,9 @@ export class TemplateActionRegistry { action: TemplateAction, ): void; } + +// @public +export type TemplateMetadata = { + name: string; +}; ``` diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts index ca3459d29c..52149be724 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts @@ -18,6 +18,7 @@ import { Logger } from 'winston'; import { Writable } from 'stream'; import { JsonValue, JsonObject } from '@backstage/types'; import { Schema } from 'jsonschema'; +import { TemplateMetadata } from '../tasks/types'; type PartialJsonObject = Partial; type PartialJsonValue = PartialJsonObject | JsonValue | undefined; @@ -44,6 +45,8 @@ export type ActionContext = { * Creates a temporary directory for use by the action, which is then cleaned up automatically. */ createTemporaryDirectory(): Promise; + + metadata?: TemplateMetadata; }; export type TemplateAction = { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts index d07ff29fae..1f7877791c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts @@ -146,6 +146,30 @@ describe('DefaultWorkflowRunner', () => { expect(fakeActionHandler).toHaveBeenCalledTimes(1); }); + + it('should pass metadata through', async () => { + const templateName = 'template name'; + const task = createMockTaskWithSpec({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + parameters: {}, + output: {}, + steps: [ + { + id: 'test', + name: 'name', + action: 'jest-validated-action', + input: { foo: 1 }, + }, + ], + metadata: { name: templateName }, + }); + + await runner.execute(task); + + expect(fakeActionHandler.mock.calls[0][0].metadata).toEqual({ + name: templateName, + }); + }); }); describe('conditionals', () => { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts index 77d0a7be09..5582b53023 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts @@ -225,6 +225,12 @@ 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({ baseUrl: task.spec.baseUrl, logger: taskLogger, @@ -242,6 +248,7 @@ export class HandlebarsWorkflowRunner implements WorkflowRunner { output(name: string, value: JsonValue) { stepOutputs[name] = value; }, + metadata: task.spec.metadata, }); // Remove all temporary directories that were created when executing the action diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.test.ts index 91e772dc4e..9b05c43831 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.test.ts @@ -28,6 +28,7 @@ describe('LegacyWorkflowRunner', () => { let runner: HandlebarsWorkflowRunner; const logger = getVoidLogger(); let actionRegistry = new TemplateActionRegistry(); + let fakeActionHandler: jest.Mock; const integrations = ScmIntegrations.fromConfig( new ConfigReader({ @@ -59,6 +60,11 @@ describe('LegacyWorkflowRunner', () => { ctx.output('badOutput', false); }, }); + fakeActionHandler = jest.fn(); + actionRegistry.register({ + id: 'test-metadata-action', + handler: fakeActionHandler, + }); runner = new HandlebarsWorkflowRunner({ actionRegistry, @@ -87,6 +93,30 @@ describe('LegacyWorkflowRunner', () => { ); }); + it('should pass metadata through', async () => { + const templateName = 'template name'; + const task = createMockTaskWithSpec({ + apiVersion: 'backstage.io/v1beta2', + steps: [ + { + id: 'test', + name: 'name', + action: 'test-metadata-action', + input: { foo: 1 }, + }, + ], + output: {}, + values: {}, + metadata: { name: templateName }, + }); + + await runner.execute(task); + + expect(fakeActionHandler.mock.calls[0][0].metadata).toEqual({ + name: templateName, + }); + }); + describe('templating', () => { it('should template the output', async () => { const task = createMockTaskWithSpec({ diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 587a93e1f2..c66d967103 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -228,6 +228,12 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { const tmpDirs = new Array(); const stepOutput: { [outputName: string]: JsonValue } = {}; + if (!task.spec.metadata) { + console.warn( + 'DEPRECATION NOTICE: metadata is undefined. metadata will be required in the future.', + ); + } + await action.handler({ baseUrl: task.spec.baseUrl, input, @@ -244,6 +250,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { output(name: string, value: JsonValue) { stepOutput[name] = value; }, + metadata: task.spec.metadata, }); // Remove all temporary directories that were created when executing the action diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts index 510c862dae..f563a08df6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts @@ -34,4 +34,5 @@ export type { TaskContext, TaskStore, DispatchResult, + TemplateMetadata, } from './types'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 6cfd914bd2..b492f8ad2c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -69,6 +69,15 @@ export type SerializedTaskEvent = { createdAt: string; }; +/** + * TemplateMetadata + * + * @public + */ +export type TemplateMetadata = { + name: string; +}; + /** * TaskSpecV1beta2 * @@ -86,6 +95,7 @@ export interface TaskSpecV1beta2 { if?: string | boolean; }>; output: { [name: string]: string }; + metadata?: TemplateMetadata; } export interface TaskStep { @@ -107,6 +117,7 @@ export interface TaskSpecV1beta3 { parameters: JsonObject; steps: TaskStep[]; output: { [name: string]: JsonValue }; + metadata?: TemplateMetadata; } /** diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 2ae30593f2..b8f2968287 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -209,6 +209,7 @@ export async function createRouter( name: step.name ?? step.action, })), output: template.spec.output ?? {}, + metadata: { name: template.metadata?.name }, } : { apiVersion: template.apiVersion, @@ -220,6 +221,7 @@ export async function createRouter( name: step.name ?? step.action, })), output: template.spec.output ?? {}, + metadata: { name: template.metadata?.name }, }; } else { throw new InputError(