diff --git a/.changeset/eighty-showers-hide.md b/.changeset/eighty-showers-hide.md new file mode 100644 index 0000000000..0871fea4bb --- /dev/null +++ b/.changeset/eighty-showers-hide.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-scaffolder-node-test-utils': minor +--- + +**BREAKING CHANGES** + +Because of the removal of the `logStream` property to the `ActionsContext` this has been removed from the `createMockActionContext` method. + +You can remove this as it's no longer supported in the scaffolder actions. diff --git a/.changeset/fresh-otters-say.md b/.changeset/fresh-otters-say.md new file mode 100644 index 0000000000..ed91c9fb8d --- /dev/null +++ b/.changeset/fresh-otters-say.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Migrating to latest action format diff --git a/.changeset/scaffolder-breakges-4.md b/.changeset/scaffolder-breakges-4.md new file mode 100644 index 0000000000..e9f0c81256 --- /dev/null +++ b/.changeset/scaffolder-breakges-4.md @@ -0,0 +1,60 @@ +--- +'@backstage/plugin-scaffolder-node': minor +--- + +**BREAKING CHANGES** + +The legacy methods to define `createTemplateActions` have been replaced with the new native `zod` approaches for defining input and output schemas. + +You can migrate actions that look like the following with the below examples: + +```ts +// really old legacy json schema +createTemplateAction<{ repoUrl: string }, { repoOutput: string }>({ + id: 'test', + schema: { + input: { + type: 'object' + required: ['repoUrl'] + properties: { + repoUrl: { + type: 'string', + description: 'repository url description' + } + } + } + } +}); + +// old zod method +createTemplateAction({ + id: 'test' + schema: { + input: { + repoUrl: z.string({ description: 'repository url description' }) + } + } +}) + +// new method: +createTemplateAction({ + id: 'test', + schema: { + input: { + repoUrl: z => z.string({ description: 'repository url description' }) + } + } +}) + +// or for more complex zod types like unions +createTemplateAction({ + id: 'test', + schema: { + input: z => z.object({ + repoUrl: z.string({ description: 'repository url description' }) + }) + } +}) +``` + +This breaking change also means that `logStream` has been removed entirely from `ActionsContext`, and that the `logger` is now just a `LoggerService` implementation instead. There is no replacement for the `logStream`, if you wish to still keep using a `logStream` we recommend that you create your own stream that writes to `ctx.logger` instead. diff --git a/.changeset/strong-trams-joke.md b/.changeset/strong-trams-joke.md new file mode 100644 index 0000000000..8b6a831d7d --- /dev/null +++ b/.changeset/strong-trams-joke.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Updating the scaffolder action boilerplate to use new `zod` schema diff --git a/packages/cli/templates/scaffolder-backend-module/src/actions/example.ts b/packages/cli/templates/scaffolder-backend-module/src/actions/example.ts index 7e47a89bcc..9ce24f92bc 100644 --- a/packages/cli/templates/scaffolder-backend-module/src/actions/example.ts +++ b/packages/cli/templates/scaffolder-backend-module/src/actions/example.ts @@ -12,22 +12,16 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; export function createExampleAction() { // For more information on how to define custom actions, see // https://backstage.io/docs/features/software-templates/writing-custom-actions - return createTemplateAction<{ - myParameter: string; - }>({ + return createTemplateAction({ id: 'acme:example', description: 'Runs an example action', schema: { input: { - type: 'object', - required: ['myParameter'], - properties: { - myParameter: { - title: 'An example parameter', - description: "This is an example parameter, don't set it to foo", - type: 'string', - }, - }, + myParameter: z => + z.string({ + description: + "This is an example parameter, don't set it to foo", + }), }, }, async handler(ctx) { diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/report.api.md b/plugins/scaffolder-backend-module-bitbucket-cloud/report.api.md index 3a27006319..af34609213 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/report.api.md +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/report.api.md @@ -5,7 +5,6 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; -import { JsonObject } from '@backstage/types'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; @@ -103,14 +102,16 @@ export function createPublishBitbucketCloudPullRequestAction(options: { { repoUrl: string; title: string; - description?: string; - targetBranch?: string; sourceBranch: string; - token?: string; - gitAuthorName?: string; - gitAuthorEmail?: string; + description?: string | undefined; + targetBranch?: string | undefined; + token?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; }, - JsonObject, - 'v1' + { + pullRequestUrl: string; + }, + 'v2' >; ``` diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts index b59aaa4e41..9969f13eb2 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.ts @@ -139,6 +139,7 @@ const findBranches = async (opts: { return r.values[0]; }; + const createBranch = async (opts: { workspace: string; repo: string; @@ -190,6 +191,7 @@ const createBranch = async (opts: { return await response.json(); }; + const getDefaultBranch = async (opts: { workspace: string; repo: string; @@ -233,73 +235,60 @@ export function createPublishBitbucketCloudPullRequestAction(options: { }) { const { integrations, config } = options; - return createTemplateAction<{ - repoUrl: string; - title: string; - description?: string; - targetBranch?: string; - sourceBranch: string; - token?: string; - gitAuthorName?: string; - gitAuthorEmail?: string; - }>({ + return createTemplateAction({ id: 'publish:bitbucketCloud:pull-request', examples, schema: { input: { - type: 'object', - required: ['repoUrl', 'title', 'sourceBranch'], - properties: { - repoUrl: { - title: 'Repository Location', - type: 'string', - }, - title: { - title: 'Pull Request title', - type: 'string', + repoUrl: z => + z.string({ + description: 'Repository Location', + }), + title: z => + z.string({ description: 'The title for the pull request', - }, - description: { - title: 'Pull Request Description', - type: 'string', - description: 'The description of the pull request', - }, - targetBranch: { - title: 'Target Branch', - type: 'string', - description: `Branch of repository to apply changes to. The default value is 'master'`, - }, - sourceBranch: { - title: 'Source Branch', - type: 'string', + }), + description: z => + z + .string({ + description: 'The description of the pull request', + }) + .optional(), + targetBranch: z => + z + .string({ + description: `Branch of repository to apply changes to. The default value is 'master'`, + }) + .optional(), + sourceBranch: z => + z.string({ description: 'Branch of repository to copy changes from', - }, - token: { - title: 'Authorization Token', - type: 'string', - description: - 'The token to use for authorization to BitBucket Cloud', - }, - gitAuthorName: { - title: 'Author Name', - type: 'string', - description: `Sets the author name for the commit. The default value is 'Scaffolder'`, - }, - gitAuthorEmail: { - title: 'Author Email', - type: 'string', - description: `Sets the author email for the commit.`, - }, - }, + }), + token: z => + z + .string({ + description: + 'The token to use for authorization to BitBucket Cloud', + }) + .optional(), + gitAuthorName: z => + z + .string({ + description: `Sets the author name for the commit. The default value is 'Scaffolder'`, + }) + .optional(), + gitAuthorEmail: z => + z + .string({ + description: `Sets the author email for the commit.`, + }) + .optional(), }, output: { - type: 'object', - properties: { - pullRequestUrl: { - title: 'A URL to the pull request with the provider', - type: 'string', - }, - }, + pullRequestUrl: z => + z.string({ + description: 'A URL to the pull request with the provider', + }), }, }, async handler(ctx) { diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts index 8f85f08ea4..670af15595 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts @@ -97,7 +97,7 @@ describe('createPublishGithubPullRequestAction', () => { describe('with targetBranchName', () => { let input: GithubPullRequestActionInput; - let ctx: ActionContext; + let ctx: ActionContext; beforeEach(() => { fakeClient = { @@ -181,7 +181,7 @@ describe('createPublishGithubPullRequestAction', () => { describe('with no sourcePath', () => { let input: GithubPullRequestActionInput; - let ctx: ActionContext; + let ctx: ActionContext; beforeEach(() => { input = { @@ -250,7 +250,7 @@ describe('createPublishGithubPullRequestAction', () => { describe('with sourcePath', () => { let input: GithubPullRequestActionInput; - let ctx: ActionContext; + let ctx: ActionContext; beforeEach(() => { input = { @@ -306,7 +306,7 @@ describe('createPublishGithubPullRequestAction', () => { describe('with repoUrl', () => { let input: GithubPullRequestActionInput; - let ctx: ActionContext; + let ctx: ActionContext; beforeEach(() => { input = { @@ -359,7 +359,7 @@ describe('createPublishGithubPullRequestAction', () => { describe('with reviewers and teamReviewers', () => { let input: GithubPullRequestActionInput; - let ctx: ActionContext; + let ctx: ActionContext; beforeEach(() => { input = { @@ -406,7 +406,7 @@ describe('createPublishGithubPullRequestAction', () => { describe('with no reviewers and teamReviewers', () => { let input: GithubPullRequestActionInput; - let ctx: ActionContext; + let ctx: ActionContext; beforeEach(() => { input = { @@ -431,7 +431,7 @@ describe('createPublishGithubPullRequestAction', () => { describe('with assignees', () => { let input: GithubPullRequestActionInput; - let ctx: ActionContext; + let ctx: ActionContext; beforeEach(() => { input = { @@ -475,7 +475,7 @@ describe('createPublishGithubPullRequestAction', () => { describe('with broken symlink', () => { let input: GithubPullRequestActionInput; - let ctx: ActionContext; + let ctx: ActionContext; beforeEach(() => { input = { @@ -520,7 +520,7 @@ describe('createPublishGithubPullRequestAction', () => { describe('with executable file mode 755', () => { let input: GithubPullRequestActionInput; - let ctx: ActionContext; + let ctx: ActionContext; beforeEach(() => { input = { @@ -579,7 +579,7 @@ describe('createPublishGithubPullRequestAction', () => { describe('with executable file mode 775', () => { let input: GithubPullRequestActionInput; - let ctx: ActionContext; + let ctx: ActionContext; beforeEach(() => { input = { @@ -638,7 +638,7 @@ describe('createPublishGithubPullRequestAction', () => { describe('with commit message', () => { let input: GithubPullRequestActionInput; - let ctx: ActionContext; + let ctx: ActionContext; beforeEach(() => { input = { @@ -683,7 +683,7 @@ describe('createPublishGithubPullRequestAction', () => { describe('with force fork', () => { let input: GithubPullRequestActionInput; - let ctx: ActionContext; + let ctx: ActionContext; beforeEach(() => { input = { @@ -729,7 +729,7 @@ describe('createPublishGithubPullRequestAction', () => { describe('with author name and email', () => { let input: GithubPullRequestActionInput; - let ctx: ActionContext; + let ctx: ActionContext; beforeEach(() => { input = { @@ -779,7 +779,7 @@ describe('createPublishGithubPullRequestAction', () => { describe('with author name', () => { let input: GithubPullRequestActionInput; - let ctx: ActionContext; + let ctx: ActionContext; beforeEach(() => { input = { @@ -828,7 +828,7 @@ describe('createPublishGithubPullRequestAction', () => { describe('with author email', () => { let input: GithubPullRequestActionInput; - let ctx: ActionContext; + let ctx: ActionContext; beforeEach(() => { input = { @@ -877,7 +877,7 @@ describe('createPublishGithubPullRequestAction', () => { describe('with author from config file', () => { let input: GithubPullRequestActionInput; - let ctx: ActionContext; + let ctx: ActionContext; beforeEach(() => { input = { @@ -946,7 +946,7 @@ describe('createPublishGithubPullRequestAction', () => { describe('with author attributes and config file', () => { let input: GithubPullRequestActionInput; - let ctx: ActionContext; + let ctx: ActionContext; beforeEach(() => { input = { @@ -1017,7 +1017,7 @@ describe('createPublishGithubPullRequestAction', () => { describe('with author fallback and no config', () => { let input: GithubPullRequestActionInput; - let ctx: ActionContext; + let ctx: ActionContext; beforeEach(() => { input = { @@ -1112,7 +1112,7 @@ describe('createPublishGithubPullRequestAction', () => { describe('with createWhenEmpty equals true', () => { let input: GithubPullRequestActionInput; - let ctx: ActionContext; + let ctx: ActionContext; beforeEach(() => { input = { @@ -1175,7 +1175,7 @@ describe('createPublishGithubPullRequestAction', () => { describe('with createWhenEmpty equals false', () => { let input: GithubPullRequestActionInput; - let ctx: ActionContext; + let ctx: ActionContext; beforeEach(() => { fakeClient.createPullRequest.mockResolvedValueOnce(null); diff --git a/plugins/scaffolder-backend/report.api.md b/plugins/scaffolder-backend/report.api.md index edfddb9a27..9b0e0b05c3 100644 --- a/plugins/scaffolder-backend/report.api.md +++ b/plugins/scaffolder-backend/report.api.md @@ -76,15 +76,26 @@ export function createCatalogRegisterAction(options: { }): TemplateAction< | { catalogInfoUrl: string; - optional?: boolean; + optional?: boolean | undefined; + } + | { + catalogInfoUrl: string; + optional?: boolean | undefined; + catalogInfoPath?: string | undefined; } | { repoContentsUrl: string; - catalogInfoPath?: string; - optional?: boolean; + optional?: boolean | undefined; + } + | { + repoContentsUrl: string; + optional?: boolean | undefined; + catalogInfoPath?: string | undefined; }, - JsonObject, - 'v1' + { + [x: string]: any; + }, + 'v2' >; // @public @@ -93,12 +104,23 @@ export function createCatalogWriteAction(): TemplateAction< entity: Record; filePath?: string | undefined; }, - any, - 'v1' + { + [x: string]: any; + }, + 'v2' >; // @public -export function createDebugLogAction(): TemplateAction; +export function createDebugLogAction(): TemplateAction< + { + message?: string | undefined; + listWorkspace?: boolean | 'with-contents' | 'with-filenames' | undefined; + }, + { + [x: string]: any; + }, + 'v2' +>; // @public export function createFetchCatalogEntityAction(options: { @@ -126,11 +148,13 @@ export function createFetchPlainAction(options: { }): TemplateAction< { url: string; - targetPath?: string; - token?: string; + targetPath?: string | undefined; + token?: string | undefined; }, - JsonObject, - 'v1' + { + [x: string]: any; + }, + 'v2' >; // @public @@ -141,10 +165,12 @@ export function createFetchPlainFileAction(options: { { url: string; targetPath: string; - token?: string; + token?: string | undefined; }, - JsonObject, - 'v1' + { + [x: string]: any; + }, + 'v2' >; // @public @@ -156,18 +182,21 @@ export function createFetchTemplateAction(options: { }): TemplateAction< { url: string; - targetPath?: string; - values: any; - templateFileExtension?: string | boolean; - copyWithoutTemplating?: string[]; - cookiecutterCompat?: boolean; - replace?: boolean; - trimBlocks?: boolean; - lstripBlocks?: boolean; - token?: string; + targetPath?: string | undefined; + values?: Record | undefined; + copyWithoutRender?: string[] | undefined; + copyWithoutTemplating?: string[] | undefined; + cookiecutterCompat?: boolean | undefined; + templateFileExtension?: string | boolean | undefined; + replace?: boolean | undefined; + trimBlocks?: boolean | undefined; + lstripBlocks?: boolean | undefined; + token?: string | undefined; }, - JsonObject, - 'v1' + { + [x: string]: any; + }, + 'v2' >; // @public @@ -180,15 +209,17 @@ export function createFetchTemplateFileAction(options: { { url: string; targetPath: string; - values: any; - cookiecutterCompat?: boolean; - replace?: boolean; - trimBlocks?: boolean; - lstripBlocks?: boolean; - token?: string; + values?: Record | undefined; + cookiecutterCompat?: boolean | undefined; + replace?: boolean | undefined; + trimBlocks?: boolean | undefined; + lstripBlocks?: boolean | undefined; + token?: string | undefined; }, - JsonObject, - 'v1' + { + [x: string]: any; + }, + 'v2' >; // @public @@ -196,15 +227,17 @@ export const createFilesystemDeleteAction: () => TemplateAction< { files: string[]; }, - JsonObject, - 'v1' + { + [x: string]: any; + }, + 'v2' >; // @public export const createFilesystemReadDirAction: () => TemplateAction< { - recursive: boolean; paths: string[]; + recursive: boolean; }, { files: { @@ -218,26 +251,38 @@ export const createFilesystemReadDirAction: () => TemplateAction< fullPath: string; }[]; }, - 'v1' + 'v2' >; // @public export const createFilesystemRenameAction: () => TemplateAction< { - files: Array<{ + files: { from: string; to: string; - overwrite?: boolean; - }>; + overwrite?: boolean | undefined; + }[]; }, - JsonObject, - 'v1' + { + [x: string]: any; + }, + 'v2' >; // @public export function createWaitAction(options?: { maxWaitTime?: Duration | HumanDuration; -}): TemplateAction; +}): TemplateAction< + { + minutes?: number | undefined; + seconds?: number | undefined; + milliseconds?: number | undefined; + }, + { + [x: string]: any; + }, + 'v2' +>; // @public @deprecated export type CreateWorkerOptions = { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts index eab31f14df..46b0e12de7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts @@ -35,73 +35,45 @@ export function createCatalogRegisterAction(options: { }) { const { catalogClient, integrations, auth } = options; - return createTemplateAction< - | { catalogInfoUrl: string; optional?: boolean } - | { repoContentsUrl: string; catalogInfoPath?: string; optional?: boolean } - >({ + return createTemplateAction({ id, description: 'Registers entities from a catalog descriptor file in the workspace into the software catalog.', examples, schema: { - input: { - oneOf: [ - { - type: 'object', - required: ['catalogInfoUrl'], - properties: { - catalogInfoUrl: { - title: 'Catalog Info URL', - description: - 'An absolute URL pointing to the catalog info file location', - type: 'string', - }, - optional: { - title: 'Optional', + input: z => + z.union([ + z.object({ + catalogInfoUrl: z.string({ + description: + 'An absolute URL pointing to the catalog info file location', + }), + optional: z + .boolean({ description: 'Permit the registered location to optionally exist. Default: false', - type: 'boolean', - }, - }, - }, - { - type: 'object', - required: ['repoContentsUrl'], - properties: { - repoContentsUrl: { - title: 'Repository Contents URL', - description: - 'An absolute URL pointing to the root of a repository directory tree', - type: 'string', - }, - catalogInfoPath: { - title: 'Fetch URL', + }) + .optional(), + }), + z.object({ + repoContentsUrl: z.string({ + description: + 'An absolute URL pointing to the root of a repository directory tree', + }), + catalogInfoPath: z + .string({ description: 'A relative path from the repo root pointing to the catalog info file, defaults to /catalog-info.yaml', - type: 'string', - }, - optional: { - title: 'Optional', + }) + .optional(), + optional: z + .boolean({ description: 'Permit the registered location to optionally exist. Default: false', - type: 'boolean', - }, - }, - }, - ], - }, - output: { - type: 'object', - required: ['catalogInfoUrl'], - properties: { - entityRef: { - type: 'string', - }, - catalogInfoUrl: { - type: 'string', - }, - }, - }, + }) + .optional(), + }), + ]), }, async handler(ctx) { const { input } = ctx; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts index 29ce6c986d..e01cc3b88a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts @@ -18,7 +18,6 @@ import fs from 'fs-extra'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import * as yaml from 'yaml'; import { resolveSafeChildPath } from '@backstage/backend-plugin-api'; -import { z } from 'zod'; import { examples } from './write.examples'; const id = 'catalog:write'; @@ -33,18 +32,17 @@ export function createCatalogWriteAction() { id, description: 'Writes the catalog-info.yaml for your template', schema: { - input: z.object({ - filePath: z - .string() - .optional() - .describe('Defaults to catalog-info.yaml'), + input: { + filePath: z => + z.string().optional().describe('Defaults to catalog-info.yaml'), // TODO: this should reference an zod entity validator if it existed. - entity: z - .record(z.any()) - .describe( - 'You can provide the same values used in the Entity schema.', - ), - }), + entity: z => + z + .record(z.any()) + .describe( + 'You can provide the same values used in the Entity schema.', + ), + }, }, examples, supportsDryRun: true, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts index 2ff64e1025..6eae1794f8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts @@ -19,7 +19,6 @@ import { join, relative } from 'path'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { examples } from './log.examples'; import fs from 'fs'; -import { z } from 'zod'; const id = 'debug:log'; @@ -34,24 +33,23 @@ const id = 'debug:log'; * @public */ export function createDebugLogAction() { - return createTemplateAction<{ - message?: string; - listWorkspace?: boolean | 'with-filenames' | 'with-contents'; - }>({ + return createTemplateAction({ id, description: 'Writes a message into the log and/or lists all files in the workspace.', examples, schema: { - input: z.object({ - message: z.string({ description: 'Message to output.' }).optional(), - listWorkspace: z - .union([z.boolean(), z.enum(['with-filenames', 'with-contents'])], { - description: - 'List all files in the workspace. If used with "with-contents", also the file contents are listed.', - }) - .optional(), - }), + input: { + message: z => + z.string({ description: 'Message to output.' }).optional(), + listWorkspace: z => + z + .union([z.boolean(), z.enum(['with-filenames', 'with-contents'])], { + description: + 'List all files in the workspace. If used with "with-contents", also the file contents are listed.', + }) + .optional(), + }, }, supportsDryRun: true, async handler(ctx) { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts index 8bc29a74c9..6ffd338a11 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts @@ -15,7 +15,6 @@ */ import { createWaitAction } from './wait'; -import { Writable } from 'stream'; import { examples } from './wait.examples'; import yaml from 'yaml'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; @@ -23,13 +22,7 @@ import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test- describe('debug:wait examples', () => { const action = createWaitAction(); - const logStream = { - write: jest.fn(), - } as jest.Mocked> as jest.Mocked; - - const mockContext = createMockActionContext({ - logStream, - }); + const mockContext = createMockActionContext(); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts index fc688eee44..919cb9faee 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts @@ -15,19 +15,12 @@ */ import { createWaitAction } from './wait'; -import { Writable } from 'stream'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('debug:wait', () => { const action = createWaitAction(); - const logStream = { - write: jest.fn(), - } as jest.Mocked> as jest.Mocked; - - const mockContext = createMockActionContext({ - logStream, - }); + const mockContext = createMockActionContext(); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.ts index fd4a77754b..e048720f29 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.ts @@ -48,27 +48,30 @@ export function createWaitAction(options?: { return Duration.fromISOTime(MAX_WAIT_TIME_IN_ISO); }; - return createTemplateAction({ + return createTemplateAction({ id, description: 'Waits for a certain period of time.', examples, schema: { input: { - type: 'object', - properties: { - minutes: { - title: 'Waiting period in minutes.', - type: 'number', - }, - seconds: { - title: 'Waiting period in seconds.', - type: 'number', - }, - milliseconds: { - title: 'Waiting period in milliseconds.', - type: 'number', - }, - }, + minutes: z => + z + .number({ + description: 'Waiting period in minutes.', + }) + .optional(), + seconds: z => + z + .number({ + description: 'Waiting period in seconds.', + }) + .optional(), + milliseconds: z => + z + .number({ + description: 'Waiting period in milliseconds.', + }) + .optional(), }, }, async handler(ctx) { 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 a4f795dd54..c0bed9bea7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts @@ -39,39 +39,32 @@ export function createFetchPlainAction(options: { }) { const { reader, integrations } = options; - return createTemplateAction<{ - url: string; - targetPath?: string; - token?: string; - }>({ + return createTemplateAction({ id: ACTION_ID, examples, description: 'Downloads content and places it in the workspace, or optionally in a subdirectory specified by the `targetPath` input option.', schema: { input: { - type: 'object', - required: ['url'], - properties: { - url: { - title: 'Fetch URL', + url: z => + z.string({ description: 'Relative path or absolute URL pointing to the directory tree to fetch', - type: 'string', - }, - targetPath: { - title: 'Target Path', - description: - 'Target path within the working directory to download the contents to.', - type: 'string', - }, - token: { - title: 'Token', - description: - 'An optional token to use for authentication when reading the resources.', - type: 'string', - }, - }, + }), + targetPath: z => + z + .string({ + description: + 'Target path within the working directory to download the contents to.', + }) + .optional(), + token: z => + z + .string({ + description: + 'An optional token to use for authentication when reading the resources.', + }) + .optional(), }, }, supportsDryRun: true, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.ts index a07c9bfc6b..c132214c10 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.ts @@ -14,18 +14,20 @@ * limitations under the License. */ -import { UrlReaderService } from '@backstage/backend-plugin-api'; -import { resolveSafeChildPath } from '@backstage/backend-plugin-api'; +import { + resolveSafeChildPath, + UrlReaderService, +} from '@backstage/backend-plugin-api'; import { ScmIntegrations } from '@backstage/integration'; import { examples } from './plainFile.examples'; + import { createTemplateAction, fetchFile, } from '@backstage/plugin-scaffolder-node'; /** - * Downloads content and places it in the workspace, or optionally - * in a subdirectory specified by the 'targetPath' input option. + * Downloads a single file and places it in the workspace. * @public */ export function createFetchPlainFileAction(options: { @@ -34,38 +36,29 @@ export function createFetchPlainFileAction(options: { }) { const { reader, integrations } = options; - return createTemplateAction<{ - url: string; - targetPath: string; - token?: string; - }>({ + return createTemplateAction({ id: 'fetch:plain:file', description: 'Downloads single file and places it in the workspace.', examples, schema: { input: { - type: 'object', - required: ['url', 'targetPath'], - properties: { - url: { - title: 'Fetch URL', + url: z => + z.string({ description: 'Relative path or absolute URL pointing to the single file to fetch.', - type: 'string', - }, - targetPath: { - title: 'Target Path', + }), + targetPath: z => + z.string({ description: 'Target path within the working directory to download the file as.', - type: 'string', - }, - token: { - title: 'Token', - description: - 'An optional token to use for authentication when reading the resources.', - type: 'string', - }, - }, + }), + token: z => + z + .string({ + description: + 'An optional token to use for authentication when reading the resources.', + }) + .optional(), }, }, supportsDryRun: true, 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 3b8ca6f227..159bd1c4a2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts @@ -41,89 +41,87 @@ export function createFetchTemplateAction(options: { additionalTemplateFilters?: Record; additionalTemplateGlobals?: Record; }) { - return createTemplateAction<{ - url: string; - targetPath?: string; - values: any; - templateFileExtension?: string | boolean; - - // Cookiecutter compat options - copyWithoutTemplating?: string[]; - cookiecutterCompat?: boolean; - replace?: boolean; - trimBlocks?: boolean; - lstripBlocks?: boolean; - token?: string; - }>({ + return createTemplateAction({ id: 'fetch:template', description: 'Downloads a skeleton, templates variables into file and directory names and content, and places the result in the workspace, or optionally in a subdirectory specified by the `targetPath` input option.', examples, schema: { input: { - type: 'object', - required: ['url'], - properties: { - url: { - title: 'Fetch URL', + url: z => + z.string({ description: 'Relative path or absolute URL pointing to the directory tree to fetch', - type: 'string', - }, - targetPath: { - title: 'Target Path', - description: - 'Target path within the working directory to download the contents to. Defaults to the working directory root.', - type: 'string', - }, - values: { - title: 'Template Values', - description: 'Values to pass on to the templating engine', - type: 'object', - }, - copyWithoutRender: { - title: '[Deprecated] Copy Without Render', - description: - 'An array of glob patterns. Any files or directories which match are copied without being processed as templates.', - type: 'array', - items: { - type: 'string', - }, - }, - copyWithoutTemplating: { - title: 'Copy Without Templating', - description: - 'An array of glob patterns. Contents of matched files or directories are copied without being processed, but paths are subject to rendering.', - type: 'array', - items: { - type: 'string', - }, - }, - cookiecutterCompat: { - title: 'Cookiecutter compatibility mode', - description: - 'Enable features to maximise compatibility with templates built for fetch:cookiecutter', - type: 'boolean', - }, - templateFileExtension: { - title: 'Template File Extension', - description: - 'If set, only files with the given extension will be templated. If set to `true`, the default extension `.njk` is used.', - type: ['string', 'boolean'], - }, - replace: { - title: 'Replace files', - description: - 'If set, replace files in targetPath instead of skipping existing ones.', - type: 'boolean', - }, - token: { - title: 'Token', - description: - 'An optional token to use for authentication when reading the resources.', - type: 'string', - }, - }, + }), + targetPath: z => + z + .string({ + description: + 'Target path within the working directory to download the contents to. Defaults to the working directory root.', + }) + .optional(), + values: z => + z + .record(z.any(), { + description: 'Values to pass on to the templating engine', + }) + .optional(), + copyWithoutRender: z => + z + .array(z.string(), { + description: + 'An array of glob patterns. Any files or directories which match are copied without being processed as templates.', + }) + .optional(), + copyWithoutTemplating: z => + z + .array(z.string(), { + description: + 'An array of glob patterns. Contents of matched files or directories are copied without being processed, but paths are subject to rendering.', + }) + .optional(), + cookiecutterCompat: z => + z + .boolean({ + description: + 'Enable features to maximise compatibility with templates built for fetch:cookiecutter', + }) + .optional(), + templateFileExtension: z => + z + .union([z.string(), z.boolean()], { + description: + 'If set, only files with the given extension will be templated. If set to `true`, the default extension `.njk` is used.', + }) + .optional(), + replace: z => + z + .boolean({ + description: + 'If set, replace files in targetPath instead of skipping existing ones.', + }) + .optional(), + trimBlocks: z => + z + .boolean({ + description: + 'If set, the first newline after a block is removed (block, not variable tag).', + }) + .optional(), + lstripBlocks: z => + z + .boolean({ + description: + 'If set, leading spaces and tabs are stripped from the start of a line to a block.', + }) + .optional(), + token: z => + z + .string({ + description: + 'An optional token to use for authentication when reading the resources.', + }) + .optional(), }, }, supportsDryRun: true, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/templateFile.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/templateFile.ts index e70e9435c5..2cfe16f900 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/templateFile.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/templateFile.ts @@ -38,60 +38,63 @@ export function createFetchTemplateFileAction(options: { additionalTemplateFilters?: Record; additionalTemplateGlobals?: Record; }) { - return createTemplateAction<{ - url: string; - targetPath: string; - values: any; - cookiecutterCompat?: boolean; - replace?: boolean; - trimBlocks?: boolean; - lstripBlocks?: boolean; - token?: string; - }>({ + return createTemplateAction({ id: 'fetch:template:file', description: 'Downloads single file and places it in the workspace.', examples, schema: { input: { - type: 'object', - required: ['url', 'targetPath'], - properties: { - url: { - title: 'Fetch URL', + url: z => + z.string({ description: 'Relative path or absolute URL pointing to the single file to fetch.', - type: 'string', - }, - targetPath: { - title: 'Target Path', + }), + targetPath: z => + z.string({ description: 'Target path within the working directory to download the file as.', - type: 'string', - }, - values: { - title: 'Template Values', - description: 'Values to pass on to the templating engine', - type: 'object', - }, - cookiecutterCompat: { - title: 'Cookiecutter compatibility mode', - description: - 'Enable features to maximise compatibility with templates built for fetch:cookiecutter', - type: 'boolean', - }, - replace: { - title: 'Replace file', - description: - 'If set, replace file in targetPath instead of overwriting existing one.', - type: 'boolean', - }, - token: { - title: 'Token', - description: - 'An optional token to use for authentication when reading the resources.', - type: 'string', - }, - }, + }), + values: z => + z + .record(z.any(), { + description: 'Values to pass on to the templating engine', + }) + .optional(), + cookiecutterCompat: z => + z + .boolean({ + description: + 'Enable features to maximise compatibility with templates built for fetch:cookiecutter', + }) + .optional(), + replace: z => + z + .boolean({ + description: + 'If set, replace file in targetPath instead of overwriting existing one.', + }) + .optional(), + trimBlocks: z => + z + .boolean({ + description: + 'If set, the first newline after a block is removed (block, not variable tag).', + }) + .optional(), + lstripBlocks: z => + z + .boolean({ + description: + 'If set, leading spaces and tabs are stripped from the start of a line to a block.', + }) + .optional(), + token: z => + z + .string({ + description: + 'An optional token to use for authentication when reading the resources.', + }) + .optional(), }, }, supportsDryRun: true, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts index 5f894a4b3e..0290414323 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts @@ -26,24 +26,16 @@ import { examples } from './delete.examples'; * @public */ export const createFilesystemDeleteAction = () => { - return createTemplateAction<{ files: string[] }>({ + return createTemplateAction({ id: 'fs:delete', description: 'Deletes files and directories from the workspace', examples, schema: { input: { - required: ['files'], - type: 'object', - properties: { - files: { - title: 'Files', + files: z => + z.array(z.string(), { description: 'A list of files and directories that will be deleted', - type: 'array', - items: { - type: 'string', - }, - }, - }, + }), }, }, supportsDryRun: true, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/read.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/read.ts index 0351e7cb0e..3ab6526b4b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/read.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/read.ts @@ -14,19 +14,21 @@ * limitations under the License. */ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; -import z from 'zod'; import { resolveSafeChildPath } from '@backstage/backend-plugin-api'; import fs from 'fs/promises'; import path from 'path'; +import { z as zod } from 'zod'; -const contentSchema = z.object({ - name: z.string().describe('Name of the file or directory'), - path: z - .string() - .describe('path to the file or directory relative to the workspace'), - fullPath: z.string().describe('full path to the file or directory'), -}); -type Content = z.infer; +const contentSchema = (z: typeof zod) => + z.object({ + name: z.string().describe('Name of the file or directory'), + path: z + .string() + .describe('path to the file or directory relative to the workspace'), + fullPath: z.string().describe('full path to the file or directory'), + }); + +type Content = zod.infer>; /** * Creates new action that enables reading directories in the workspace. @@ -38,14 +40,14 @@ export const createFilesystemReadDirAction = () => { description: 'Reads files and directories from the workspace', supportsDryRun: true, schema: { - input: z.object({ - paths: z.array(z.string().min(1)), - recursive: z.boolean().default(false), - }), - output: z.object({ - files: z.array(contentSchema), - folders: z.array(contentSchema), - }), + input: { + paths: z => z.array(z.string().min(1)), + recursive: z => z.boolean().default(false), + }, + output: { + files: z => z.array(contentSchema(z)), + folders: z => z.array(contentSchema(z)), + }, }, async handler(ctx) { const files: Content[] = []; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts index 238fc9e8c0..7ced3ca6b0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts @@ -16,56 +16,42 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { resolveSafeChildPath } from '@backstage/backend-plugin-api'; -import { InputError } from '@backstage/errors'; import fs from 'fs-extra'; import { examples } from './rename.examples'; +import { InputError } from '@backstage/errors'; /** * Creates a new action that allows renames of files and directories in the workspace. * @public */ export const createFilesystemRenameAction = () => { - return createTemplateAction<{ - files: Array<{ - from: string; - to: string; - overwrite?: boolean; - }>; - }>({ + return createTemplateAction({ id: 'fs:rename', description: 'Renames files and directories within the workspace', examples, schema: { input: { - required: ['files'], - type: 'object', - properties: { - files: { - title: 'Files', - description: - 'A list of file and directory names that will be renamed', - type: 'array', - items: { - type: 'object', - required: ['from', 'to'], - properties: { - from: { - type: 'string', - title: 'The source location of the file to be renamed', - }, - to: { - type: 'string', - title: 'The destination of the new file', - }, - overwrite: { - type: 'boolean', - title: + files: z => + z.array( + z.object({ + from: z.string({ + description: 'The source location of the file to be renamed', + }), + to: z.string({ + description: 'The destination of the new file', + }), + overwrite: z + .boolean({ + description: 'Overwrite existing file or directory, default is false', - }, - }, + }) + .optional(), + }), + { + description: + 'A list of file and directory names that will be renamed', }, - }, - }, + ), }, }, supportsDryRun: true, @@ -78,7 +64,6 @@ export const createFilesystemRenameAction = () => { if (!file.from || !file.to) { throw new InputError('each file must have a from and to property'); } - const sourceFilepath = resolveSafeChildPath( ctx.workspacePath, file.from, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index 6d9692cf18..a4876f2edb 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -23,11 +23,9 @@ import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { createTemplateAction, TaskSecrets, - TemplateAction, TaskContext, } from '@backstage/plugin-scaffolder-node'; import { UserEntity } from '@backstage/catalog-model'; -import { z } from 'zod'; import { AuthorizeResult, PermissionEvaluator, @@ -38,7 +36,6 @@ import { mockCredentials, mockServices, } from '@backstage/backend-test-utils'; -import { loggerToWinstonLogger } from '../../util/loggerToWinstonLogger'; describe('NunjucksWorkflowRunner', () => { let actionRegistry: TemplateActionRegistry; @@ -127,32 +124,12 @@ describe('NunjucksWorkflowRunner', () => { handler: fakeActionHandler, schema: { input: { - type: 'object', - required: ['foo'], - properties: { - foo: { - type: 'number', - }, - }, + foo: z => z.number(), }, }, }), ); - actionRegistry.register( - createTemplateAction({ - id: 'jest-legacy-zod-validated-action', - description: 'Mock action for testing', - handler: fakeActionHandler, - supportsDryRun: true, - schema: { - input: z.object({ - foo: z.number(), - }), - }, - }) as TemplateAction, - ); - actionRegistry.register( createTemplateAction({ id: 'jest-zod-validated-action', @@ -186,13 +163,14 @@ describe('NunjucksWorkflowRunner', () => { id: 'checkpoints-action', description: 'Mock action with checkpoints', schema: { - output: z.object({ - key1: z.string(), - key2: z.string(), - key3: z.string(), - key4: z.string(), - key5: z.string(), - }), + output: z => + z.object({ + key1: z.string(), + key2: z.string(), + key3: z.string(), + key4: z.string(), + key5: z.string(), + }), }, handler: async ctx => { const key1 = await ctx.checkpoint({ @@ -222,7 +200,9 @@ describe('NunjucksWorkflowRunner', () => { ctx.output('key2', key2); ctx.output('key3', key3); + // @ts-expect-error - not valid output type ctx.output('key4', key4); + // @ts-expect-error - not valid output type ctx.output('key5', key5); }, }), @@ -236,7 +216,7 @@ describe('NunjucksWorkflowRunner', () => { actionRegistry, integrations, workingDirectory: mockDir.path, - logger: loggerToWinstonLogger(logger), + logger, permissions: mockedPermissionApi, }); }); @@ -280,22 +260,6 @@ describe('NunjucksWorkflowRunner', () => { ); }); - it('should throw an error if the action has legacy zod schema and the input does not match', async () => { - const task = createMockTaskWithSpec({ - steps: [ - { - id: 'test', - name: 'name', - action: 'jest-legacy-zod-validated-action', - }, - ], - }); - - await expect(runner.execute(task)).rejects.toThrow( - /Invalid input passed to action jest-legacy-zod-validated-action, instance requires property \"foo\"/, - ); - }); - it('should run the action when the zod validation passes', async () => { const task = createMockTaskWithSpec({ steps: [ @@ -313,23 +277,6 @@ describe('NunjucksWorkflowRunner', () => { expect(fakeActionHandler).toHaveBeenCalledTimes(1); }); - it('should run the action when the zod validation passes with legacy zod', async () => { - const task = createMockTaskWithSpec({ - steps: [ - { - id: 'test', - name: 'name', - action: 'jest-legacy-zod-validated-action', - input: { foo: 1 }, - }, - ], - }); - - await runner.execute(task); - - expect(fakeActionHandler).toHaveBeenCalledTimes(1); - }); - it('should run the action when the validation passes', async () => { const task = createMockTaskWithSpec({ steps: [ diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 80e9517fd6..a66ae0e789 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -28,7 +28,6 @@ import fs from 'fs-extra'; import { validate as validateJsonSchema } from 'jsonschema'; import nunjucks from 'nunjucks'; import path from 'path'; -import { PassThrough } from 'stream'; import * as winston from 'winston'; import { SecureTemplater, @@ -40,6 +39,7 @@ import { TaskTrackType, WorkflowResponse, WorkflowRunner } from './types'; import type { AuditorService, + LoggerService, PermissionsService, } from '@backstage/backend-plugin-api'; import { UserEntity } from '@backstage/catalog-model'; @@ -59,14 +59,13 @@ import { createDefaultFilters } from '../../lib/templating/filters/createDefault import { scaffolderActionRules } from '../../service/rules'; import { createCounterMetric, createHistogramMetric } from '../../util/metrics'; import { BackstageLoggerTransport, WinstonLogger } from './logger'; -import { loggerToWinstonLogger } from '../../util/loggerToWinstonLogger'; import { convertFiltersToRecord } from '../../util/templating'; type NunjucksWorkflowRunnerOptions = { workingDirectory: string; actionRegistry: TemplateActionRegistry; integrations: ScmIntegrations; - logger: winston.Logger; + logger: LoggerService; auditor?: AuditorService; additionalTemplateFilters?: Record; additionalTemplateGlobals?: Record; @@ -113,7 +112,7 @@ const createStepLogger = ({ }: { task: TaskContext; step: TaskStep; - rootLogger: winston.Logger; + rootLogger: LoggerService; }) => { const taskLogger = WinstonLogger.create({ level: process.env.LOG_LEVEL || 'info', @@ -126,22 +125,7 @@ const createStepLogger = ({ taskLogger.addRedactions(Object.values(task.secrets ?? {})); - // This stream logger should be deprecated. We're going to replace it with - // just using the logger directly, as all those logs get written to step logs - // using the stepLogStream above. - // Initially this stream used to be the only way to write to the client logs, but that - // has changed over time, there's not really a need for this anymore. - // You can just create a simple wrapper like the below in your action to write to the main logger. - // This way we also get redactions for free. - const streamLogger = new PassThrough(); - streamLogger.on('data', async data => { - const message = data.toString().trim(); - if (message?.length > 1) { - taskLogger.info(message); - } - }); - - return { taskLogger, streamLogger }; + return { taskLogger }; }; const isActionAuthorized = createConditionAuthorizer( @@ -268,7 +252,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { } const action: TemplateAction = this.options.actionRegistry.get(step.action); - const { taskLogger, streamLogger } = createStepLogger({ + const { taskLogger } = createStepLogger({ task, step, rootLogger: this.options.logger, @@ -393,9 +377,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { id: await task.getWorkspaceName(), }, secrets: task.secrets ?? {}, - // TODO(blam): move to LoggerService and away from Winston - logger: loggerToWinstonLogger(taskLogger), - logStream: streamLogger, + logger: taskLogger, workspacePath, async checkpoint(opts: { key?: string; diff --git a/plugins/scaffolder-node-test-utils/report.api.md b/plugins/scaffolder-node-test-utils/report.api.md index b95d4cef0c..2780baf1d5 100644 --- a/plugins/scaffolder-node-test-utils/report.api.md +++ b/plugins/scaffolder-node-test-utils/report.api.md @@ -7,12 +7,12 @@ import { ActionContext } from '@backstage/plugin-scaffolder-node'; import { JsonObject } from '@backstage/types'; // @public -export const createMockActionContext: < +export function createMockActionContext< TActionInput extends JsonObject = JsonObject, - TActionOutput extends JsonObject = JsonObject, + TActionOutput extends JsonObject = any, >( options?: Partial>, -) => ActionContext; +): ActionContext; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/scaffolder-node-test-utils/src/actions/mockActionContext.ts b/plugins/scaffolder-node-test-utils/src/actions/mockActionContext.ts index bb6e3917f1..a6703a12d1 100644 --- a/plugins/scaffolder-node-test-utils/src/actions/mockActionContext.ts +++ b/plugins/scaffolder-node-test-utils/src/actions/mockActionContext.ts @@ -30,13 +30,14 @@ import { loggerToWinstonLogger } from './loggerToWinstonLogger'; * @public * @param options - optional parameters to override default mock context */ -export const createMockActionContext = < +export function createMockActionContext< TActionInput extends JsonObject = JsonObject, - TActionOutput extends JsonObject = JsonObject, + TActionOutput extends JsonObject = any, >( options?: Partial>, -): ActionContext => { +): ActionContext { const credentials = mockCredentials.user(); + const defaultContext = { logger: loggerToWinstonLogger(mockServices.logger.mock()), logStream: new PassThrough(), @@ -66,16 +67,8 @@ export const createMockActionContext = < }; } - const { - input, - logger, - logStream, - secrets, - templateInfo, - workspacePath, - task, - user, - } = options; + const { input, logger, secrets, templateInfo, workspacePath, task, user } = + options; return { ...defaultContext, @@ -84,11 +77,10 @@ export const createMockActionContext = < createTemporaryDirectory: jest.fn().mockResolvedValue(workspacePath), }), ...(logger && { logger }), - ...(logStream && { logStream }), ...(input && { input }), ...(secrets && { secrets }), ...(task && { task }), ...(user && { user }), templateInfo, }; -}; +} diff --git a/plugins/scaffolder-node/report.api.md b/plugins/scaffolder-node/report.api.md index 08fc2ddb12..a23a9137b1 100644 --- a/plugins/scaffolder-node/report.api.md +++ b/plugins/scaffolder-node/report.api.md @@ -7,7 +7,6 @@ import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { Expand } from '@backstage/types'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; -import { Logger } from 'winston'; import { LoggerService } from '@backstage/backend-plugin-api'; import { Observable } from '@backstage/types'; import { Schema } from 'jsonschema'; @@ -25,63 +24,34 @@ import { z } from 'zod'; export type ActionContext< TActionInput extends JsonObject, TActionOutput extends JsonObject = JsonObject, - TSchemaType extends 'v1' | 'v2' = 'v1', -> = TSchemaType extends 'v2' - ? { - logger: LoggerService; - secrets?: TaskSecrets; - workspacePath: string; - input: TActionInput; - checkpoint(opts: { - key: string; - fn: () => Promise | T; - }): Promise; - output( - name: keyof TActionOutput, - value: TActionOutput[keyof TActionOutput], - ): void; - createTemporaryDirectory(): Promise; - getInitiatorCredentials(): Promise; - task: { - id: string; - }; - templateInfo?: TemplateInfo; - isDryRun?: boolean; - user?: { - entity?: UserEntity; - ref?: string; - }; - signal?: AbortSignal; - each?: JsonObject; - } - : { - logger: Logger; - logStream: Writable; - secrets?: TaskSecrets; - workspacePath: string; - input: TActionInput; - checkpoint(opts: { - key: string; - fn: () => Promise | T; - }): Promise; - output( - name: keyof TActionOutput, - value: TActionOutput[keyof TActionOutput], - ): void; - createTemporaryDirectory(): Promise; - getInitiatorCredentials(): Promise; - task: { - id: string; - }; - templateInfo?: TemplateInfo; - isDryRun?: boolean; - user?: { - entity?: UserEntity; - ref?: string; - }; - signal?: AbortSignal; - each?: JsonObject; - }; + _TSchemaType extends 'v2' = 'v2', +> = { + logger: LoggerService; + secrets?: TaskSecrets; + workspacePath: string; + input: TActionInput; + checkpoint(opts: { + key: string; + fn: () => Promise | T; + }): Promise; + output( + name: keyof TActionOutput, + value: TActionOutput[keyof TActionOutput], + ): void; + createTemporaryDirectory(): Promise; + getInitiatorCredentials(): Promise; + task: { + id: string; + }; + templateInfo?: TemplateInfo; + isDryRun?: boolean; + user?: { + entity?: UserEntity; + ref?: string; + }; + signal?: AbortSignal; + each?: JsonObject; +}; // @public (undocumented) export function addFiles(options: { @@ -180,69 +150,67 @@ export function createBranch(options: { logger?: LoggerService | undefined; }): Promise; -// @public @deprecated (undocumented) -export function createTemplateAction< - TInputParams extends JsonObject = JsonObject, - TOutputParams extends JsonObject = JsonObject, - TInputSchema extends JsonObject = JsonObject, - TOutputSchema extends JsonObject = JsonObject, - TActionInput extends JsonObject = TInputParams, - TActionOutput extends JsonObject = TOutputParams, ->( - action: TemplateActionOptions< - TActionInput, - TActionOutput, - TInputSchema, - TOutputSchema, - 'v1' - >, -): TemplateAction; - -// @public @deprecated (undocumented) -export function createTemplateAction< - TInputParams extends JsonObject = JsonObject, - TOutputParams extends JsonObject = JsonObject, - TInputSchema extends z.ZodType = z.ZodType, - TOutputSchema extends z.ZodType = z.ZodType, - TActionInput extends JsonObject = z.infer, - TActionOutput extends JsonObject = z.infer, ->( - action: TemplateActionOptions< - TActionInput, - TActionOutput, - TInputSchema, - TOutputSchema, - 'v1' - >, -): TemplateAction; - // @public export function createTemplateAction< - TInputSchema extends { - [key in string]: (zImpl: typeof z) => z.ZodType; - }, - TOutputSchema extends { - [key in string]: (zImpl: typeof z) => z.ZodType; - }, + TInputSchema extends + | { + [key in string]: (zImpl: typeof z) => z.ZodType; + } + | ((zImpl: typeof z) => z.ZodType), + TOutputSchema extends + | { + [key in string]: (zImpl: typeof z) => z.ZodType; + } + | ((zImpl: typeof z) => z.ZodType), >( action: TemplateActionOptions< - { - [key in keyof TInputSchema]: z.infer>; - }, - { - [key in keyof TOutputSchema]: z.infer>; - }, + TInputSchema extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + } + ? { + [key in keyof TInputSchema]: z.infer>; + } + : TInputSchema extends (zImpl: typeof z) => z.ZodType + ? z.infer> + : never, + TOutputSchema extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + } + ? { + [key in keyof TOutputSchema]: z.infer>; + } + : TOutputSchema extends (zImpl: typeof z) => z.ZodType + ? z.infer> + : never, TInputSchema, TOutputSchema, 'v2' >, ): TemplateAction< - FlattenOptionalProperties<{ - [key in keyof TInputSchema]: z.output>; - }>, - FlattenOptionalProperties<{ - [key in keyof TOutputSchema]: z.output>; - }>, + FlattenOptionalProperties< + TInputSchema extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + } + ? { + [key in keyof TInputSchema]: z.output>; + } + : TInputSchema extends (zImpl: typeof z) => z.ZodType + ? z.output> + : never + >, + FlattenOptionalProperties< + TOutputSchema extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + } + ? { + [key in keyof TOutputSchema]: z.output< + ReturnType + >; + } + : TOutputSchema extends (zImpl: typeof z) => z.ZodType + ? z.output> + : never + >, 'v2' >; @@ -513,7 +481,7 @@ export type TaskStatus = export type TemplateAction< TActionInput extends JsonObject = JsonObject, TActionOutput extends JsonObject = JsonObject, - TSchemaType extends 'v1' | 'v2' = 'v1', + TSchemaType extends 'v2' = 'v2', > = { id: string; description?: string; @@ -536,18 +504,20 @@ export type TemplateActionOptions< TActionInput extends JsonObject = {}, TActionOutput extends JsonObject = {}, TInputSchema extends - | JsonObject - | z.ZodType | { [key in string]: (zImpl: typeof z) => z.ZodType; - } = JsonObject, + } + | ((zImpl: typeof z) => z.ZodType) = { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, TOutputSchema extends - | JsonObject - | z.ZodType | { [key in string]: (zImpl: typeof z) => z.ZodType; - } = JsonObject, - TSchemaType extends 'v1' | 'v2' = 'v1' | 'v2', + } + | ((zImpl: typeof z) => z.ZodType) = { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, + TSchemaType extends 'v2' = 'v2', > = { id: string; description?: string; diff --git a/plugins/scaffolder-node/src/actions/createTemplateAction.test.ts b/plugins/scaffolder-node/src/actions/createTemplateAction.test.ts index 43d5d2f660..b4ed095f03 100644 --- a/plugins/scaffolder-node/src/actions/createTemplateAction.test.ts +++ b/plugins/scaffolder-node/src/actions/createTemplateAction.test.ts @@ -14,85 +14,8 @@ * limitations under the License. */ import { createTemplateAction } from './createTemplateAction'; -import { z } from 'zod'; describe('createTemplateAction', () => { - it('should allow creating with jsonschema and use the old deprecated types', () => { - const action = createTemplateAction<{ repoUrl: string }, { test: string }>({ - id: 'test', - schema: { - input: { - type: 'object', - required: ['repoUrl'], - properties: { - repoUrl: { type: 'string' }, - }, - }, - output: { - type: 'object', - required: ['test'], - properties: { - test: { type: 'string' }, - }, - }, - }, - handler: async ctx => { - // @ts-expect-error - repoUrl is string - const a: number = ctx.input.repoUrl; - - const b: string = ctx.input.repoUrl; - expect(b).toBeDefined(); - - const stream = ctx.logStream; - expect(stream).toBeDefined(); - - ctx.output('test', 'value'); - - // @ts-expect-error - not valid output type - ctx.output('test', 4); - - // @ts-expect-error - not valid output name - ctx.output('test2', 'value'); - }, - }); - - expect(action).toBeDefined(); - }); - - it('should allow creating with zod and use the old deprecated types', () => { - const action = createTemplateAction({ - id: 'test', - schema: { - input: z.object({ - repoUrl: z.string(), - }), - output: z.object({ - test: z.string(), - }), - }, - handler: async ctx => { - // @ts-expect-error - repoUrl is string - const a: number = ctx.input.repoUrl; - - const b: string = ctx.input.repoUrl; - expect(b).toBeDefined(); - - const stream = ctx.logStream; - expect(stream).toBeDefined(); - - ctx.output('test', 'value'); - - // @ts-expect-error - not valid output type - ctx.output('test', 4); - - // @ts-expect-error - not valid output name - ctx.output('test2', 'value'); - }, - }); - - expect(action).toBeDefined(); - }); - it('should allow creating with new first class zod support', () => { const action = createTemplateAction({ id: 'test', @@ -107,14 +30,10 @@ describe('createTemplateAction', () => { handler: async ctx => { // @ts-expect-error - repoUrl is string const a: number = ctx.input.repoUrl; - const b: string = ctx.input.repoUrl; - expect(b).toBeDefined(); - // @ts-expect-error - logStream is not available - const stream = ctx.logStream; - - expect(stream).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + [a, b]; ctx.output('test', 'value'); @@ -128,4 +47,56 @@ describe('createTemplateAction', () => { expect(action).toBeDefined(); }); + + it('should allow creating with a function for input and output schema for more complex types', () => { + const action = createTemplateAction({ + id: 'test', + schema: { + input: z => + z.union([ + z.object({ + repoUrl: z.string(), + }), + z.object({ + numberThing: z.number(), + }), + ]), + output: z => + z.object({ + test: z.string(), + }), + }, + handler: async ctx => { + ctx.output('test', 'value'); + + // @ts-expect-error - not valid output type + ctx.output('test', 4); + + // @ts-expect-error - not valid output name + ctx.output('test2', 'value'); + + if ('repoUrl' in ctx.input) { + // @ts-expect-error - not valid input type + const a: number = ctx.input.repoUrl; + + const b: string = ctx.input.repoUrl; + + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + [a, b]; + } + + if ('numberThing' in ctx.input) { + const a: number = ctx.input.numberThing; + + // @ts-expect-error - not valid input type + const b: string = ctx.input.numberThing; + + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + [a, b]; + } + }, + }); + + expect(action).toBeDefined(); + }); }); diff --git a/plugins/scaffolder-node/src/actions/createTemplateAction.ts b/plugins/scaffolder-node/src/actions/createTemplateAction.ts index ce38f3b4e1..edf3e203b9 100644 --- a/plugins/scaffolder-node/src/actions/createTemplateAction.ts +++ b/plugins/scaffolder-node/src/actions/createTemplateAction.ts @@ -30,14 +30,18 @@ export type TemplateActionOptions< TActionInput extends JsonObject = {}, TActionOutput extends JsonObject = {}, TInputSchema extends - | JsonObject - | z.ZodType - | { [key in string]: (zImpl: typeof z) => z.ZodType } = JsonObject, + | { [key in string]: (zImpl: typeof z) => z.ZodType } + | ((zImpl: typeof z) => z.ZodType) = { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, TOutputSchema extends - | JsonObject - | z.ZodType - | { [key in string]: (zImpl: typeof z) => z.ZodType } = JsonObject, - TSchemaType extends 'v1' | 'v2' = 'v1' | 'v2', + | { + [key in string]: (zImpl: typeof z) => z.ZodType; + } + | ((zImpl: typeof z) => z.ZodType) = { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, + TSchemaType extends 'v2' = 'v2', > = { id: string; description?: string; @@ -62,109 +66,82 @@ type FlattenOptionalProperties = Expand< [K in keyof T as undefined extends T[K] ? K : never]?: T[K]; } >; - -/** - * @public - * @deprecated migrate to using the new built in zod schema definitions for schemas - */ -export function createTemplateAction< - TInputParams extends JsonObject = JsonObject, - TOutputParams extends JsonObject = JsonObject, - TInputSchema extends JsonObject = JsonObject, - TOutputSchema extends JsonObject = JsonObject, - TActionInput extends JsonObject = TInputParams, - TActionOutput extends JsonObject = TOutputParams, ->( - action: TemplateActionOptions< - TActionInput, - TActionOutput, - TInputSchema, - TOutputSchema, - 'v1' - >, -): TemplateAction; -/** - * @public - * @deprecated migrate to using the new built in zod schema definitions for schemas - */ -export function createTemplateAction< - TInputParams extends JsonObject = JsonObject, - TOutputParams extends JsonObject = JsonObject, - TInputSchema extends z.ZodType = z.ZodType, - TOutputSchema extends z.ZodType = z.ZodType, - TActionInput extends JsonObject = z.infer, - TActionOutput extends JsonObject = z.infer, ->( - action: TemplateActionOptions< - TActionInput, - TActionOutput, - TInputSchema, - TOutputSchema, - 'v1' - >, -): TemplateAction; /** * This function is used to create new template actions to get type safety. * Will convert zod schemas to json schemas for use throughout the system. * @public */ export function createTemplateAction< - TInputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }, - TOutputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }, + TInputSchema extends + | { [key in string]: (zImpl: typeof z) => z.ZodType } + | ((zImpl: typeof z) => z.ZodType), + TOutputSchema extends + | { [key in string]: (zImpl: typeof z) => z.ZodType } + | ((zImpl: typeof z) => z.ZodType), >( action: TemplateActionOptions< - { - [key in keyof TInputSchema]: z.infer>; - }, - { - [key in keyof TOutputSchema]: z.infer>; - }, + TInputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType } + ? { + [key in keyof TInputSchema]: z.infer>; + } + : TInputSchema extends (zImpl: typeof z) => z.ZodType + ? z.infer> + : never, + TOutputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType } + ? { + [key in keyof TOutputSchema]: z.infer>; + } + : TOutputSchema extends (zImpl: typeof z) => z.ZodType + ? z.infer> + : never, TInputSchema, TOutputSchema, 'v2' >, ): TemplateAction< - FlattenOptionalProperties<{ - [key in keyof TInputSchema]: z.output>; - }>, - FlattenOptionalProperties<{ - [key in keyof TOutputSchema]: z.output>; - }>, + FlattenOptionalProperties< + TInputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType } + ? { + [key in keyof TInputSchema]: z.output>; + } + : TInputSchema extends (zImpl: typeof z) => z.ZodType + ? z.output> + : never + >, + FlattenOptionalProperties< + TOutputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType } + ? { + [key in keyof TOutputSchema]: z.output< + ReturnType + >; + } + : TOutputSchema extends (zImpl: typeof z) => z.ZodType + ? z.output> + : never + >, 'v2' >; export function createTemplateAction< - TInputParams extends JsonObject = JsonObject, - TOutputParams extends JsonObject = JsonObject, - TInputSchema extends - | JsonObject - | z.ZodType - | { [key in string]: (zImpl: typeof z) => z.ZodType } = JsonObject, - TOutputSchema extends - | JsonObject - | z.ZodType - | { [key in string]: (zImpl: typeof z) => z.ZodType } = JsonObject, - TActionInput extends JsonObject = TInputSchema extends z.ZodType< - any, - any, - infer IReturn - > - ? IReturn - : TInputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType } + TInputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType } = { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, + TOutputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType } = { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, + TActionInput extends JsonObject = TInputSchema extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + } ? Expand<{ [key in keyof TInputSchema]: z.infer>; }> - : TInputParams, - TActionOutput extends JsonObject = TOutputSchema extends z.ZodType< - any, - any, - infer IReturn - > - ? IReturn - : TOutputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType } + : never, + TActionOutput extends JsonObject = TOutputSchema extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + } ? Expand<{ [key in keyof TOutputSchema]: z.infer>; }> - : TOutputParams, + : never, >( action: TemplateActionOptions< TActionInput, @@ -172,13 +149,7 @@ export function createTemplateAction< TInputSchema, TOutputSchema >, -): TemplateAction< - TActionInput, - TActionOutput, - TInputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType } - ? 'v2' - : 'v1' -> { +): TemplateAction { const { inputSchema, outputSchema } = parseSchemas( action as TemplateActionOptions, ); diff --git a/plugins/scaffolder-node/src/actions/types.ts b/plugins/scaffolder-node/src/actions/types.ts index 92a2c7464a..f13d256332 100644 --- a/plugins/scaffolder-node/src/actions/types.ts +++ b/plugins/scaffolder-node/src/actions/types.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import { Logger } from 'winston'; -import { Writable } from 'stream'; import { JsonObject, JsonValue } from '@backstage/types'; import { TaskSecrets } from '../tasks'; import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; @@ -32,143 +30,75 @@ import { export type ActionContext< TActionInput extends JsonObject, TActionOutput extends JsonObject = JsonObject, - TSchemaType extends 'v1' | 'v2' = 'v1', -> = TSchemaType extends 'v2' - ? { - logger: LoggerService; - secrets?: TaskSecrets; - workspacePath: string; - input: TActionInput; - checkpoint(opts: { - key: string; - fn: () => Promise | T; - }): Promise; - output( - name: keyof TActionOutput, - value: TActionOutput[keyof TActionOutput], - ): void; - /** - * Creates a temporary directory for use by the action, which is then cleaned up automatically. - */ - createTemporaryDirectory(): Promise; + _TSchemaType extends 'v2' = 'v2', +> = { + logger: LoggerService; + secrets?: TaskSecrets; + workspacePath: string; + input: TActionInput; + checkpoint(opts: { + key: string; + fn: () => Promise | T; + }): Promise; + output( + name: keyof TActionOutput, + value: TActionOutput[keyof TActionOutput], + ): void; + /** + * Creates a temporary directory for use by the action, which is then cleaned up automatically. + */ + createTemporaryDirectory(): Promise; - /** - * Get the credentials for the current request - */ - getInitiatorCredentials(): Promise; + /** + * Get the credentials for the current request + */ + getInitiatorCredentials(): Promise; - /** - * Task information - */ - task: { - id: string; - }; + /** + * Task information + */ + task: { + id: string; + }; - templateInfo?: TemplateInfo; + templateInfo?: TemplateInfo; - /** - * Whether this action invocation is a dry-run or not. - * This will only ever be true if the actions as marked as supporting dry-runs. - */ - isDryRun?: boolean; + /** + * Whether this action invocation is a dry-run or not. + * This will only ever be true if the actions as marked as supporting dry-runs. + */ + isDryRun?: boolean; - /** - * The user which triggered the action. - */ - user?: { - /** - * The decorated entity from the Catalog - */ - entity?: UserEntity; - /** - * An entity ref for the author of the task - */ - ref?: string; - }; + /** + * The user which triggered the action. + */ + user?: { + /** + * The decorated entity from the Catalog + */ + entity?: UserEntity; + /** + * An entity ref for the author of the task + */ + ref?: string; + }; - /** - * Implement the signal to make your custom step abortable https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal - */ - signal?: AbortSignal; + /** + * Implement the signal to make your custom step abortable https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal + */ + signal?: AbortSignal; - /** - * Optional value of each invocation - */ - each?: JsonObject; - } - : /** @deprecated **/ - { - // TODO(blam): move this to LoggerService - logger: Logger; - /** @deprecated - use `ctx.logger` instead */ - logStream: Writable; - secrets?: TaskSecrets; - workspacePath: string; - input: TActionInput; - checkpoint(opts: { - key: string; - fn: () => Promise | T; - }): Promise; - output( - name: keyof TActionOutput, - value: TActionOutput[keyof TActionOutput], - ): void; - - /** - * Creates a temporary directory for use by the action, which is then cleaned up automatically. - */ - createTemporaryDirectory(): Promise; - - /** - * Get the credentials for the current request - */ - getInitiatorCredentials(): Promise; - - /** - * Task information - */ - task: { - id: string; - }; - - templateInfo?: TemplateInfo; - - /** - * Whether this action invocation is a dry-run or not. - * This will only ever be true if the actions as marked as supporting dry-runs. - */ - isDryRun?: boolean; - - /** - * The user which triggered the action. - */ - user?: { - /** - * The decorated entity from the Catalog - */ - entity?: UserEntity; - /** - * An entity ref for the author of the task - */ - ref?: string; - }; - - /** - * Implement the signal to make your custom step abortable https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal - */ - signal?: AbortSignal; - - /** - * Optional value of each invocation - */ - each?: JsonObject; - }; + /** + * Optional value of each invocation + */ + each?: JsonObject; +}; /** @public */ export type TemplateAction< TActionInput extends JsonObject = JsonObject, TActionOutput extends JsonObject = JsonObject, - TSchemaType extends 'v1' | 'v2' = 'v1', + TSchemaType extends 'v2' = 'v2', > = { id: string; description?: string; diff --git a/plugins/scaffolder-node/src/actions/util.ts b/plugins/scaffolder-node/src/actions/util.ts index 57141e9c24..34cc74c811 100644 --- a/plugins/scaffolder-node/src/actions/util.ts +++ b/plugins/scaffolder-node/src/actions/util.ts @@ -130,11 +130,7 @@ function checkRequiredParams(repoUrl: URL, ...params: string[]) { } } -const isZodSchema = (schema: unknown): schema is z.ZodType => { - return typeof schema === 'object' && !!schema && 'safeParseAsync' in schema; -}; - -const isNativeZodSchema = ( +const isKeyValueZodCallback = ( schema: unknown, ): schema is { [key in string]: (zImpl: typeof z) => z.ZodType } => { return ( @@ -144,23 +140,20 @@ const isNativeZodSchema = ( ); }; +const isZodFunctionDefinition = ( + schema: unknown, +): schema is (zImpl: typeof z) => z.ZodType => { + return typeof schema === 'function'; +}; + export const parseSchemas = ( - action: TemplateActionOptions, + action: TemplateActionOptions, ): { inputSchema?: Schema; outputSchema?: Schema } => { if (!action.schema) { return { inputSchema: undefined, outputSchema: undefined }; } - if (isZodSchema(action.schema.input)) { - return { - inputSchema: zodToJsonSchema(action.schema.input) as Schema, - outputSchema: isZodSchema(action.schema.output) - ? (zodToJsonSchema(action.schema.output) as Schema) - : undefined, - }; - } - - if (isNativeZodSchema(action.schema.input)) { + if (isKeyValueZodCallback(action.schema.input)) { const input = z.object( Object.fromEntries( Object.entries(action.schema.input).map(([k, v]) => [k, v(z)]), @@ -169,7 +162,7 @@ export const parseSchemas = ( return { inputSchema: zodToJsonSchema(input) as Schema, - outputSchema: isNativeZodSchema(action.schema.output) + outputSchema: isKeyValueZodCallback(action.schema.output) ? (zodToJsonSchema( z.object( Object.fromEntries( @@ -181,9 +174,18 @@ export const parseSchemas = ( }; } + if (isZodFunctionDefinition(action.schema.input)) { + return { + inputSchema: zodToJsonSchema(action.schema.input(z)) as Schema, + outputSchema: isZodFunctionDefinition(action.schema.output) + ? (zodToJsonSchema(action.schema.output(z)) as Schema) + : undefined, + }; + } + return { - inputSchema: action.schema.input, - outputSchema: action.schema.output, + inputSchema: undefined, + outputSchema: undefined, }; };