From 7d724d8ef56076bfb1e24e8191500dbc706c944c Mon Sep 17 00:00:00 2001 From: zcason Date: Mon, 2 Jan 2023 16:35:22 -0600 Subject: [PATCH 01/12] parent 31afee7de86a567cd5a713eba47b9b823bf87fae author zcason 1672698922 -0600 committer blam 1677246287 +0100 added zod-to-json-schema package Signed-off-by: zcason updated input types Signed-off-by: zcason updated the conversion function to return an object Signed-off-by: zcason zod converter function refactor Signed-off-by: zcason added changeset Signed-off-by: zcason zod to json package update Signed-off-by: zcason zod to json package update Signed-off-by: zcason removed comments Signed-off-by: zcason Signed-off-by: zcason Signed-off-by: zcason empty commit Signed-off-by: zcason moved zod converter into its own file Signed-off-by: zcason moved zod converter into its own file Signed-off-by: zcason updated report Signed-off-by: zcason added public tag Signed-off-by: zcason added public tag to exported function Signed-off-by: zcason updated changeset Signed-off-by: zcason removed zod to json module from global package Signed-off-by: zcason update zod to json module from scaffolder package.json Signed-off-by: zcason update zod to json module from scaffolder package.json Signed-off-by: zcason reverted zod to json module from scaffolder package.json Signed-off-by: zcason moved zod to json converter function into a new directory Signed-off-by: zcason refactored zod coverion function Signed-off-by: zcason updated changeset description Signed-off-by: zcason --- .changeset/afraid-trees-stare.md | 5 +++ plugins/scaffolder-backend/api-report.md | 6 ++- plugins/scaffolder-backend/package.json | 3 +- .../actions/builtin/catalog/write.ts | 30 +++++++------- .../scaffolder/actions/convertZodtoJson.ts | 41 +++++++++++++++++++ yarn.lock | 1 + 6 files changed, 67 insertions(+), 19 deletions(-) create mode 100644 .changeset/afraid-trees-stare.md create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/convertZodtoJson.ts diff --git a/.changeset/afraid-trees-stare.md b/.changeset/afraid-trees-stare.md new file mode 100644 index 0000000000..ac1a53b4e4 --- /dev/null +++ b/.changeset/afraid-trees-stare.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +zod to json conversion funcitonality for action template schema diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 293ff7ed29..a25da2045a 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -69,8 +69,10 @@ export function createCatalogRegisterAction(options: { } >; -// @public -export function createCatalogWriteAction(): TemplateAction_2<{ +// Warning: (ae-missing-release-tag) "createCatalogWriteAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function createCatalogWriteAction(): TemplateAction<{ filePath?: string | undefined; entity: Entity; }>; diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 12ef31899c..8a61c7dfac 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -92,7 +92,8 @@ "winston": "^3.2.1", "yaml": "^2.0.0", "zen-observable": "^0.10.0", - "zod": "~3.18.0" + "zod": "~3.18.0", + "zod-to-json-schema": "~3.18.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", 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 5c5a62956f..ad1b1eb0c3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts @@ -19,6 +19,8 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import * as yaml from 'yaml'; import { Entity } from '@backstage/catalog-model'; import { resolveSafeChildPath } from '@backstage/backend-common'; +import { z } from 'zod'; +import { convertZodtoJson } from '../../../tasks'; const id = 'catalog:write'; @@ -56,28 +58,24 @@ const examples = [ * Writes a catalog descriptor file containing the provided entity to a path in the workspace. * @public */ + export function createCatalogWriteAction() { + const inputSchema = convertZodtoJson( + z.object({ + filePath: z.string().optional().describe('Defaults to catalog-info.yaml'), + entity: z + .string() + .optional() + .describe('You can provide the same values used in the Entity schema.'), + }), + ); + return createTemplateAction<{ filePath?: string; entity: Entity }>({ id, description: 'Writes the catalog-info.yaml for your template', examples, schema: { - input: { - type: 'object', - properties: { - filePath: { - title: 'Catalog file path', - description: 'Defaults to catalog-info.yaml', - type: 'string', - }, - entity: { - title: 'Entity info to write catalog-info.yaml', - description: - 'You can provide the same values used in the Entity schema.', - type: 'object', - }, - }, - }, + input: inputSchema, }, supportsDryRun: true, async handler(ctx) { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/convertZodtoJson.ts b/plugins/scaffolder-backend/src/scaffolder/actions/convertZodtoJson.ts new file mode 100644 index 0000000000..71104814a3 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/convertZodtoJson.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { z } from 'zod'; +import zodToJsonSchema from 'zod-to-json-schema'; +import { Schema } from 'jsonschema'; + +interface TemplateActionSchema { + readonly schema: { + input?: Schema; + output?: Schema; + }; +} + +/** @public */ +export function convertZodtoJson< + TInputSchema extends z.ZodType = z.ZodType, + TOutputSchema extends z.ZodType = z.ZodType, +>( + zodInputSchema?: TInputSchema, + zodOutputSchema?: TOutputSchema, +): TemplateActionSchema { + return { + schema: { + input: zodInputSchema ? zodToJsonSchema(zodInputSchema) : undefined, + output: zodOutputSchema ? zodToJsonSchema(zodOutputSchema) : undefined, + }, + }; +} diff --git a/yarn.lock b/yarn.lock index d365859add..7c9bae5be5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7716,6 +7716,7 @@ __metadata: yaml: ^2.0.0 zen-observable: ^0.10.0 zod: ~3.18.0 + zod-to-json-schema: ~3.18.0 languageName: unknown linkType: soft From 10a64ff693edb3e9ef6e85bd0ab8c3c6345d033e Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 14 Feb 2023 14:31:17 +0100 Subject: [PATCH 02/12] feat: reworking zod integration a little bit to reduce the amount of typing you have to do Signed-off-by: blam --- .../api-report.md | 19 +- .../api-report.md | 15 +- .../api-report.md | 17 +- .../api-report.md | 13 +- plugins/scaffolder-backend/api-report.md | 739 ++++++++++-------- .../actions/builtin/catalog/write.ts | 31 +- .../scaffolder/actions/convertZodtoJson.ts | 41 - .../tasks/NunjucksWorkflowRunner.test.ts | 48 ++ plugins/scaffolder-node/api-report.md | 25 +- plugins/scaffolder-node/package.json | 3 +- .../src/actions/createTemplateAction.ts | 12 +- plugins/scaffolder-node/src/actions/types.ts | 17 +- yarn.lock | 1 + 13 files changed, 556 insertions(+), 425 deletions(-) delete mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/convertZodtoJson.ts diff --git a/plugins/scaffolder-backend-module-cookiecutter/api-report.md b/plugins/scaffolder-backend-module-cookiecutter/api-report.md index 76f2ced66f..1b409b2af0 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/api-report.md +++ b/plugins/scaffolder-backend-module-cookiecutter/api-report.md @@ -16,12 +16,15 @@ export function createFetchCookiecutterAction(options: { reader: UrlReader; integrations: ScmIntegrations; containerRunner: ContainerRunner; -}): TemplateAction<{ - url: string; - targetPath?: string | undefined; - values: JsonObject; - copyWithoutRender?: string[] | undefined; - extensions?: string[] | undefined; - imageName?: string | undefined; -}>; +}): TemplateAction< + { + url: string; + targetPath?: string | undefined; + values: JsonObject; + copyWithoutRender?: string[] | undefined; + extensions?: string[] | undefined; + imageName?: string | undefined; + }, + {} +>; ``` diff --git a/plugins/scaffolder-backend-module-rails/api-report.md b/plugins/scaffolder-backend-module-rails/api-report.md index c91daa1344..fc2e077055 100644 --- a/plugins/scaffolder-backend-module-rails/api-report.md +++ b/plugins/scaffolder-backend-module-rails/api-report.md @@ -15,10 +15,13 @@ export function createFetchRailsAction(options: { integrations: ScmIntegrations; containerRunner: ContainerRunner; allowedImageNames?: string[]; -}): TemplateAction<{ - url: string; - targetPath?: string | undefined; - values: JsonObject; - imageName?: string | undefined; -}>; +}): TemplateAction< + { + url: string; + targetPath?: string | undefined; + values: JsonObject; + imageName?: string | undefined; + }, + {} +>; ``` diff --git a/plugins/scaffolder-backend-module-sentry/api-report.md b/plugins/scaffolder-backend-module-sentry/api-report.md index ef09bdebc0..17e9a71fd5 100644 --- a/plugins/scaffolder-backend-module-sentry/api-report.md +++ b/plugins/scaffolder-backend-module-sentry/api-report.md @@ -9,13 +9,16 @@ import { TemplateAction } from '@backstage/plugin-scaffolder-node'; // @public export function createSentryCreateProjectAction(options: { config: Config; -}): TemplateAction<{ - organizationSlug: string; - teamSlug: string; - name: string; - slug?: string | undefined; - authToken?: string | undefined; -}>; +}): TemplateAction< + { + organizationSlug: string; + teamSlug: string; + name: string; + slug?: string | undefined; + authToken?: string | undefined; + }, + {} +>; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/scaffolder-backend-module-yeoman/api-report.md b/plugins/scaffolder-backend-module-yeoman/api-report.md index 562fe52ac2..e71dfddd93 100644 --- a/plugins/scaffolder-backend-module-yeoman/api-report.md +++ b/plugins/scaffolder-backend-module-yeoman/api-report.md @@ -7,9 +7,12 @@ import { JsonObject } from '@backstage/types'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; // @public -export function createRunYeomanAction(): TemplateAction<{ - namespace: string; - args?: string[] | undefined; - options?: JsonObject | undefined; -}>; +export function createRunYeomanAction(): TemplateAction< + { + namespace: string; + args?: string[] | undefined; + options?: JsonObject | undefined; + }, + {} +>; ``` diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index a25da2045a..c344d64094 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -24,6 +24,7 @@ import { Observable } from '@backstage/types'; import { Octokit } from 'octokit'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; +import { Schema } from 'jsonschema'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { ScmIntegrations } from '@backstage/integration'; import { SpawnOptionsWithoutStdio } from 'child_process'; @@ -33,6 +34,9 @@ import { TaskSpecV1beta3 } from '@backstage/plugin-scaffolder-common'; import { TemplateAction as TemplateAction_2 } from '@backstage/plugin-scaffolder-node'; import { UrlReader } from '@backstage/backend-common'; import { Writable } from 'stream'; +import { z } from 'zod'; +import { ZodType } from 'zod'; +import { ZodTypeDef } from 'zod'; // @public @deprecated (undocumented) export type ActionContext = ActionContext_2; @@ -66,39 +70,63 @@ export function createCatalogRegisterAction(options: { repoContentsUrl: string; catalogInfoPath?: string | undefined; optional?: boolean | undefined; - } + }, + {} >; -// Warning: (ae-missing-release-tag) "createCatalogWriteAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export function createCatalogWriteAction(): TemplateAction<{ - filePath?: string | undefined; - entity: Entity; -}>; // @public -export function createDebugLogAction(): TemplateAction_2<{ - message?: string | undefined; - listWorkspace?: boolean | undefined; -}>; +export function createCatalogWriteAction(): TemplateAction_2< + unknown, + z.ZodObject< + { + filePath: z.ZodOptional; + entity: z.ZodOptional>; + }, + 'strip', + z.ZodTypeAny, + { + filePath?: string | undefined; + entity?: {} | undefined; + }, + { + filePath?: string | undefined; + entity?: {} | undefined; + } + > +>; + +// @public +export function createDebugLogAction(): TemplateAction_2< + { + message?: string | undefined; + listWorkspace?: boolean | undefined; + }, + {} +>; // @public export function createFetchCatalogEntityAction(options: { catalogClient: CatalogApi; -}): TemplateAction_2<{ - entityRef: string; - optional?: boolean | undefined; -}>; +}): TemplateAction_2< + { + entityRef: string; + optional?: boolean | undefined; + }, + {} +>; // @public export function createFetchPlainAction(options: { reader: UrlReader; integrations: ScmIntegrations; -}): TemplateAction_2<{ - url: string; - targetPath?: string | undefined; -}>; +}): TemplateAction_2< + { + url: string; + targetPath?: string | undefined; + }, + {} +>; // @public export function createFetchTemplateAction(options: { @@ -106,57 +134,72 @@ export function createFetchTemplateAction(options: { integrations: ScmIntegrations; additionalTemplateFilters?: Record; additionalTemplateGlobals?: Record; -}): TemplateAction_2<{ - url: string; - targetPath?: string | undefined; - values: any; - templateFileExtension?: string | boolean | undefined; - copyWithoutRender?: string[] | undefined; - copyWithoutTemplating?: string[] | undefined; - cookiecutterCompat?: boolean | undefined; - replace?: boolean | undefined; -}>; +}): TemplateAction_2< + { + url: string; + targetPath?: string | undefined; + values: any; + templateFileExtension?: string | boolean | undefined; + copyWithoutRender?: string[] | undefined; + copyWithoutTemplating?: string[] | undefined; + cookiecutterCompat?: boolean | undefined; + replace?: boolean | undefined; + }, + {} +>; // @public -export const createFilesystemDeleteAction: () => TemplateAction_2<{ - files: string[]; -}>; +export const createFilesystemDeleteAction: () => TemplateAction_2< + { + files: string[]; + }, + {} +>; // @public -export const createFilesystemRenameAction: () => TemplateAction_2<{ - files: Array<{ - from: string; - to: string; - overwrite?: boolean; - }>; -}>; +export const createFilesystemRenameAction: () => TemplateAction_2< + { + files: Array<{ + from: string; + to: string; + overwrite?: boolean; + }>; + }, + {} +>; // @public export function createGithubActionsDispatchAction(options: { integrations: ScmIntegrations; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction_2<{ - repoUrl: string; - workflowId: string; - branchOrTagName: string; - workflowInputs?: - | { - [key: string]: string; - } - | undefined; - token?: string | undefined; -}>; +}): TemplateAction_2< + { + repoUrl: string; + workflowId: string; + branchOrTagName: string; + workflowInputs?: + | { + [key: string]: string; + } + | undefined; + token?: string | undefined; + }, + {} +>; // @public export function createGithubIssuesLabelAction(options: { integrations: ScmIntegrationRegistry; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction_2<{ - repoUrl: string; - number: number; - labels: string[]; - token?: string | undefined; -}>; +}): TemplateAction_2< + { + repoUrl: string; + number: number; + labels: string[]; + token?: string | undefined; + }, + {} +>; // @public export interface CreateGithubPullRequestActionOptions { @@ -181,344 +224,386 @@ export type CreateGithubPullRequestClientFactoryInput = { export function createGithubRepoCreateAction(options: { integrations: ScmIntegrationRegistry; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction_2<{ - repoUrl: string; - description?: string | undefined; - homepage?: string | undefined; - access?: string | undefined; - deleteBranchOnMerge?: boolean | undefined; - gitAuthorName?: string | undefined; - gitAuthorEmail?: string | undefined; - allowRebaseMerge?: boolean | undefined; - allowSquashMerge?: boolean | undefined; - squashMergeCommitTitle?: 'PR_TITLE' | 'COMMIT_OR_PR_TITLE' | undefined; - squashMergeCommitMessage?: - | 'PR_BODY' - | 'COMMIT_MESSAGES' - | 'BLANK' - | undefined; - allowMergeCommit?: boolean | undefined; - allowAutoMerge?: boolean | undefined; - requireCodeOwnerReviews?: boolean | undefined; - bypassPullRequestAllowances?: - | { - users?: string[] | undefined; - teams?: string[] | undefined; - apps?: string[] | undefined; - } - | undefined; - requiredApprovingReviewCount?: number | undefined; - restrictions?: - | { - users: string[]; - teams: string[]; - apps?: string[] | undefined; - } - | undefined; - requiredStatusCheckContexts?: string[] | undefined; - requireBranchesToBeUpToDate?: boolean | undefined; - requiredConversationResolution?: boolean | undefined; - repoVisibility?: 'internal' | 'private' | 'public' | undefined; - collaborators?: - | ( - | { - user: string; - access: string; - } - | { - team: string; - access: string; - } - | { - username: string; - access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; - } - )[] - | undefined; - hasProjects?: boolean | undefined; - hasWiki?: boolean | undefined; - hasIssues?: boolean | undefined; - token?: string | undefined; - topics?: string[] | undefined; - requireCommitSigning?: boolean | undefined; -}>; +}): TemplateAction_2< + { + repoUrl: string; + description?: string | undefined; + homepage?: string | undefined; + access?: string | undefined; + deleteBranchOnMerge?: boolean | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; + allowRebaseMerge?: boolean | undefined; + allowSquashMerge?: boolean | undefined; + squashMergeCommitTitle?: 'PR_TITLE' | 'COMMIT_OR_PR_TITLE' | undefined; + squashMergeCommitMessage?: + | 'PR_BODY' + | 'COMMIT_MESSAGES' + | 'BLANK' + | undefined; + allowMergeCommit?: boolean | undefined; + allowAutoMerge?: boolean | undefined; + requireCodeOwnerReviews?: boolean | undefined; + bypassPullRequestAllowances?: + | { + users?: string[] | undefined; + teams?: string[] | undefined; + apps?: string[] | undefined; + } + | undefined; + requiredApprovingReviewCount?: number | undefined; + restrictions?: + | { + users: string[]; + teams: string[]; + apps?: string[] | undefined; + } + | undefined; + requiredStatusCheckContexts?: string[] | undefined; + requireBranchesToBeUpToDate?: boolean | undefined; + requiredConversationResolution?: boolean | undefined; + repoVisibility?: 'internal' | 'private' | 'public' | undefined; + collaborators?: + | ( + | { + user: string; + access: string; + } + | { + team: string; + access: string; + } + | { + username: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + } + )[] + | undefined; + hasProjects?: boolean | undefined; + hasWiki?: boolean | undefined; + hasIssues?: boolean | undefined; + token?: string | undefined; + topics?: string[] | undefined; + requireCommitSigning?: boolean | undefined; + }, + {} +>; // @public export function createGithubRepoPushAction(options: { integrations: ScmIntegrationRegistry; config: Config; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction_2<{ - repoUrl: string; - description?: string | undefined; - defaultBranch?: string | undefined; - protectDefaultBranch?: boolean | undefined; - protectEnforceAdmins?: boolean | undefined; - gitCommitMessage?: string | undefined; - gitAuthorName?: string | undefined; - gitAuthorEmail?: string | undefined; - requireCodeOwnerReviews?: boolean | undefined; - dismissStaleReviews?: boolean | undefined; - bypassPullRequestAllowances?: - | { - users?: string[]; - teams?: string[]; - apps?: string[]; - } - | undefined; - requiredApprovingReviewCount?: number | undefined; - restrictions?: - | { - users: string[]; - teams: string[]; - apps?: string[]; - } - | undefined; - requiredStatusCheckContexts?: string[] | undefined; - requireBranchesToBeUpToDate?: boolean | undefined; - requiredConversationResolution?: boolean | undefined; - sourcePath?: string | undefined; - token?: string | undefined; - requiredCommitSigning?: boolean | undefined; -}>; +}): TemplateAction_2< + { + repoUrl: string; + description?: string | undefined; + defaultBranch?: string | undefined; + protectDefaultBranch?: boolean | undefined; + protectEnforceAdmins?: boolean | undefined; + gitCommitMessage?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; + requireCodeOwnerReviews?: boolean | undefined; + dismissStaleReviews?: boolean | undefined; + bypassPullRequestAllowances?: + | { + users?: string[]; + teams?: string[]; + apps?: string[]; + } + | undefined; + requiredApprovingReviewCount?: number | undefined; + restrictions?: + | { + users: string[]; + teams: string[]; + apps?: string[]; + } + | undefined; + requiredStatusCheckContexts?: string[] | undefined; + requireBranchesToBeUpToDate?: boolean | undefined; + requiredConversationResolution?: boolean | undefined; + sourcePath?: string | undefined; + token?: string | undefined; + requiredCommitSigning?: boolean | undefined; + }, + {} +>; // @public export function createGithubWebhookAction(options: { integrations: ScmIntegrationRegistry; defaultWebhookSecret?: string; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction_2<{ - repoUrl: string; - webhookUrl: string; - webhookSecret?: string | undefined; - events?: string[] | undefined; - active?: boolean | undefined; - contentType?: 'form' | 'json' | undefined; - insecureSsl?: boolean | undefined; - token?: string | undefined; -}>; +}): TemplateAction_2< + { + repoUrl: string; + webhookUrl: string; + webhookSecret?: string | undefined; + events?: string[] | undefined; + active?: boolean | undefined; + contentType?: 'form' | 'json' | undefined; + insecureSsl?: boolean | undefined; + token?: string | undefined; + }, + {} +>; // @public export function createPublishAzureAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction_2<{ - repoUrl: string; - description?: string | undefined; - defaultBranch?: string | undefined; - sourcePath?: string | undefined; - token?: string | undefined; - gitCommitMessage?: string | undefined; - gitAuthorName?: string | undefined; - gitAuthorEmail?: string | undefined; -}>; +}): TemplateAction_2< + { + repoUrl: string; + description?: string | undefined; + defaultBranch?: string | undefined; + sourcePath?: string | undefined; + token?: string | undefined; + gitCommitMessage?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; + }, + {} +>; // @public @deprecated export function createPublishBitbucketAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction_2<{ - repoUrl: string; - description?: string | undefined; - defaultBranch?: string | undefined; - repoVisibility?: 'private' | 'public' | undefined; - sourcePath?: string | undefined; - enableLFS?: boolean | undefined; - token?: string | undefined; - gitCommitMessage?: string | undefined; - gitAuthorName?: string | undefined; - gitAuthorEmail?: string | undefined; -}>; +}): TemplateAction_2< + { + repoUrl: string; + description?: string | undefined; + defaultBranch?: string | undefined; + repoVisibility?: 'private' | 'public' | undefined; + sourcePath?: string | undefined; + enableLFS?: boolean | undefined; + token?: string | undefined; + gitCommitMessage?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; + }, + {} +>; // @public export function createPublishBitbucketCloudAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction_2<{ - repoUrl: string; - description?: string | undefined; - defaultBranch?: string | undefined; - repoVisibility?: 'private' | 'public' | undefined; - sourcePath?: string | undefined; - token?: string | undefined; -}>; +}): TemplateAction_2< + { + repoUrl: string; + description?: string | undefined; + defaultBranch?: string | undefined; + repoVisibility?: 'private' | 'public' | undefined; + sourcePath?: string | undefined; + token?: string | undefined; + }, + {} +>; // @public export function createPublishBitbucketServerAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction_2<{ - repoUrl: string; - description?: string | undefined; - defaultBranch?: string | undefined; - repoVisibility?: 'private' | 'public' | undefined; - sourcePath?: string | undefined; - enableLFS?: boolean | undefined; - token?: string | undefined; - gitCommitMessage?: string | undefined; - gitAuthorName?: string | undefined; - gitAuthorEmail?: string | undefined; -}>; +}): TemplateAction_2< + { + repoUrl: string; + description?: string | undefined; + defaultBranch?: string | undefined; + repoVisibility?: 'private' | 'public' | undefined; + sourcePath?: string | undefined; + enableLFS?: boolean | undefined; + token?: string | undefined; + gitCommitMessage?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; + }, + {} +>; // @public export function createPublishGerritAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction_2<{ - repoUrl: string; - description: string; - defaultBranch?: string | undefined; - gitCommitMessage?: string | undefined; - gitAuthorName?: string | undefined; - gitAuthorEmail?: string | undefined; - sourcePath?: string | undefined; -}>; +}): TemplateAction_2< + { + repoUrl: string; + description: string; + defaultBranch?: string | undefined; + gitCommitMessage?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; + sourcePath?: string | undefined; + }, + {} +>; // @public export function createPublishGerritReviewAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction_2<{ - repoUrl: string; - branch?: string | undefined; - sourcePath?: string | undefined; - gitCommitMessage?: string | undefined; - gitAuthorName?: string | undefined; - gitAuthorEmail?: string | undefined; -}>; +}): TemplateAction_2< + { + repoUrl: string; + branch?: string | undefined; + sourcePath?: string | undefined; + gitCommitMessage?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; + }, + {} +>; // @public export function createPublishGithubAction(options: { integrations: ScmIntegrationRegistry; config: Config; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction_2<{ - repoUrl: string; - description?: string | undefined; - homepage?: string | undefined; - access?: string | undefined; - defaultBranch?: string | undefined; - protectDefaultBranch?: boolean | undefined; - protectEnforceAdmins?: boolean | undefined; - deleteBranchOnMerge?: boolean | undefined; - gitCommitMessage?: string | undefined; - gitAuthorName?: string | undefined; - gitAuthorEmail?: string | undefined; - allowRebaseMerge?: boolean | undefined; - allowSquashMerge?: boolean | undefined; - squashMergeCommitTitle?: 'PR_TITLE' | 'COMMIT_OR_PR_TITLE' | undefined; - squashMergeCommitMessage?: - | 'PR_BODY' - | 'COMMIT_MESSAGES' - | 'BLANK' - | undefined; - allowMergeCommit?: boolean | undefined; - allowAutoMerge?: boolean | undefined; - sourcePath?: string | undefined; - bypassPullRequestAllowances?: - | { - users?: string[]; - teams?: string[]; - apps?: string[]; - } - | undefined; - requiredApprovingReviewCount?: number | undefined; - restrictions?: - | { - users: string[]; - teams: string[]; - apps?: string[]; - } - | undefined; - requireCodeOwnerReviews?: boolean | undefined; - dismissStaleReviews?: boolean | undefined; - requiredStatusCheckContexts?: string[] | undefined; - requireBranchesToBeUpToDate?: boolean | undefined; - requiredConversationResolution?: boolean | undefined; - repoVisibility?: 'internal' | 'private' | 'public' | undefined; - collaborators?: - | ( - | { - user: string; - access: string; - } - | { - team: string; - access: string; - } - | { - username: string; - access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; - } - )[] - | undefined; - hasProjects?: boolean | undefined; - hasWiki?: boolean | undefined; - hasIssues?: boolean | undefined; - token?: string | undefined; - topics?: string[] | undefined; - requiredCommitSigning?: boolean | undefined; -}>; +}): TemplateAction_2< + { + repoUrl: string; + description?: string | undefined; + homepage?: string | undefined; + access?: string | undefined; + defaultBranch?: string | undefined; + protectDefaultBranch?: boolean | undefined; + protectEnforceAdmins?: boolean | undefined; + deleteBranchOnMerge?: boolean | undefined; + gitCommitMessage?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; + allowRebaseMerge?: boolean | undefined; + allowSquashMerge?: boolean | undefined; + squashMergeCommitTitle?: 'PR_TITLE' | 'COMMIT_OR_PR_TITLE' | undefined; + squashMergeCommitMessage?: + | 'PR_BODY' + | 'COMMIT_MESSAGES' + | 'BLANK' + | undefined; + allowMergeCommit?: boolean | undefined; + allowAutoMerge?: boolean | undefined; + sourcePath?: string | undefined; + bypassPullRequestAllowances?: + | { + users?: string[]; + teams?: string[]; + apps?: string[]; + } + | undefined; + requiredApprovingReviewCount?: number | undefined; + restrictions?: + | { + users: string[]; + teams: string[]; + apps?: string[]; + } + | undefined; + requireCodeOwnerReviews?: boolean | undefined; + dismissStaleReviews?: boolean | undefined; + requiredStatusCheckContexts?: string[] | undefined; + requireBranchesToBeUpToDate?: boolean | undefined; + requiredConversationResolution?: boolean | undefined; + repoVisibility?: 'internal' | 'private' | 'public' | undefined; + collaborators?: + | ( + | { + user: string; + access: string; + } + | { + team: string; + access: string; + } + | { + username: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + } + )[] + | undefined; + hasProjects?: boolean | undefined; + hasWiki?: boolean | undefined; + hasIssues?: boolean | undefined; + token?: string | undefined; + topics?: string[] | undefined; + requiredCommitSigning?: boolean | undefined; + }, + {} +>; // @public export const createPublishGithubPullRequestAction: ({ integrations, githubCredentialsProvider, clientFactory, -}: CreateGithubPullRequestActionOptions) => TemplateAction_2<{ - title: string; - branchName: string; - description: string; - repoUrl: string; - draft?: boolean | undefined; - targetPath?: string | undefined; - sourcePath?: string | undefined; - token?: string | undefined; - reviewers?: string[] | undefined; - teamReviewers?: string[] | undefined; -}>; +}: CreateGithubPullRequestActionOptions) => TemplateAction_2< + { + title: string; + branchName: string; + description: string; + repoUrl: string; + draft?: boolean | undefined; + targetPath?: string | undefined; + sourcePath?: string | undefined; + token?: string | undefined; + reviewers?: string[] | undefined; + teamReviewers?: string[] | undefined; + }, + {} +>; // @public export function createPublishGitlabAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction_2<{ - repoUrl: string; - defaultBranch?: string | undefined; - repoVisibility?: 'internal' | 'private' | 'public' | undefined; - sourcePath?: string | undefined; - token?: string | undefined; - gitCommitMessage?: string | undefined; - gitAuthorName?: string | undefined; - gitAuthorEmail?: string | undefined; - setUserAsOwner?: boolean | undefined; - topics?: string[] | undefined; -}>; +}): TemplateAction_2< + { + repoUrl: string; + defaultBranch?: string | undefined; + repoVisibility?: 'internal' | 'private' | 'public' | undefined; + sourcePath?: string | undefined; + token?: string | undefined; + gitCommitMessage?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; + setUserAsOwner?: boolean | undefined; + topics?: string[] | undefined; + }, + {} +>; // @public export const createPublishGitlabMergeRequestAction: (options: { integrations: ScmIntegrationRegistry; -}) => TemplateAction_2<{ - repoUrl: string; - title: string; - description: string; - branchName: string; - sourcePath?: string | undefined; - targetPath?: string | undefined; - token?: string | undefined; - commitAction?: 'update' | 'delete' | 'create' | undefined; - projectid?: string | undefined; - removeSourceBranch?: boolean | undefined; - assignee?: string | undefined; -}>; +}) => TemplateAction_2< + { + repoUrl: string; + title: string; + description: string; + branchName: string; + sourcePath?: string | undefined; + targetPath?: string | undefined; + token?: string | undefined; + commitAction?: 'update' | 'delete' | 'create' | undefined; + projectid?: string | undefined; + removeSourceBranch?: boolean | undefined; + assignee?: string | undefined; + }, + {} +>; // @public export function createRouter(options: RouterOptions): Promise; // @public @deprecated (undocumented) -export const createTemplateAction: ( - templateAction: TemplateAction_2, -) => TemplateAction_2; +export const createTemplateAction: < + TParams, + TInputSchema extends ZodType | Schema = {}, +>( + templateAction: TemplateAction_2, +) => TemplateAction_2; // @public export type CreateWorkerOptions = { 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 ad1b1eb0c3..3094f4e692 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts @@ -17,10 +17,8 @@ import fs from 'fs-extra'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import * as yaml from 'yaml'; -import { Entity } from '@backstage/catalog-model'; import { resolveSafeChildPath } from '@backstage/backend-common'; import { z } from 'zod'; -import { convertZodtoJson } from '../../../tasks'; const id = 'catalog:write'; @@ -60,23 +58,24 @@ const examples = [ */ export function createCatalogWriteAction() { - const inputSchema = convertZodtoJson( - z.object({ - filePath: z.string().optional().describe('Defaults to catalog-info.yaml'), - entity: z - .string() - .optional() - .describe('You can provide the same values used in the Entity schema.'), - }), - ); - - return createTemplateAction<{ filePath?: string; entity: Entity }>({ - id, + return createTemplateAction({ + id: 'catalog:write', description: 'Writes the catalog-info.yaml for your template', - examples, schema: { - input: inputSchema, + input: z.object({ + filePath: z + .string() + .optional() + .describe('Defaults to catalog-info.yaml'), + entity: z + .object({}) + .optional() + .describe( + 'You can provide the same values used in the Entity schema.', + ), + }), }, + examples, supportsDryRun: true, async handler(ctx) { ctx.logStream.write(`Writing catalog-info.yaml`); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/convertZodtoJson.ts b/plugins/scaffolder-backend/src/scaffolder/actions/convertZodtoJson.ts deleted file mode 100644 index 71104814a3..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/actions/convertZodtoJson.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2023 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { z } from 'zod'; -import zodToJsonSchema from 'zod-to-json-schema'; -import { Schema } from 'jsonschema'; - -interface TemplateActionSchema { - readonly schema: { - input?: Schema; - output?: Schema; - }; -} - -/** @public */ -export function convertZodtoJson< - TInputSchema extends z.ZodType = z.ZodType, - TOutputSchema extends z.ZodType = z.ZodType, ->( - zodInputSchema?: TInputSchema, - zodOutputSchema?: TOutputSchema, -): TemplateActionSchema { - return { - schema: { - input: zodInputSchema ? zodToJsonSchema(zodInputSchema) : undefined, - output: zodOutputSchema ? zodToJsonSchema(zodOutputSchema) : undefined, - }, - }; -} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index 453cfae83b..8bfbf8c7e5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -26,6 +26,7 @@ import { TaskContext } from './types'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; import { UserEntity } from '@backstage/catalog-model'; +import { z } from 'zod'; // The Stream module is lazy loaded, so make sure it's in the module cache before mocking fs void winston.transports.Stream; @@ -103,6 +104,18 @@ describe('DefaultWorkflowRunner', () => { }, }); + actionRegistry.register({ + id: 'jest-zod-validated-action', + description: 'Mock action for testing', + supportsDryRun: true, + handler: fakeActionHandler, + schema: { + input: z.object({ + foo: z.number(), + }), + }, + }); + actionRegistry.register({ id: 'output-action', description: 'Mock action for testing', @@ -151,6 +164,41 @@ describe('DefaultWorkflowRunner', () => { ); }); + it('should throw an error if the action has a zod schema and the input does not match', async () => { + const task = createMockTaskWithSpec({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + parameters: {}, + output: {}, + steps: [ + { id: 'test', name: 'name', action: 'jest-zod-validated-action' }, + ], + }); + + await expect(runner.execute(task)).rejects.toThrow( + /Invalid input passed to action jest-validated-action, instance requires property \"foo\"/, + ); + }); + + it('should run the action when the zod validation passes', async () => { + const task = createMockTaskWithSpec({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + parameters: {}, + output: {}, + steps: [ + { + id: 'test', + name: 'name', + action: 'jest-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({ apiVersion: 'scaffolder.backstage.io/v1beta3', diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md index 506ea0517d..977445f765 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -13,6 +13,7 @@ import { Schema } from 'jsonschema'; import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; import { UserEntity } from '@backstage/catalog-model'; import { Writable } from 'stream'; +import { z } from 'zod'; // @public export type ActionContext = { @@ -32,9 +33,12 @@ export type ActionContext = { }; // @public -export const createTemplateAction: ( - templateAction: TemplateAction, -) => TemplateAction; +export const createTemplateAction: < + TParams, + TInputSchema extends z.ZodType | Schema = {}, +>( + templateAction: TemplateAction, +) => TemplateAction; // @alpha export interface ScaffolderActionsExtensionPoint { @@ -51,7 +55,10 @@ export type TaskSecrets = Record & { }; // @public (undocumented) -export type TemplateAction = { +export type TemplateAction< + TParams, + TInputSchema extends Schema | z.ZodType = {}, +> = { id: string; description?: string; examples?: { @@ -60,9 +67,15 @@ export type TemplateAction = { }[]; supportsDryRun?: boolean; schema?: { - input?: Schema; + input?: TInputSchema; output?: Schema; }; - handler: (ctx: ActionContext) => Promise; + handler: ( + ctx: ActionContext< + TInputSchema extends z.ZodType + ? IReturn + : TParams + >, + ) => Promise; }; ``` diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json index 642a59b801..791376f7d0 100644 --- a/plugins/scaffolder-node/package.json +++ b/plugins/scaffolder-node/package.json @@ -29,7 +29,8 @@ "@backstage/plugin-scaffolder-common": "workspace:^", "@backstage/types": "workspace:^", "jsonschema": "^1.2.6", - "winston": "^3.2.1" + "winston": "^3.2.1", + "zod": "~3.18.0" }, "devDependencies": { "@backstage/cli": "workspace:^" diff --git a/plugins/scaffolder-node/src/actions/createTemplateAction.ts b/plugins/scaffolder-node/src/actions/createTemplateAction.ts index 24d88a1681..2228ddb904 100644 --- a/plugins/scaffolder-node/src/actions/createTemplateAction.ts +++ b/plugins/scaffolder-node/src/actions/createTemplateAction.ts @@ -14,17 +14,21 @@ * limitations under the License. */ -import { JsonObject } from '@backstage/types'; import { TemplateAction } from './types'; +import { z } from 'zod'; +import { Schema } from 'jsonschema'; /** * This function is used to create new template actions to get type safety. * * @public */ -export const createTemplateAction = ( - templateAction: TemplateAction, -): TemplateAction => { +export const createTemplateAction = < + TParams, + TInputSchema extends Schema | z.ZodType = {}, +>( + templateAction: TemplateAction, +): TemplateAction => { // TODO(blam): Can add some more validation here to validate the action later on return templateAction; }; diff --git a/plugins/scaffolder-node/src/actions/types.ts b/plugins/scaffolder-node/src/actions/types.ts index 5a4f5796a1..e1d5096d03 100644 --- a/plugins/scaffolder-node/src/actions/types.ts +++ b/plugins/scaffolder-node/src/actions/types.ts @@ -21,7 +21,7 @@ import { Schema } from 'jsonschema'; import { TaskSecrets } from '../tasks/types'; import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; import { UserEntity } from '@backstage/catalog-model'; - +import { z } from 'zod'; /** * ActionContext is passed into scaffolder actions. * @public @@ -63,14 +63,23 @@ export type ActionContext = { }; /** @public */ -export type TemplateAction = { +export type TemplateAction< + TParams, + TInputSchema extends Schema | z.ZodType = {}, +> = { id: string; description?: string; examples?: { description: string; example: string }[]; supportsDryRun?: boolean; schema?: { - input?: Schema; + input?: TInputSchema; output?: Schema; }; - handler: (ctx: ActionContext) => Promise; + handler: ( + ctx: ActionContext< + TInputSchema extends z.ZodType + ? IReturn + : TParams + >, + ) => Promise; }; diff --git a/yarn.lock b/yarn.lock index 7c9bae5be5..bf4403c54f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7741,6 +7741,7 @@ __metadata: "@backstage/types": "workspace:^" jsonschema: ^1.2.6 winston: ^3.2.1 + zod: ~3.18.0 languageName: unknown linkType: soft From 1bbd6dfe67f5a3681306ffdac8ec368f1a3576ac Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 23 Feb 2023 12:07:04 +0100 Subject: [PATCH 03/12] chore: implemented zod parsing in test Signed-off-by: blam --- .../scaffolder/tasks/NunjucksWorkflowRunner.test.ts | 2 +- .../src/scaffolder/tasks/NunjucksWorkflowRunner.ts | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index 8bfbf8c7e5..0ab251208c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -175,7 +175,7 @@ describe('DefaultWorkflowRunner', () => { }); await expect(runner.execute(task)).rejects.toThrow( - /Invalid input passed to action jest-validated-action, instance requires property \"foo\"/, + /Invalid input passed to action jest-zod-validated-action, instance requires property \"foo\"/, ); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 516abe79e4..defed86d83 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -25,6 +25,7 @@ import { InputError } from '@backstage/errors'; import { PassThrough } from 'stream'; import { generateExampleOutput, isTruthy } from './helper'; import { validate as validateJsonSchema } from 'jsonschema'; +import zodToJsonSchema from 'zod-to-json-schema'; import { parseRepoUrl } from '../actions/builtin/publish/util'; import { TemplateActionRegistry } from '../actions'; import { @@ -41,6 +42,7 @@ import { import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { UserEntity } from '@backstage/catalog-model'; import { createCounterMetric, createHistogramMetric } from '../../util/metrics'; +import { z } from 'zod'; type NunjucksWorkflowRunnerOptions = { workingDirectory: string; @@ -286,10 +288,12 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { {}; if (action.schema?.input) { - const validateResult = validateJsonSchema( - input, - action.schema.input, - ); + const inputSchema = + action.schema.input instanceof z.ZodSchema + ? zodToJsonSchema(action.schema.input) + : action.schema.input; + + const validateResult = validateJsonSchema(input, inputSchema); if (!validateResult.valid) { const errors = validateResult.errors.join(', '); throw new InputError( From b239c818ff991548f5b1e0b769a8ac313b616ded Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 23 Feb 2023 12:13:41 +0100 Subject: [PATCH 04/12] chore: change the validation lookup Signed-off-by: blam --- .../src/scaffolder/tasks/NunjucksWorkflowRunner.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index defed86d83..3172a2cdae 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -42,7 +42,6 @@ import { import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { UserEntity } from '@backstage/catalog-model'; import { createCounterMetric, createHistogramMetric } from '../../util/metrics'; -import { z } from 'zod'; type NunjucksWorkflowRunnerOptions = { workingDirectory: string; @@ -288,8 +287,9 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { {}; if (action.schema?.input) { + // Check to see if the input is a zod schema without using instanceof. const inputSchema = - action.schema.input instanceof z.ZodSchema + 'safeParseAsync' in action.schema.input ? zodToJsonSchema(action.schema.input) : action.schema.input; From b1edb1669ebb3da184364e92e5cce187536b968d Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 23 Feb 2023 12:34:16 +0100 Subject: [PATCH 05/12] chore: move the zod logic Signed-off-by: blam --- .../actions/TemplateActionRegistry.ts | 19 +++++++++++++++++-- .../tasks/NunjucksWorkflowRunner.ts | 11 ++++------- .../scaffolder-backend/src/service/router.ts | 2 +- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts b/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts index d044d095cd..90e32ba6ee 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts @@ -17,7 +17,7 @@ import { JsonObject } from '@backstage/types'; import { ConflictError, NotFoundError } from '@backstage/errors'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; - +import zodToJsonSchema from 'zod-to-json-schema'; /** * Registry of all registered template actions. * @public @@ -31,7 +31,22 @@ export class TemplateActionRegistry { `Template action with ID '${action.id}' has already been registered`, ); } - this.actions.set(action.id, action); + + // It's better to convert the zod here, and just deal with jsonschema everywhere + // rather than adding the zod check everywhere like the nunjucks engine, and the /actions/list + // endpoint to create jsonschema for the frontend. + const templateAction = + action.schema?.input && 'safeParseAsync' in action.schema.input + ? { + ...action, + schema: { + ...action.schema, + input: zodToJsonSchema(action.schema.input), + }, + } + : action; + + this.actions.set(action.id, templateAction); } get(actionId: string): TemplateAction { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 3172a2cdae..541531d159 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -287,13 +287,10 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { {}; if (action.schema?.input) { - // Check to see if the input is a zod schema without using instanceof. - const inputSchema = - 'safeParseAsync' in action.schema.input - ? zodToJsonSchema(action.schema.input) - : action.schema.input; - - const validateResult = validateJsonSchema(input, inputSchema); + const validateResult = validateJsonSchema( + input, + action.schema.input, + ); if (!validateResult.valid) { const errors = validateResult.errors.join(', '); throw new InputError( diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index c339de8d10..1dc7036450 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -293,7 +293,7 @@ export async function createRouter( id: action.id, description: action.description, examples: action.examples, - schema: action.schema, + schema: {}, }; }); res.json(actionsList); From f86eb7f17d24903f173e0c2fd411d25d4e472ef3 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 23 Feb 2023 14:20:35 +0100 Subject: [PATCH 06/12] chore: finishing off zod zupport Signed-off-by: blam --- .changeset/afraid-trees-stare.md | 2 +- .../writing-custom-actions.md | 74 ++++++++++++------- .../api-report.md | 1 + .../api-report.md | 1 + .../api-report.md | 1 + .../api-report.md | 1 + plugins/scaffolder-backend/api-report.md | 36 +++++++-- .../actions/TemplateActionRegistry.ts | 35 +++++---- .../tasks/NunjucksWorkflowRunner.ts | 1 - plugins/scaffolder-node/api-report.md | 10 ++- .../src/actions/createTemplateAction.ts | 7 +- plugins/scaffolder-node/src/actions/types.ts | 5 +- 12 files changed, 117 insertions(+), 57 deletions(-) diff --git a/.changeset/afraid-trees-stare.md b/.changeset/afraid-trees-stare.md index ac1a53b4e4..d8ee3489c0 100644 --- a/.changeset/afraid-trees-stare.md +++ b/.changeset/afraid-trees-stare.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder-backend': minor --- -zod to json conversion funcitonality for action template schema +Added the ability to be able to define actions input schema using `zod` instead of hand writing types and jsonschema for the input of actions. diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index ffe3312501..65e2111f15 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -24,8 +24,53 @@ passed as `input` to the function. In `packages/backend/src/plugins/scaffolder/actions/custom.ts` we can create a new action. -```ts -import { createTemplateAction } from '@backstage/plugin-scaffolder-backend'; +```ts title="With Zod" +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; +import fs from 'fs-extra'; +import { z } from 'zod'; + +export const createNewFileAction = () => { + return createTemplateAction({ + id: 'mycompany:create-file', + schema: { + input: z.object({ + contents: z.string().describe('The contents of the file'), + filename: z + .string() + .describe('The filename of the file that will be created'), + }), + }, + + async handler(ctx) { + await fs.outputFile( + `${ctx.workspacePath}/${ctx.input.filename}`, + ctx.input.contents, + ); + }, + }); +}; +``` + +So let's break this down. The `createNewFileAction` is a function that returns a +`createTemplateAction`, and it's a good place to pass in dependencies which +close over the `TemplateAction`. Take a look at our +[built-in actions](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/src/scaffolder/actions/builtin) +for reference. + +The `createTemplateAction` takes an object which specifies the following: + +- `id` - a unique ID for your custom action. We encourage you to namespace these + in some way so that they won't collide with future built-in actions that we + may ship with the `scaffolder-backend` plugin. +- `schema.input` - A `zod` or JSON schema object for input values to your function +- `schema.output` - A `zod` or JSON schema object for values which are outputted from the + function using `ctx.output` +- `handler` - the actual code which is run part of the action, with a context + +You can also choose to define your custom action using JSON schema instead of `zod`: + +```ts title="With JSON Schema" +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import fs from 'fs-extra'; export const createNewFileAction = () => { @@ -59,27 +104,6 @@ export const createNewFileAction = () => { }; ``` -So let's break this down. The `createNewFileAction` is a function that returns a -`createTemplateAction`, and it's a good place to pass in dependencies which -close over the `TemplateAction`. Take a look at our -[built-in actions](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/src/scaffolder/actions/builtin) -for reference. - -We set the type generic to `{ contents: string, filename: string }` which is -there to set the type on the handler `ctx` `inputs` property so we get good type -checking. This could be generated from the next part of this guide, the `input` -schema, but it's not supported right now. Feel free to contribute 🚀 👍. - -The `createTemplateAction` takes an object which specifies the following: - -- `id` - a unique ID for your custom action. We encourage you to namespace these - in some way so that they won't collide with future built-in actions that we - may ship with the `scaffolder-backend` plugin. -- `schema.input` - A JSON schema for input values to your function -- `schema.output` - A JSON schema for values which are outputted from the - function using `ctx.output` -- `handler` - the actual code which is run part of the action, with a context - ### The context object When the action `handler` is called, we provide you a `context` as the only @@ -89,10 +113,10 @@ argument. It looks like the following: - `ctx.logger` - a Winston logger for additional logging inside your action - `ctx.logStream` - a stream version of the logger if needed - `ctx.workspacePath` - a string of the working directory of the template run -- `ctx.input` - an object which should match the JSON schema provided in the +- `ctx.input` - an object which should match the `zod` or JSON schema provided in the `schema.input` part of the action definition - `ctx.output` - a function which you can call to set outputs that match the - JSON schema in `schema.output` for ex. `ctx.output('downloadUrl', something)` + JSON schema or `zod` in `schema.output` for ex. `ctx.output('downloadUrl', myDownloadUrl)` - `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` diff --git a/plugins/scaffolder-backend-module-cookiecutter/api-report.md b/plugins/scaffolder-backend-module-cookiecutter/api-report.md index 1b409b2af0..554732104d 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/api-report.md +++ b/plugins/scaffolder-backend-module-cookiecutter/api-report.md @@ -25,6 +25,7 @@ export function createFetchCookiecutterAction(options: { extensions?: string[] | undefined; imageName?: string | undefined; }, + {}, {} >; ``` diff --git a/plugins/scaffolder-backend-module-rails/api-report.md b/plugins/scaffolder-backend-module-rails/api-report.md index fc2e077055..c910d5ae79 100644 --- a/plugins/scaffolder-backend-module-rails/api-report.md +++ b/plugins/scaffolder-backend-module-rails/api-report.md @@ -22,6 +22,7 @@ export function createFetchRailsAction(options: { values: JsonObject; imageName?: string | undefined; }, + {}, {} >; ``` diff --git a/plugins/scaffolder-backend-module-sentry/api-report.md b/plugins/scaffolder-backend-module-sentry/api-report.md index 17e9a71fd5..c0ae268339 100644 --- a/plugins/scaffolder-backend-module-sentry/api-report.md +++ b/plugins/scaffolder-backend-module-sentry/api-report.md @@ -17,6 +17,7 @@ export function createSentryCreateProjectAction(options: { slug?: string | undefined; authToken?: string | undefined; }, + {}, {} >; diff --git a/plugins/scaffolder-backend-module-yeoman/api-report.md b/plugins/scaffolder-backend-module-yeoman/api-report.md index e71dfddd93..7e70ea8580 100644 --- a/plugins/scaffolder-backend-module-yeoman/api-report.md +++ b/plugins/scaffolder-backend-module-yeoman/api-report.md @@ -13,6 +13,7 @@ export function createRunYeomanAction(): TemplateAction< args?: string[] | undefined; options?: JsonObject | undefined; }, + {}, {} >; ``` diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index c344d64094..0169770649 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -71,6 +71,7 @@ export function createCatalogRegisterAction(options: { catalogInfoPath?: string | undefined; optional?: boolean | undefined; }, + {}, {} >; @@ -93,7 +94,8 @@ export function createCatalogWriteAction(): TemplateAction_2< filePath?: string | undefined; entity?: {} | undefined; } - > + >, + {} >; // @public @@ -102,6 +104,7 @@ export function createDebugLogAction(): TemplateAction_2< message?: string | undefined; listWorkspace?: boolean | undefined; }, + {}, {} >; @@ -113,6 +116,7 @@ export function createFetchCatalogEntityAction(options: { entityRef: string; optional?: boolean | undefined; }, + {}, {} >; @@ -125,6 +129,7 @@ export function createFetchPlainAction(options: { url: string; targetPath?: string | undefined; }, + {}, {} >; @@ -145,6 +150,7 @@ export function createFetchTemplateAction(options: { cookiecutterCompat?: boolean | undefined; replace?: boolean | undefined; }, + {}, {} >; @@ -153,6 +159,7 @@ export const createFilesystemDeleteAction: () => TemplateAction_2< { files: string[]; }, + {}, {} >; @@ -165,6 +172,7 @@ export const createFilesystemRenameAction: () => TemplateAction_2< overwrite?: boolean; }>; }, + {}, {} >; @@ -184,6 +192,7 @@ export function createGithubActionsDispatchAction(options: { | undefined; token?: string | undefined; }, + {}, {} >; @@ -198,6 +207,7 @@ export function createGithubIssuesLabelAction(options: { labels: string[]; token?: string | undefined; }, + {}, {} >; @@ -286,6 +296,7 @@ export function createGithubRepoCreateAction(options: { topics?: string[] | undefined; requireCommitSigning?: boolean | undefined; }, + {}, {} >; @@ -328,6 +339,7 @@ export function createGithubRepoPushAction(options: { token?: string | undefined; requiredCommitSigning?: boolean | undefined; }, + {}, {} >; @@ -347,6 +359,7 @@ export function createGithubWebhookAction(options: { insecureSsl?: boolean | undefined; token?: string | undefined; }, + {}, {} >; @@ -365,6 +378,7 @@ export function createPublishAzureAction(options: { gitAuthorName?: string | undefined; gitAuthorEmail?: string | undefined; }, + {}, {} >; @@ -385,6 +399,7 @@ export function createPublishBitbucketAction(options: { gitAuthorName?: string | undefined; gitAuthorEmail?: string | undefined; }, + {}, {} >; @@ -401,6 +416,7 @@ export function createPublishBitbucketCloudAction(options: { sourcePath?: string | undefined; token?: string | undefined; }, + {}, {} >; @@ -421,6 +437,7 @@ export function createPublishBitbucketServerAction(options: { gitAuthorName?: string | undefined; gitAuthorEmail?: string | undefined; }, + {}, {} >; @@ -438,6 +455,7 @@ export function createPublishGerritAction(options: { gitAuthorEmail?: string | undefined; sourcePath?: string | undefined; }, + {}, {} >; @@ -454,6 +472,7 @@ export function createPublishGerritReviewAction(options: { gitAuthorName?: string | undefined; gitAuthorEmail?: string | undefined; }, + {}, {} >; @@ -530,6 +549,7 @@ export function createPublishGithubAction(options: { topics?: string[] | undefined; requiredCommitSigning?: boolean | undefined; }, + {}, {} >; @@ -551,6 +571,7 @@ export const createPublishGithubPullRequestAction: ({ reviewers?: string[] | undefined; teamReviewers?: string[] | undefined; }, + {}, {} >; @@ -571,6 +592,7 @@ export function createPublishGitlabAction(options: { setUserAsOwner?: boolean | undefined; topics?: string[] | undefined; }, + {}, {} >; @@ -591,6 +613,7 @@ export const createPublishGitlabMergeRequestAction: (options: { removeSourceBranch?: boolean | undefined; assignee?: string | undefined; }, + {}, {} >; @@ -601,9 +624,10 @@ export function createRouter(options: RouterOptions): Promise; export const createTemplateAction: < TParams, TInputSchema extends ZodType | Schema = {}, + TOutputSchema extends ZodType | Schema = {}, >( - templateAction: TemplateAction_2, -) => TemplateAction_2; + templateAction: TemplateAction_2, +) => TemplateAction_2; // @public export type CreateWorkerOptions = { @@ -950,11 +974,11 @@ export type TemplateAction = // @public export class TemplateActionRegistry { // (undocumented) - get(actionId: string): TemplateAction_2; + get(actionId: string): TemplateAction_2; // (undocumented) - list(): TemplateAction_2[]; + list(): TemplateAction_2[]; // (undocumented) - register(action: TemplateAction_2): void; + register(action: TemplateAction_2): void; } // @public (undocumented) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts b/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts index 90e32ba6ee..3964c4b1f2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { JsonObject } from '@backstage/types'; import { ConflictError, NotFoundError } from '@backstage/errors'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import zodToJsonSchema from 'zod-to-json-schema'; @@ -23,9 +22,9 @@ import zodToJsonSchema from 'zod-to-json-schema'; * @public */ export class TemplateActionRegistry { - private readonly actions = new Map>(); + private readonly actions = new Map(); - register(action: TemplateAction) { + register(action: TemplateAction) { if (this.actions.has(action.id)) { throw new ConflictError( `Template action with ID '${action.id}' has already been registered`, @@ -35,21 +34,29 @@ export class TemplateActionRegistry { // It's better to convert the zod here, and just deal with jsonschema everywhere // rather than adding the zod check everywhere like the nunjucks engine, and the /actions/list // endpoint to create jsonschema for the frontend. - const templateAction = + const inputSchema = action.schema?.input && 'safeParseAsync' in action.schema.input - ? { - ...action, - schema: { - ...action.schema, - input: zodToJsonSchema(action.schema.input), - }, - } - : action; + ? zodToJsonSchema(action.schema.input) + : action.schema?.input; + + const outputSchema = + action.schema?.output && 'safeParseAsync' in action.schema.output + ? zodToJsonSchema(action.schema.output) + : action.schema?.output; + + const templateAction = { + ...action, + schema: { + ...action.schema, + input: inputSchema, + output: outputSchema, + }, + }; this.actions.set(action.id, templateAction); } - get(actionId: string): TemplateAction { + get(actionId: string): TemplateAction { const action = this.actions.get(actionId); if (!action) { throw new NotFoundError( @@ -59,7 +66,7 @@ export class TemplateActionRegistry { return action; } - list(): TemplateAction[] { + list(): TemplateAction[] { return [...this.actions.values()]; } } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 541531d159..516abe79e4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -25,7 +25,6 @@ import { InputError } from '@backstage/errors'; import { PassThrough } from 'stream'; import { generateExampleOutput, isTruthy } from './helper'; import { validate as validateJsonSchema } from 'jsonschema'; -import zodToJsonSchema from 'zod-to-json-schema'; import { parseRepoUrl } from '../actions/builtin/publish/util'; import { TemplateActionRegistry } from '../actions'; import { diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md index 977445f765..7e949030b6 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -36,9 +36,10 @@ export type ActionContext = { export const createTemplateAction: < TParams, TInputSchema extends z.ZodType | Schema = {}, + TOutputSchema extends z.ZodType | Schema = {}, >( - templateAction: TemplateAction, -) => TemplateAction; + templateAction: TemplateAction, +) => TemplateAction; // @alpha export interface ScaffolderActionsExtensionPoint { @@ -56,8 +57,9 @@ export type TaskSecrets = Record & { // @public (undocumented) export type TemplateAction< - TParams, + TParams = {}, TInputSchema extends Schema | z.ZodType = {}, + TOutputSchema extends Schema | z.ZodType = {}, > = { id: string; description?: string; @@ -68,7 +70,7 @@ export type TemplateAction< supportsDryRun?: boolean; schema?: { input?: TInputSchema; - output?: Schema; + output?: TOutputSchema; }; handler: ( ctx: ActionContext< diff --git a/plugins/scaffolder-node/src/actions/createTemplateAction.ts b/plugins/scaffolder-node/src/actions/createTemplateAction.ts index 2228ddb904..11c01a6086 100644 --- a/plugins/scaffolder-node/src/actions/createTemplateAction.ts +++ b/plugins/scaffolder-node/src/actions/createTemplateAction.ts @@ -17,7 +17,6 @@ import { TemplateAction } from './types'; import { z } from 'zod'; import { Schema } from 'jsonschema'; - /** * This function is used to create new template actions to get type safety. * @@ -26,9 +25,9 @@ import { Schema } from 'jsonschema'; export const createTemplateAction = < TParams, TInputSchema extends Schema | z.ZodType = {}, + TOutputSchema extends Schema | z.ZodType = {}, >( - templateAction: TemplateAction, -): TemplateAction => { - // TODO(blam): Can add some more validation here to validate the action later on + templateAction: TemplateAction, +): TemplateAction => { return templateAction; }; diff --git a/plugins/scaffolder-node/src/actions/types.ts b/plugins/scaffolder-node/src/actions/types.ts index e1d5096d03..2adad00bba 100644 --- a/plugins/scaffolder-node/src/actions/types.ts +++ b/plugins/scaffolder-node/src/actions/types.ts @@ -64,8 +64,9 @@ export type ActionContext = { /** @public */ export type TemplateAction< - TParams, + TParams = {}, TInputSchema extends Schema | z.ZodType = {}, + TOutputSchema extends Schema | z.ZodType = {}, > = { id: string; description?: string; @@ -73,7 +74,7 @@ export type TemplateAction< supportsDryRun?: boolean; schema?: { input?: TInputSchema; - output?: Schema; + output?: TOutputSchema; }; handler: ( ctx: ActionContext< From 38745ac76bd9e1457ec01b47bebdf709ad2dd002 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 23 Feb 2023 14:28:05 +0100 Subject: [PATCH 07/12] chore: fix changeset Signed-off-by: blam --- .changeset/afraid-trees-stare.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/afraid-trees-stare.md b/.changeset/afraid-trees-stare.md index d8ee3489c0..748bbb3cfe 100644 --- a/.changeset/afraid-trees-stare.md +++ b/.changeset/afraid-trees-stare.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder-backend': minor --- -Added the ability to be able to define actions input schema using `zod` instead of hand writing types and jsonschema for the input of actions. +Added the ability to be able to define an actions `input` and `output` schema using `zod` instead of hand writing types and `jsonschema` From 14d1a325b7a2d90988b1e99afbc73483e7400a39 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 24 Feb 2023 13:56:50 +0100 Subject: [PATCH 08/12] chore: reworking the types a little bit Signed-off-by: blam --- plugins/scaffolder-backend/api-report.md | 7 +-- plugins/scaffolder-backend/package.json | 3 +- .../actions/TemplateActionRegistry.ts | 25 +-------- .../actions/builtin/createBuiltinActions.ts | 5 +- .../dryrun/DecoratedActionsRegistry.ts | 5 +- .../tasks/NunjucksWorkflowRunner.test.ts | 29 +++++----- .../tasks/NunjucksWorkflowRunner.ts | 7 +-- .../scaffolder-backend/src/service/router.ts | 4 +- plugins/scaffolder-node/api-report.md | 25 +++++++-- plugins/scaffolder-node/package.json | 3 +- .../src/actions/createTemplateAction.ts | 54 +++++++++++++++++-- plugins/scaffolder-node/src/actions/index.ts | 5 +- plugins/scaffolder-node/src/actions/types.ts | 25 +++------ yarn.lock | 2 +- 14 files changed, 116 insertions(+), 83 deletions(-) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 0169770649..a8133b0bd7 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -32,6 +32,7 @@ import { TaskSecrets as TaskSecrets_2 } from '@backstage/plugin-scaffolder-node' import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { TaskSpecV1beta3 } from '@backstage/plugin-scaffolder-common'; import { TemplateAction as TemplateAction_2 } from '@backstage/plugin-scaffolder-node'; +import { TemplateActionOptions } from '@backstage/plugin-scaffolder-node'; import { UrlReader } from '@backstage/backend-common'; import { Writable } from 'stream'; import { z } from 'zod'; @@ -44,7 +45,7 @@ export type ActionContext = ActionContext_2; // @public export const createBuiltinActions: ( options: CreateBuiltInActionsOptions, -) => TemplateAction_2[]; +) => TemplateAction_2[]; // @public export interface CreateBuiltInActionsOptions { @@ -626,7 +627,7 @@ export const createTemplateAction: < TInputSchema extends ZodType | Schema = {}, TOutputSchema extends ZodType | Schema = {}, >( - templateAction: TemplateAction_2, + action: TemplateActionOptions, ) => TemplateAction_2; // @public @@ -725,7 +726,7 @@ export type OctokitWithPullRequestPluginClient = Octokit & { // @public export interface RouterOptions { // (undocumented) - actions?: TemplateAction_2[]; + actions?: TemplateAction_2[]; // (undocumented) additionalTemplateFilters?: Record; // (undocumented) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 8a61c7dfac..12ef31899c 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -92,8 +92,7 @@ "winston": "^3.2.1", "yaml": "^2.0.0", "zen-observable": "^0.10.0", - "zod": "~3.18.0", - "zod-to-json-schema": "~3.18.0" + "zod": "~3.18.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts b/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts index 3964c4b1f2..3868380919 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts @@ -16,7 +16,6 @@ import { ConflictError, NotFoundError } from '@backstage/errors'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import zodToJsonSchema from 'zod-to-json-schema'; /** * Registry of all registered template actions. * @public @@ -31,29 +30,7 @@ export class TemplateActionRegistry { ); } - // It's better to convert the zod here, and just deal with jsonschema everywhere - // rather than adding the zod check everywhere like the nunjucks engine, and the /actions/list - // endpoint to create jsonschema for the frontend. - const inputSchema = - action.schema?.input && 'safeParseAsync' in action.schema.input - ? zodToJsonSchema(action.schema.input) - : action.schema?.input; - - const outputSchema = - action.schema?.output && 'safeParseAsync' in action.schema.output - ? zodToJsonSchema(action.schema.output) - : action.schema?.output; - - const templateAction = { - ...action, - schema: { - ...action.schema, - input: inputSchema, - output: outputSchema, - }, - }; - - this.actions.set(action.id, templateAction); + this.actions.set(action.id, action); } get(actionId: string): TemplateAction { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index d83d9c5b07..9ba4a27b1a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -22,7 +22,6 @@ import { GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; -import { JsonObject } from '@backstage/types'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { createCatalogRegisterAction, @@ -95,7 +94,7 @@ export interface CreateBuiltInActionsOptions { */ export const createBuiltinActions = ( options: CreateBuiltInActionsOptions, -): TemplateAction[] => { +): TemplateAction[] => { const { reader, integrations, @@ -188,5 +187,5 @@ export const createBuiltinActions = ( }), ]; - return actions as TemplateAction[]; + return actions as TemplateAction[]; }; diff --git a/plugins/scaffolder-backend/src/scaffolder/dryrun/DecoratedActionsRegistry.ts b/plugins/scaffolder-backend/src/scaffolder/dryrun/DecoratedActionsRegistry.ts index c02375554c..7109a6621e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/dryrun/DecoratedActionsRegistry.ts +++ b/plugins/scaffolder-backend/src/scaffolder/dryrun/DecoratedActionsRegistry.ts @@ -15,14 +15,13 @@ */ import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { JsonObject } from '@backstage/types'; import { TemplateActionRegistry } from '../actions'; /** @internal */ export class DecoratedActionsRegistry extends TemplateActionRegistry { constructor( private readonly innerRegistry: TemplateActionRegistry, - extraActions: Array>, + extraActions: Array, ) { super(); for (const action of extraActions) { @@ -30,7 +29,7 @@ export class DecoratedActionsRegistry extends TemplateActionRegistry { } } - get(actionId: string): TemplateAction { + get(actionId: string): TemplateAction { try { return super.get(actionId); } catch { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index 0ab251208c..135ed4473e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -24,7 +24,10 @@ import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { TaskContext } from './types'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; -import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; +import { + createTemplateAction, + TaskSecrets, +} from '@backstage/plugin-scaffolder-node'; import { UserEntity } from '@backstage/catalog-model'; import { z } from 'zod'; @@ -104,17 +107,19 @@ describe('DefaultWorkflowRunner', () => { }, }); - actionRegistry.register({ - id: 'jest-zod-validated-action', - description: 'Mock action for testing', - supportsDryRun: true, - handler: fakeActionHandler, - schema: { - input: z.object({ - foo: z.number(), - }), - }, - }); + actionRegistry.register( + createTemplateAction({ + id: 'jest-zod-validated-action', + description: 'Mock action for testing', + supportsDryRun: true, + handler: fakeActionHandler, + schema: { + input: z.object({ + foo: z.number(), + }), + }, + }), + ); actionRegistry.register({ id: 'output-action', diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 516abe79e4..31d3479026 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -24,7 +24,7 @@ import { JsonObject, JsonValue } from '@backstage/types'; import { InputError } from '@backstage/errors'; import { PassThrough } from 'stream'; import { generateExampleOutput, isTruthy } from './helper'; -import { validate as validateJsonSchema } from 'jsonschema'; +import { Schema, validate as validateJsonSchema } from 'jsonschema'; import { parseRepoUrl } from '../actions/builtin/publish/util'; import { TemplateActionRegistry } from '../actions'; import { @@ -380,10 +380,7 @@ function scaffoldingTracker() { template, }); - async function skipDryRun( - step: TaskStep, - action: TemplateAction, - ) { + async function skipDryRun(step: TaskStep, action: TemplateAction) { task.emitLog(`Skipping because ${action.id} does not support dry-run`, { stepId: step.id, status: 'skipped', diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 1dc7036450..b219c81219 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -67,7 +67,7 @@ export interface RouterOptions { catalogClient: CatalogApi; scheduler?: PluginTaskScheduler; - actions?: TemplateAction[]; + actions?: TemplateAction[]; /** * @deprecated taskWorkers is deprecated in favor of concurrentTasksLimit option with a single TaskWorker * @defaultValue 1 @@ -293,7 +293,7 @@ export async function createRouter( id: action.id, description: action.description, examples: action.examples, - schema: {}, + schema: action.schema, }; }); res.json(actionsList); diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md index 7e949030b6..8acc32f9d3 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -6,7 +6,6 @@ /// import { ExtensionPoint } from '@backstage/backend-plugin-api'; -import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { Logger } from 'winston'; import { Schema } from 'jsonschema'; @@ -16,7 +15,7 @@ import { Writable } from 'stream'; import { z } from 'zod'; // @public -export type ActionContext = { +export type ActionContext = { logger: Logger; logStream: Writable; secrets?: TaskSecrets; @@ -38,7 +37,7 @@ export const createTemplateAction: < TInputSchema extends z.ZodType | Schema = {}, TOutputSchema extends z.ZodType | Schema = {}, >( - templateAction: TemplateAction, + action: TemplateActionOptions, ) => TemplateAction; // @alpha @@ -57,6 +56,26 @@ export type TaskSecrets = Record & { // @public (undocumented) export type TemplateAction< + TParams = unknown, + TInputSchema extends Schema | unknown = unknown, + TOutputSchema extends Schema | unknown = unknown, +> = { + id: string; + description?: string; + examples?: { + description: string; + example: string; + }[]; + supportsDryRun?: boolean; + schema?: { + input?: TInputSchema; + output?: TOutputSchema; + }; + handler: (ctx: ActionContext) => Promise; +}; + +// @public (undocumented) +export type TemplateActionOptions< TParams = {}, TInputSchema extends Schema | z.ZodType = {}, TOutputSchema extends Schema | z.ZodType = {}, diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json index 791376f7d0..b7fd22a2bd 100644 --- a/plugins/scaffolder-node/package.json +++ b/plugins/scaffolder-node/package.json @@ -30,7 +30,8 @@ "@backstage/types": "workspace:^", "jsonschema": "^1.2.6", "winston": "^3.2.1", - "zod": "~3.18.0" + "zod": "~3.18.0", + "zod-to-json-schema": "~3.18.0" }, "devDependencies": { "@backstage/cli": "workspace:^" diff --git a/plugins/scaffolder-node/src/actions/createTemplateAction.ts b/plugins/scaffolder-node/src/actions/createTemplateAction.ts index 11c01a6086..9b7848c43b 100644 --- a/plugins/scaffolder-node/src/actions/createTemplateAction.ts +++ b/plugins/scaffolder-node/src/actions/createTemplateAction.ts @@ -14,12 +14,37 @@ * limitations under the License. */ -import { TemplateAction } from './types'; +import { ActionContext, TemplateAction } from './types'; import { z } from 'zod'; import { Schema } from 'jsonschema'; +import zodToJsonSchema from 'zod-to-json-schema'; + +/** @public */ +export type TemplateActionOptions< + TParams = {}, + TInputSchema extends Schema | z.ZodType = {}, + TOutputSchema extends Schema | z.ZodType = {}, +> = { + id: string; + description?: string; + examples?: { description: string; example: string }[]; + supportsDryRun?: boolean; + schema?: { + input?: TInputSchema; + output?: TOutputSchema; + }; + handler: ( + ctx: ActionContext< + TInputSchema extends z.ZodType + ? IReturn + : TParams + >, + ) => Promise; +}; + /** * 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 const createTemplateAction = < @@ -27,7 +52,26 @@ export const createTemplateAction = < TInputSchema extends Schema | z.ZodType = {}, TOutputSchema extends Schema | z.ZodType = {}, >( - templateAction: TemplateAction, -): TemplateAction => { - return templateAction; + action: TemplateActionOptions, +): TemplateAction => { + const inputSchema = + action.schema?.input && 'safeParseAsync' in action.schema.input + ? zodToJsonSchema(action.schema.input) + : action.schema?.input; + + const outputSchema = + action.schema?.output && 'safeParseAsync' in action.schema.output + ? zodToJsonSchema(action.schema.output) + : action.schema?.output; + + const templateAction = { + ...action, + schema: { + ...action.schema, + input: inputSchema, + output: outputSchema, + }, + }; + + return templateAction as TemplateAction; }; diff --git a/plugins/scaffolder-node/src/actions/index.ts b/plugins/scaffolder-node/src/actions/index.ts index 7fdee6d692..e4af098356 100644 --- a/plugins/scaffolder-node/src/actions/index.ts +++ b/plugins/scaffolder-node/src/actions/index.ts @@ -14,5 +14,8 @@ * limitations under the License. */ -export { createTemplateAction } from './createTemplateAction'; +export { + createTemplateAction, + type TemplateActionOptions, +} from './createTemplateAction'; export { type ActionContext, type TemplateAction } from './types'; diff --git a/plugins/scaffolder-node/src/actions/types.ts b/plugins/scaffolder-node/src/actions/types.ts index 2adad00bba..a114574f85 100644 --- a/plugins/scaffolder-node/src/actions/types.ts +++ b/plugins/scaffolder-node/src/actions/types.ts @@ -16,17 +16,16 @@ import { Logger } from 'winston'; import { Writable } from 'stream'; -import { JsonValue, JsonObject } from '@backstage/types'; -import { Schema } from 'jsonschema'; +import { JsonValue } from '@backstage/types'; import { TaskSecrets } from '../tasks/types'; import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; import { UserEntity } from '@backstage/catalog-model'; -import { z } from 'zod'; +import { Schema } from 'jsonschema'; /** * ActionContext is passed into scaffolder actions. * @public */ -export type ActionContext = { +export type ActionContext = { logger: Logger; logStream: Writable; secrets?: TaskSecrets; @@ -63,24 +62,14 @@ export type ActionContext = { }; /** @public */ -export type TemplateAction< - TParams = {}, - TInputSchema extends Schema | z.ZodType = {}, - TOutputSchema extends Schema | z.ZodType = {}, -> = { +export type TemplateAction = { id: string; description?: string; examples?: { description: string; example: string }[]; supportsDryRun?: boolean; schema?: { - input?: TInputSchema; - output?: TOutputSchema; + input?: Schema; + output?: Schema; }; - handler: ( - ctx: ActionContext< - TInputSchema extends z.ZodType - ? IReturn - : TParams - >, - ) => Promise; + handler: (ctx: ActionContext) => Promise; }; diff --git a/yarn.lock b/yarn.lock index bf4403c54f..a89f3c14fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7716,7 +7716,6 @@ __metadata: yaml: ^2.0.0 zen-observable: ^0.10.0 zod: ~3.18.0 - zod-to-json-schema: ~3.18.0 languageName: unknown linkType: soft @@ -7742,6 +7741,7 @@ __metadata: jsonschema: ^1.2.6 winston: ^3.2.1 zod: ~3.18.0 + zod-to-json-schema: ~3.18.0 languageName: unknown linkType: soft From d57708a0c72a09f8027fc11bfc4972d8b8511234 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 24 Feb 2023 15:20:09 +0100 Subject: [PATCH 09/12] chore: really fix up the api-reports and make them clean and tidy Signed-off-by: blam --- .../api-report.md | 20 +- .../api-report.md | 16 +- .../api-report.md | 18 +- .../api-report.md | 14 +- plugins/scaffolder-backend/api-report.md | 757 ++++++++---------- .../tasks/NunjucksWorkflowRunner.test.ts | 5 +- .../tasks/NunjucksWorkflowRunner.ts | 2 +- .../scaffolder-backend/src/service/router.ts | 3 +- plugins/scaffolder-node/api-report.md | 33 +- .../src/actions/createTemplateAction.ts | 19 +- plugins/scaffolder-node/src/actions/types.ts | 8 +- 11 files changed, 384 insertions(+), 511 deletions(-) diff --git a/plugins/scaffolder-backend-module-cookiecutter/api-report.md b/plugins/scaffolder-backend-module-cookiecutter/api-report.md index 554732104d..76f2ced66f 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/api-report.md +++ b/plugins/scaffolder-backend-module-cookiecutter/api-report.md @@ -16,16 +16,12 @@ export function createFetchCookiecutterAction(options: { reader: UrlReader; integrations: ScmIntegrations; containerRunner: ContainerRunner; -}): TemplateAction< - { - url: string; - targetPath?: string | undefined; - values: JsonObject; - copyWithoutRender?: string[] | undefined; - extensions?: string[] | undefined; - imageName?: string | undefined; - }, - {}, - {} ->; +}): TemplateAction<{ + url: string; + targetPath?: string | undefined; + values: JsonObject; + copyWithoutRender?: string[] | undefined; + extensions?: string[] | undefined; + imageName?: string | undefined; +}>; ``` diff --git a/plugins/scaffolder-backend-module-rails/api-report.md b/plugins/scaffolder-backend-module-rails/api-report.md index c910d5ae79..c91daa1344 100644 --- a/plugins/scaffolder-backend-module-rails/api-report.md +++ b/plugins/scaffolder-backend-module-rails/api-report.md @@ -15,14 +15,10 @@ export function createFetchRailsAction(options: { integrations: ScmIntegrations; containerRunner: ContainerRunner; allowedImageNames?: string[]; -}): TemplateAction< - { - url: string; - targetPath?: string | undefined; - values: JsonObject; - imageName?: string | undefined; - }, - {}, - {} ->; +}): TemplateAction<{ + url: string; + targetPath?: string | undefined; + values: JsonObject; + imageName?: string | undefined; +}>; ``` diff --git a/plugins/scaffolder-backend-module-sentry/api-report.md b/plugins/scaffolder-backend-module-sentry/api-report.md index c0ae268339..ef09bdebc0 100644 --- a/plugins/scaffolder-backend-module-sentry/api-report.md +++ b/plugins/scaffolder-backend-module-sentry/api-report.md @@ -9,17 +9,13 @@ import { TemplateAction } from '@backstage/plugin-scaffolder-node'; // @public export function createSentryCreateProjectAction(options: { config: Config; -}): TemplateAction< - { - organizationSlug: string; - teamSlug: string; - name: string; - slug?: string | undefined; - authToken?: string | undefined; - }, - {}, - {} ->; +}): TemplateAction<{ + organizationSlug: string; + teamSlug: string; + name: string; + slug?: string | undefined; + authToken?: string | undefined; +}>; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/scaffolder-backend-module-yeoman/api-report.md b/plugins/scaffolder-backend-module-yeoman/api-report.md index 7e70ea8580..562fe52ac2 100644 --- a/plugins/scaffolder-backend-module-yeoman/api-report.md +++ b/plugins/scaffolder-backend-module-yeoman/api-report.md @@ -7,13 +7,9 @@ import { JsonObject } from '@backstage/types'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; // @public -export function createRunYeomanAction(): TemplateAction< - { - namespace: string; - args?: string[] | undefined; - options?: JsonObject | undefined; - }, - {}, - {} ->; +export function createRunYeomanAction(): TemplateAction<{ + namespace: string; + args?: string[] | undefined; + options?: JsonObject | undefined; +}>; ``` diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index a8133b0bd7..b1278e9295 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -35,7 +35,6 @@ import { TemplateAction as TemplateAction_2 } from '@backstage/plugin-scaffolder import { TemplateActionOptions } from '@backstage/plugin-scaffolder-node'; import { UrlReader } from '@backstage/backend-common'; import { Writable } from 'stream'; -import { z } from 'zod'; import { ZodType } from 'zod'; import { ZodTypeDef } from 'zod'; @@ -71,68 +70,37 @@ export function createCatalogRegisterAction(options: { repoContentsUrl: string; catalogInfoPath?: string | undefined; optional?: boolean | undefined; - }, - {}, - {} ->; - - -// @public -export function createCatalogWriteAction(): TemplateAction_2< - unknown, - z.ZodObject< - { - filePath: z.ZodOptional; - entity: z.ZodOptional>; - }, - 'strip', - z.ZodTypeAny, - { - filePath?: string | undefined; - entity?: {} | undefined; - }, - { - filePath?: string | undefined; - entity?: {} | undefined; } - >, - {} >; // @public -export function createDebugLogAction(): TemplateAction_2< - { - message?: string | undefined; - listWorkspace?: boolean | undefined; - }, - {}, - {} ->; +export function createCatalogWriteAction(): TemplateAction_2<{ + filePath?: string | undefined; + entity?: {} | undefined; +}>; + +// @public +export function createDebugLogAction(): TemplateAction_2<{ + message?: string | undefined; + listWorkspace?: boolean | undefined; +}>; // @public export function createFetchCatalogEntityAction(options: { catalogClient: CatalogApi; -}): TemplateAction_2< - { - entityRef: string; - optional?: boolean | undefined; - }, - {}, - {} ->; +}): TemplateAction_2<{ + entityRef: string; + optional?: boolean | undefined; +}>; // @public export function createFetchPlainAction(options: { reader: UrlReader; integrations: ScmIntegrations; -}): TemplateAction_2< - { - url: string; - targetPath?: string | undefined; - }, - {}, - {} ->; +}): TemplateAction_2<{ + url: string; + targetPath?: string | undefined; +}>; // @public export function createFetchTemplateAction(options: { @@ -140,77 +108,57 @@ export function createFetchTemplateAction(options: { integrations: ScmIntegrations; additionalTemplateFilters?: Record; additionalTemplateGlobals?: Record; -}): TemplateAction_2< - { - url: string; - targetPath?: string | undefined; - values: any; - templateFileExtension?: string | boolean | undefined; - copyWithoutRender?: string[] | undefined; - copyWithoutTemplating?: string[] | undefined; - cookiecutterCompat?: boolean | undefined; - replace?: boolean | undefined; - }, - {}, - {} ->; +}): TemplateAction_2<{ + url: string; + targetPath?: string | undefined; + values: any; + templateFileExtension?: string | boolean | undefined; + copyWithoutRender?: string[] | undefined; + copyWithoutTemplating?: string[] | undefined; + cookiecutterCompat?: boolean | undefined; + replace?: boolean | undefined; +}>; // @public -export const createFilesystemDeleteAction: () => TemplateAction_2< - { - files: string[]; - }, - {}, - {} ->; +export const createFilesystemDeleteAction: () => TemplateAction_2<{ + files: string[]; +}>; // @public -export const createFilesystemRenameAction: () => TemplateAction_2< - { - files: Array<{ - from: string; - to: string; - overwrite?: boolean; - }>; - }, - {}, - {} ->; +export const createFilesystemRenameAction: () => TemplateAction_2<{ + files: Array<{ + from: string; + to: string; + overwrite?: boolean; + }>; +}>; // @public export function createGithubActionsDispatchAction(options: { integrations: ScmIntegrations; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction_2< - { - repoUrl: string; - workflowId: string; - branchOrTagName: string; - workflowInputs?: - | { - [key: string]: string; - } - | undefined; - token?: string | undefined; - }, - {}, - {} ->; +}): TemplateAction_2<{ + repoUrl: string; + workflowId: string; + branchOrTagName: string; + workflowInputs?: + | { + [key: string]: string; + } + | undefined; + token?: string | undefined; +}>; // @public export function createGithubIssuesLabelAction(options: { integrations: ScmIntegrationRegistry; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction_2< - { - repoUrl: string; - number: number; - labels: string[]; - token?: string | undefined; - }, - {}, - {} ->; +}): TemplateAction_2<{ + repoUrl: string; + number: number; + labels: string[]; + token?: string | undefined; +}>; // @public export interface CreateGithubPullRequestActionOptions { @@ -235,388 +183,336 @@ export type CreateGithubPullRequestClientFactoryInput = { export function createGithubRepoCreateAction(options: { integrations: ScmIntegrationRegistry; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction_2< - { - repoUrl: string; - description?: string | undefined; - homepage?: string | undefined; - access?: string | undefined; - deleteBranchOnMerge?: boolean | undefined; - gitAuthorName?: string | undefined; - gitAuthorEmail?: string | undefined; - allowRebaseMerge?: boolean | undefined; - allowSquashMerge?: boolean | undefined; - squashMergeCommitTitle?: 'PR_TITLE' | 'COMMIT_OR_PR_TITLE' | undefined; - squashMergeCommitMessage?: - | 'PR_BODY' - | 'COMMIT_MESSAGES' - | 'BLANK' - | undefined; - allowMergeCommit?: boolean | undefined; - allowAutoMerge?: boolean | undefined; - requireCodeOwnerReviews?: boolean | undefined; - bypassPullRequestAllowances?: - | { - users?: string[] | undefined; - teams?: string[] | undefined; - apps?: string[] | undefined; - } - | undefined; - requiredApprovingReviewCount?: number | undefined; - restrictions?: - | { - users: string[]; - teams: string[]; - apps?: string[] | undefined; - } - | undefined; - requiredStatusCheckContexts?: string[] | undefined; - requireBranchesToBeUpToDate?: boolean | undefined; - requiredConversationResolution?: boolean | undefined; - repoVisibility?: 'internal' | 'private' | 'public' | undefined; - collaborators?: - | ( - | { - user: string; - access: string; - } - | { - team: string; - access: string; - } - | { - username: string; - access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; - } - )[] - | undefined; - hasProjects?: boolean | undefined; - hasWiki?: boolean | undefined; - hasIssues?: boolean | undefined; - token?: string | undefined; - topics?: string[] | undefined; - requireCommitSigning?: boolean | undefined; - }, - {}, - {} ->; +}): TemplateAction_2<{ + repoUrl: string; + description?: string | undefined; + homepage?: string | undefined; + access?: string | undefined; + deleteBranchOnMerge?: boolean | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; + allowRebaseMerge?: boolean | undefined; + allowSquashMerge?: boolean | undefined; + squashMergeCommitTitle?: 'PR_TITLE' | 'COMMIT_OR_PR_TITLE' | undefined; + squashMergeCommitMessage?: + | 'PR_BODY' + | 'COMMIT_MESSAGES' + | 'BLANK' + | undefined; + allowMergeCommit?: boolean | undefined; + allowAutoMerge?: boolean | undefined; + requireCodeOwnerReviews?: boolean | undefined; + bypassPullRequestAllowances?: + | { + users?: string[] | undefined; + teams?: string[] | undefined; + apps?: string[] | undefined; + } + | undefined; + requiredApprovingReviewCount?: number | undefined; + restrictions?: + | { + users: string[]; + teams: string[]; + apps?: string[] | undefined; + } + | undefined; + requiredStatusCheckContexts?: string[] | undefined; + requireBranchesToBeUpToDate?: boolean | undefined; + requiredConversationResolution?: boolean | undefined; + repoVisibility?: 'internal' | 'private' | 'public' | undefined; + collaborators?: + | ( + | { + user: string; + access: string; + } + | { + team: string; + access: string; + } + | { + username: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + } + )[] + | undefined; + hasProjects?: boolean | undefined; + hasWiki?: boolean | undefined; + hasIssues?: boolean | undefined; + token?: string | undefined; + topics?: string[] | undefined; + requireCommitSigning?: boolean | undefined; +}>; // @public export function createGithubRepoPushAction(options: { integrations: ScmIntegrationRegistry; config: Config; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction_2< - { - repoUrl: string; - description?: string | undefined; - defaultBranch?: string | undefined; - protectDefaultBranch?: boolean | undefined; - protectEnforceAdmins?: boolean | undefined; - gitCommitMessage?: string | undefined; - gitAuthorName?: string | undefined; - gitAuthorEmail?: string | undefined; - requireCodeOwnerReviews?: boolean | undefined; - dismissStaleReviews?: boolean | undefined; - bypassPullRequestAllowances?: - | { - users?: string[]; - teams?: string[]; - apps?: string[]; - } - | undefined; - requiredApprovingReviewCount?: number | undefined; - restrictions?: - | { - users: string[]; - teams: string[]; - apps?: string[]; - } - | undefined; - requiredStatusCheckContexts?: string[] | undefined; - requireBranchesToBeUpToDate?: boolean | undefined; - requiredConversationResolution?: boolean | undefined; - sourcePath?: string | undefined; - token?: string | undefined; - requiredCommitSigning?: boolean | undefined; - }, - {}, - {} ->; +}): TemplateAction_2<{ + repoUrl: string; + description?: string | undefined; + defaultBranch?: string | undefined; + protectDefaultBranch?: boolean | undefined; + protectEnforceAdmins?: boolean | undefined; + gitCommitMessage?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; + requireCodeOwnerReviews?: boolean | undefined; + dismissStaleReviews?: boolean | undefined; + bypassPullRequestAllowances?: + | { + users?: string[]; + teams?: string[]; + apps?: string[]; + } + | undefined; + requiredApprovingReviewCount?: number | undefined; + restrictions?: + | { + users: string[]; + teams: string[]; + apps?: string[]; + } + | undefined; + requiredStatusCheckContexts?: string[] | undefined; + requireBranchesToBeUpToDate?: boolean | undefined; + requiredConversationResolution?: boolean | undefined; + sourcePath?: string | undefined; + token?: string | undefined; + requiredCommitSigning?: boolean | undefined; +}>; // @public export function createGithubWebhookAction(options: { integrations: ScmIntegrationRegistry; defaultWebhookSecret?: string; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction_2< - { - repoUrl: string; - webhookUrl: string; - webhookSecret?: string | undefined; - events?: string[] | undefined; - active?: boolean | undefined; - contentType?: 'form' | 'json' | undefined; - insecureSsl?: boolean | undefined; - token?: string | undefined; - }, - {}, - {} ->; +}): TemplateAction_2<{ + repoUrl: string; + webhookUrl: string; + webhookSecret?: string | undefined; + events?: string[] | undefined; + active?: boolean | undefined; + contentType?: 'form' | 'json' | undefined; + insecureSsl?: boolean | undefined; + token?: string | undefined; +}>; // @public export function createPublishAzureAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction_2< - { - repoUrl: string; - description?: string | undefined; - defaultBranch?: string | undefined; - sourcePath?: string | undefined; - token?: string | undefined; - gitCommitMessage?: string | undefined; - gitAuthorName?: string | undefined; - gitAuthorEmail?: string | undefined; - }, - {}, - {} ->; +}): TemplateAction_2<{ + repoUrl: string; + description?: string | undefined; + defaultBranch?: string | undefined; + sourcePath?: string | undefined; + token?: string | undefined; + gitCommitMessage?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; +}>; // @public @deprecated export function createPublishBitbucketAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction_2< - { - repoUrl: string; - description?: string | undefined; - defaultBranch?: string | undefined; - repoVisibility?: 'private' | 'public' | undefined; - sourcePath?: string | undefined; - enableLFS?: boolean | undefined; - token?: string | undefined; - gitCommitMessage?: string | undefined; - gitAuthorName?: string | undefined; - gitAuthorEmail?: string | undefined; - }, - {}, - {} ->; +}): TemplateAction_2<{ + repoUrl: string; + description?: string | undefined; + defaultBranch?: string | undefined; + repoVisibility?: 'private' | 'public' | undefined; + sourcePath?: string | undefined; + enableLFS?: boolean | undefined; + token?: string | undefined; + gitCommitMessage?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; +}>; // @public export function createPublishBitbucketCloudAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction_2< - { - repoUrl: string; - description?: string | undefined; - defaultBranch?: string | undefined; - repoVisibility?: 'private' | 'public' | undefined; - sourcePath?: string | undefined; - token?: string | undefined; - }, - {}, - {} ->; +}): TemplateAction_2<{ + repoUrl: string; + description?: string | undefined; + defaultBranch?: string | undefined; + repoVisibility?: 'private' | 'public' | undefined; + sourcePath?: string | undefined; + token?: string | undefined; +}>; // @public export function createPublishBitbucketServerAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction_2< - { - repoUrl: string; - description?: string | undefined; - defaultBranch?: string | undefined; - repoVisibility?: 'private' | 'public' | undefined; - sourcePath?: string | undefined; - enableLFS?: boolean | undefined; - token?: string | undefined; - gitCommitMessage?: string | undefined; - gitAuthorName?: string | undefined; - gitAuthorEmail?: string | undefined; - }, - {}, - {} ->; +}): TemplateAction_2<{ + repoUrl: string; + description?: string | undefined; + defaultBranch?: string | undefined; + repoVisibility?: 'private' | 'public' | undefined; + sourcePath?: string | undefined; + enableLFS?: boolean | undefined; + token?: string | undefined; + gitCommitMessage?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; +}>; // @public export function createPublishGerritAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction_2< - { - repoUrl: string; - description: string; - defaultBranch?: string | undefined; - gitCommitMessage?: string | undefined; - gitAuthorName?: string | undefined; - gitAuthorEmail?: string | undefined; - sourcePath?: string | undefined; - }, - {}, - {} ->; +}): TemplateAction_2<{ + repoUrl: string; + description: string; + defaultBranch?: string | undefined; + gitCommitMessage?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; + sourcePath?: string | undefined; +}>; // @public export function createPublishGerritReviewAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction_2< - { - repoUrl: string; - branch?: string | undefined; - sourcePath?: string | undefined; - gitCommitMessage?: string | undefined; - gitAuthorName?: string | undefined; - gitAuthorEmail?: string | undefined; - }, - {}, - {} ->; +}): TemplateAction_2<{ + repoUrl: string; + branch?: string | undefined; + sourcePath?: string | undefined; + gitCommitMessage?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; +}>; // @public export function createPublishGithubAction(options: { integrations: ScmIntegrationRegistry; config: Config; githubCredentialsProvider?: GithubCredentialsProvider; -}): TemplateAction_2< - { - repoUrl: string; - description?: string | undefined; - homepage?: string | undefined; - access?: string | undefined; - defaultBranch?: string | undefined; - protectDefaultBranch?: boolean | undefined; - protectEnforceAdmins?: boolean | undefined; - deleteBranchOnMerge?: boolean | undefined; - gitCommitMessage?: string | undefined; - gitAuthorName?: string | undefined; - gitAuthorEmail?: string | undefined; - allowRebaseMerge?: boolean | undefined; - allowSquashMerge?: boolean | undefined; - squashMergeCommitTitle?: 'PR_TITLE' | 'COMMIT_OR_PR_TITLE' | undefined; - squashMergeCommitMessage?: - | 'PR_BODY' - | 'COMMIT_MESSAGES' - | 'BLANK' - | undefined; - allowMergeCommit?: boolean | undefined; - allowAutoMerge?: boolean | undefined; - sourcePath?: string | undefined; - bypassPullRequestAllowances?: - | { - users?: string[]; - teams?: string[]; - apps?: string[]; - } - | undefined; - requiredApprovingReviewCount?: number | undefined; - restrictions?: - | { - users: string[]; - teams: string[]; - apps?: string[]; - } - | undefined; - requireCodeOwnerReviews?: boolean | undefined; - dismissStaleReviews?: boolean | undefined; - requiredStatusCheckContexts?: string[] | undefined; - requireBranchesToBeUpToDate?: boolean | undefined; - requiredConversationResolution?: boolean | undefined; - repoVisibility?: 'internal' | 'private' | 'public' | undefined; - collaborators?: - | ( - | { - user: string; - access: string; - } - | { - team: string; - access: string; - } - | { - username: string; - access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; - } - )[] - | undefined; - hasProjects?: boolean | undefined; - hasWiki?: boolean | undefined; - hasIssues?: boolean | undefined; - token?: string | undefined; - topics?: string[] | undefined; - requiredCommitSigning?: boolean | undefined; - }, - {}, - {} ->; +}): TemplateAction_2<{ + repoUrl: string; + description?: string | undefined; + homepage?: string | undefined; + access?: string | undefined; + defaultBranch?: string | undefined; + protectDefaultBranch?: boolean | undefined; + protectEnforceAdmins?: boolean | undefined; + deleteBranchOnMerge?: boolean | undefined; + gitCommitMessage?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; + allowRebaseMerge?: boolean | undefined; + allowSquashMerge?: boolean | undefined; + squashMergeCommitTitle?: 'PR_TITLE' | 'COMMIT_OR_PR_TITLE' | undefined; + squashMergeCommitMessage?: + | 'PR_BODY' + | 'COMMIT_MESSAGES' + | 'BLANK' + | undefined; + allowMergeCommit?: boolean | undefined; + allowAutoMerge?: boolean | undefined; + sourcePath?: string | undefined; + bypassPullRequestAllowances?: + | { + users?: string[]; + teams?: string[]; + apps?: string[]; + } + | undefined; + requiredApprovingReviewCount?: number | undefined; + restrictions?: + | { + users: string[]; + teams: string[]; + apps?: string[]; + } + | undefined; + requireCodeOwnerReviews?: boolean | undefined; + dismissStaleReviews?: boolean | undefined; + requiredStatusCheckContexts?: string[] | undefined; + requireBranchesToBeUpToDate?: boolean | undefined; + requiredConversationResolution?: boolean | undefined; + repoVisibility?: 'internal' | 'private' | 'public' | undefined; + collaborators?: + | ( + | { + user: string; + access: string; + } + | { + team: string; + access: string; + } + | { + username: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + } + )[] + | undefined; + hasProjects?: boolean | undefined; + hasWiki?: boolean | undefined; + hasIssues?: boolean | undefined; + token?: string | undefined; + topics?: string[] | undefined; + requiredCommitSigning?: boolean | undefined; +}>; // @public export const createPublishGithubPullRequestAction: ({ integrations, githubCredentialsProvider, clientFactory, -}: CreateGithubPullRequestActionOptions) => TemplateAction_2< - { - title: string; - branchName: string; - description: string; - repoUrl: string; - draft?: boolean | undefined; - targetPath?: string | undefined; - sourcePath?: string | undefined; - token?: string | undefined; - reviewers?: string[] | undefined; - teamReviewers?: string[] | undefined; - }, - {}, - {} ->; +}: CreateGithubPullRequestActionOptions) => TemplateAction_2<{ + title: string; + branchName: string; + description: string; + repoUrl: string; + draft?: boolean | undefined; + targetPath?: string | undefined; + sourcePath?: string | undefined; + token?: string | undefined; + reviewers?: string[] | undefined; + teamReviewers?: string[] | undefined; +}>; // @public export function createPublishGitlabAction(options: { integrations: ScmIntegrationRegistry; config: Config; -}): TemplateAction_2< - { - repoUrl: string; - defaultBranch?: string | undefined; - repoVisibility?: 'internal' | 'private' | 'public' | undefined; - sourcePath?: string | undefined; - token?: string | undefined; - gitCommitMessage?: string | undefined; - gitAuthorName?: string | undefined; - gitAuthorEmail?: string | undefined; - setUserAsOwner?: boolean | undefined; - topics?: string[] | undefined; - }, - {}, - {} ->; +}): TemplateAction_2<{ + repoUrl: string; + defaultBranch?: string | undefined; + repoVisibility?: 'internal' | 'private' | 'public' | undefined; + sourcePath?: string | undefined; + token?: string | undefined; + gitCommitMessage?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; + setUserAsOwner?: boolean | undefined; + topics?: string[] | undefined; +}>; // @public export const createPublishGitlabMergeRequestAction: (options: { integrations: ScmIntegrationRegistry; -}) => TemplateAction_2< - { - repoUrl: string; - title: string; - description: string; - branchName: string; - sourcePath?: string | undefined; - targetPath?: string | undefined; - token?: string | undefined; - commitAction?: 'update' | 'delete' | 'create' | undefined; - projectid?: string | undefined; - removeSourceBranch?: boolean | undefined; - assignee?: string | undefined; - }, - {}, - {} ->; +}) => TemplateAction_2<{ + repoUrl: string; + title: string; + description: string; + branchName: string; + sourcePath?: string | undefined; + targetPath?: string | undefined; + token?: string | undefined; + commitAction?: 'update' | 'delete' | 'create' | undefined; + projectid?: string | undefined; + removeSourceBranch?: boolean | undefined; + assignee?: string | undefined; +}>; // @public export function createRouter(options: RouterOptions): Promise; @@ -626,9 +522,12 @@ export const createTemplateAction: < TParams, TInputSchema extends ZodType | Schema = {}, TOutputSchema extends ZodType | Schema = {}, + TActionInput = TInputSchema extends ZodType + ? IReturn + : TParams, >( - action: TemplateActionOptions, -) => TemplateAction_2; + action: TemplateActionOptions, +) => TemplateAction_2; // @public export type CreateWorkerOptions = { @@ -726,7 +625,7 @@ export type OctokitWithPullRequestPluginClient = Octokit & { // @public export interface RouterOptions { // (undocumented) - actions?: TemplateAction_2[]; + actions?: TemplateAction_2[]; // (undocumented) additionalTemplateFilters?: Record; // (undocumented) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index 135ed4473e..0d2246ed36 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -27,6 +27,7 @@ import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { createTemplateAction, TaskSecrets, + TemplateAction, } from '@backstage/plugin-scaffolder-node'; import { UserEntity } from '@backstage/catalog-model'; import { z } from 'zod'; @@ -111,14 +112,14 @@ describe('DefaultWorkflowRunner', () => { createTemplateAction({ id: 'jest-zod-validated-action', description: 'Mock action for testing', - supportsDryRun: true, handler: fakeActionHandler, + supportsDryRun: true, schema: { input: z.object({ foo: z.number(), }), }, - }), + }) as TemplateAction, ); actionRegistry.register({ diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 31d3479026..88abe05cad 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -24,7 +24,7 @@ import { JsonObject, JsonValue } from '@backstage/types'; import { InputError } from '@backstage/errors'; import { PassThrough } from 'stream'; import { generateExampleOutput, isTruthy } from './helper'; -import { Schema, validate as validateJsonSchema } from 'jsonschema'; +import { validate as validateJsonSchema } from 'jsonschema'; import { parseRepoUrl } from '../actions/builtin/publish/util'; import { TemplateActionRegistry } from '../actions'; import { diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index b219c81219..e40958c778 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -66,8 +66,7 @@ export interface RouterOptions { database: PluginDatabaseManager; catalogClient: CatalogApi; scheduler?: PluginTaskScheduler; - - actions?: TemplateAction[]; + actions?: TemplateAction[]; /** * @deprecated taskWorkers is deprecated in favor of concurrentTasksLimit option with a single TaskWorker * @defaultValue 1 diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md index 8acc32f9d3..a6943f29f6 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -15,12 +15,12 @@ import { Writable } from 'stream'; import { z } from 'zod'; // @public -export type ActionContext = { +export type ActionContext = { logger: Logger; logStream: Writable; secrets?: TaskSecrets; workspacePath: string; - input: TInput; + input: TActionInput; output(name: string, value: JsonValue): void; createTemporaryDirectory(): Promise; templateInfo?: TemplateInfo; @@ -36,9 +36,12 @@ export const createTemplateAction: < TParams, TInputSchema extends z.ZodType | Schema = {}, TOutputSchema extends z.ZodType | Schema = {}, + TActionInput = TInputSchema extends z.ZodType + ? IReturn + : TParams, >( - action: TemplateActionOptions, -) => TemplateAction; + action: TemplateActionOptions, +) => TemplateAction; // @alpha export interface ScaffolderActionsExtensionPoint { @@ -55,11 +58,7 @@ export type TaskSecrets = Record & { }; // @public (undocumented) -export type TemplateAction< - TParams = unknown, - TInputSchema extends Schema | unknown = unknown, - TOutputSchema extends Schema | unknown = unknown, -> = { +export type TemplateAction = { id: string; description?: string; examples?: { @@ -68,15 +67,15 @@ export type TemplateAction< }[]; supportsDryRun?: boolean; schema?: { - input?: TInputSchema; - output?: TOutputSchema; + input?: Schema; + output?: Schema; }; - handler: (ctx: ActionContext) => Promise; + handler: (ctx: ActionContext) => Promise; }; // @public (undocumented) export type TemplateActionOptions< - TParams = {}, + TActionInput = {}, TInputSchema extends Schema | z.ZodType = {}, TOutputSchema extends Schema | z.ZodType = {}, > = { @@ -91,12 +90,6 @@ export type TemplateActionOptions< input?: TInputSchema; output?: TOutputSchema; }; - handler: ( - ctx: ActionContext< - TInputSchema extends z.ZodType - ? IReturn - : TParams - >, - ) => Promise; + handler: (ctx: ActionContext) => Promise; }; ``` diff --git a/plugins/scaffolder-node/src/actions/createTemplateAction.ts b/plugins/scaffolder-node/src/actions/createTemplateAction.ts index 9b7848c43b..8c98042d8c 100644 --- a/plugins/scaffolder-node/src/actions/createTemplateAction.ts +++ b/plugins/scaffolder-node/src/actions/createTemplateAction.ts @@ -21,7 +21,7 @@ import zodToJsonSchema from 'zod-to-json-schema'; /** @public */ export type TemplateActionOptions< - TParams = {}, + TActionInput = {}, TInputSchema extends Schema | z.ZodType = {}, TOutputSchema extends Schema | z.ZodType = {}, > = { @@ -33,13 +33,7 @@ export type TemplateActionOptions< input?: TInputSchema; output?: TOutputSchema; }; - handler: ( - ctx: ActionContext< - TInputSchema extends z.ZodType - ? IReturn - : TParams - >, - ) => Promise; + handler: (ctx: ActionContext) => Promise; }; /** @@ -51,9 +45,12 @@ export const createTemplateAction = < TParams, TInputSchema extends Schema | z.ZodType = {}, TOutputSchema extends Schema | z.ZodType = {}, + TActionInput = TInputSchema extends z.ZodType + ? IReturn + : TParams, >( - action: TemplateActionOptions, -): TemplateAction => { + action: TemplateActionOptions, +): TemplateAction => { const inputSchema = action.schema?.input && 'safeParseAsync' in action.schema.input ? zodToJsonSchema(action.schema.input) @@ -73,5 +70,5 @@ export const createTemplateAction = < }, }; - return templateAction as TemplateAction; + return templateAction; }; diff --git a/plugins/scaffolder-node/src/actions/types.ts b/plugins/scaffolder-node/src/actions/types.ts index a114574f85..2bde4bcd39 100644 --- a/plugins/scaffolder-node/src/actions/types.ts +++ b/plugins/scaffolder-node/src/actions/types.ts @@ -25,12 +25,12 @@ import { Schema } from 'jsonschema'; * ActionContext is passed into scaffolder actions. * @public */ -export type ActionContext = { +export type ActionContext = { logger: Logger; logStream: Writable; secrets?: TaskSecrets; workspacePath: string; - input: TInput; + input: TActionInput; output(name: string, value: JsonValue): void; /** @@ -62,7 +62,7 @@ export type ActionContext = { }; /** @public */ -export type TemplateAction = { +export type TemplateAction = { id: string; description?: string; examples?: { description: string; example: string }[]; @@ -71,5 +71,5 @@ export type TemplateAction = { input?: Schema; output?: Schema; }; - handler: (ctx: ActionContext) => Promise; + handler: (ctx: ActionContext) => Promise; }; From a5b890b57da1ddd513ec40b51d5a4084cc733251 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 24 Feb 2023 15:28:16 +0100 Subject: [PATCH 10/12] chore: fix Signed-off-by: blam Signed-off-by: blam --- plugins/scaffolder-backend/api-report.md | 2 +- .../src/scaffolder/actions/builtin/catalog/write.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index b1278e9295..7b4ac83d58 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -76,7 +76,7 @@ export function createCatalogRegisterAction(options: { // @public export function createCatalogWriteAction(): TemplateAction_2<{ filePath?: string | undefined; - entity?: {} | undefined; + entity: {}; }>; // @public 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 3094f4e692..aaaf504833 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts @@ -59,7 +59,7 @@ const examples = [ export function createCatalogWriteAction() { return createTemplateAction({ - id: 'catalog:write', + id, description: 'Writes the catalog-info.yaml for your template', schema: { input: z.object({ @@ -67,9 +67,9 @@ export function createCatalogWriteAction() { .string() .optional() .describe('Defaults to catalog-info.yaml'), + // TODO: this should reference an zod entity validator if it existed. entity: z .object({}) - .optional() .describe( 'You can provide the same values used in the Entity schema.', ), From 0afbe44ab90dc9d16e24c529ec97c065ad752929 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 28 Feb 2023 10:52:03 +0100 Subject: [PATCH 11/12] chore: fixing the action context Signed-off-by: blam --- plugins/scaffolder-node/api-report.md | 3 ++- plugins/scaffolder-node/src/actions/types.ts | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md index a6943f29f6..89b7171ef9 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -6,6 +6,7 @@ /// import { ExtensionPoint } from '@backstage/backend-plugin-api'; +import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { Logger } from 'winston'; import { Schema } from 'jsonschema'; @@ -15,7 +16,7 @@ import { Writable } from 'stream'; import { z } from 'zod'; // @public -export type ActionContext = { +export type ActionContext = { logger: Logger; logStream: Writable; secrets?: TaskSecrets; diff --git a/plugins/scaffolder-node/src/actions/types.ts b/plugins/scaffolder-node/src/actions/types.ts index 2bde4bcd39..cce149adbe 100644 --- a/plugins/scaffolder-node/src/actions/types.ts +++ b/plugins/scaffolder-node/src/actions/types.ts @@ -16,16 +16,17 @@ import { Logger } from 'winston'; import { Writable } from 'stream'; -import { JsonValue } from '@backstage/types'; +import { JsonObject, JsonValue } from '@backstage/types'; import { TaskSecrets } from '../tasks/types'; import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; import { UserEntity } from '@backstage/catalog-model'; import { Schema } from 'jsonschema'; + /** * ActionContext is passed into scaffolder actions. * @public */ -export type ActionContext = { +export type ActionContext = { logger: Logger; logStream: Writable; secrets?: TaskSecrets; From e7458ceca1a82703cbb41c432c7d0006498187c6 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Mon, 27 Feb 2023 11:55:54 +0100 Subject: [PATCH 12/12] Update docs/features/software-templates/writing-custom-actions.md Co-authored-by: Adam Harvey Signed-off-by: Ben Lambert --- docs/features/software-templates/writing-custom-actions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index 65e2111f15..3e89e89582 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -63,7 +63,7 @@ The `createTemplateAction` takes an object which specifies the following: in some way so that they won't collide with future built-in actions that we may ship with the `scaffolder-backend` plugin. - `schema.input` - A `zod` or JSON schema object for input values to your function -- `schema.output` - A `zod` or JSON schema object for values which are outputted from the +- `schema.output` - A `zod` or JSON schema object for values which are output from the function using `ctx.output` - `handler` - the actual code which is run part of the action, with a context