From 10a64ff693edb3e9ef6e85bd0ab8c3c6345d033e Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 14 Feb 2023 14:31:17 +0100 Subject: [PATCH] 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