@@ -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.
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -25,6 +25,7 @@ export function createFetchCookiecutterAction(options: {
|
||||
extensions?: string[] | undefined;
|
||||
imageName?: string | undefined;
|
||||
},
|
||||
{},
|
||||
{}
|
||||
>;
|
||||
```
|
||||
|
||||
@@ -22,6 +22,7 @@ export function createFetchRailsAction(options: {
|
||||
values: JsonObject;
|
||||
imageName?: string | undefined;
|
||||
},
|
||||
{},
|
||||
{}
|
||||
>;
|
||||
```
|
||||
|
||||
@@ -17,6 +17,7 @@ export function createSentryCreateProjectAction(options: {
|
||||
slug?: string | undefined;
|
||||
authToken?: string | undefined;
|
||||
},
|
||||
{},
|
||||
{}
|
||||
>;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ export function createRunYeomanAction(): TemplateAction<
|
||||
args?: string[] | undefined;
|
||||
options?: JsonObject | undefined;
|
||||
},
|
||||
{},
|
||||
{}
|
||||
>;
|
||||
```
|
||||
|
||||
@@ -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<express.Router>;
|
||||
export const createTemplateAction: <
|
||||
TParams,
|
||||
TInputSchema extends ZodType<any, ZodTypeDef, any> | Schema = {},
|
||||
TOutputSchema extends ZodType<any, ZodTypeDef, any> | Schema = {},
|
||||
>(
|
||||
templateAction: TemplateAction_2<TParams, TInputSchema>,
|
||||
) => TemplateAction_2<TParams, TInputSchema>;
|
||||
templateAction: TemplateAction_2<TParams, TInputSchema, TOutputSchema>,
|
||||
) => TemplateAction_2<TParams, TInputSchema, TOutputSchema>;
|
||||
|
||||
// @public
|
||||
export type CreateWorkerOptions = {
|
||||
@@ -950,11 +974,11 @@ export type TemplateAction<TInput extends JsonObject> =
|
||||
// @public
|
||||
export class TemplateActionRegistry {
|
||||
// (undocumented)
|
||||
get(actionId: string): TemplateAction_2<JsonObject>;
|
||||
get(actionId: string): TemplateAction_2;
|
||||
// (undocumented)
|
||||
list(): TemplateAction_2<JsonObject>[];
|
||||
list(): TemplateAction_2[];
|
||||
// (undocumented)
|
||||
register<TInput extends JsonObject>(action: TemplateAction_2<TInput>): void;
|
||||
register(action: TemplateAction_2): void;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -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<string, TemplateAction<any>>();
|
||||
private readonly actions = new Map<string, TemplateAction>();
|
||||
|
||||
register<TInput extends JsonObject>(action: TemplateAction<TInput>) {
|
||||
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<JsonObject> {
|
||||
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<JsonObject>[] {
|
||||
list(): TemplateAction[] {
|
||||
return [...this.actions.values()];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -36,9 +36,10 @@ export type ActionContext<TInput extends JsonObject> = {
|
||||
export const createTemplateAction: <
|
||||
TParams,
|
||||
TInputSchema extends z.ZodType<any, z.ZodTypeDef, any> | Schema = {},
|
||||
TOutputSchema extends z.ZodType<any, z.ZodTypeDef, any> | Schema = {},
|
||||
>(
|
||||
templateAction: TemplateAction<TParams, TInputSchema>,
|
||||
) => TemplateAction<TParams, TInputSchema>;
|
||||
templateAction: TemplateAction<TParams, TInputSchema, TOutputSchema>,
|
||||
) => TemplateAction<TParams, TInputSchema, TOutputSchema>;
|
||||
|
||||
// @alpha
|
||||
export interface ScaffolderActionsExtensionPoint {
|
||||
@@ -56,8 +57,9 @@ export type TaskSecrets = Record<string, string> & {
|
||||
|
||||
// @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<
|
||||
|
||||
@@ -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<TParams, TInputSchema>,
|
||||
): TemplateAction<TParams, TInputSchema> => {
|
||||
// TODO(blam): Can add some more validation here to validate the action later on
|
||||
templateAction: TemplateAction<TParams, TInputSchema, TOutputSchema>,
|
||||
): TemplateAction<TParams, TInputSchema, TOutputSchema> => {
|
||||
return templateAction;
|
||||
};
|
||||
|
||||
@@ -64,8 +64,9 @@ export type ActionContext<TInput extends JsonObject> = {
|
||||
|
||||
/** @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<
|
||||
|
||||
Reference in New Issue
Block a user