breaking: remove old action format design

Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
benjdlambert
2025-06-04 10:59:56 +02:00
parent 7c7398ba2d
commit ddb0939b4a
5 changed files with 284 additions and 438 deletions
@@ -14,85 +14,8 @@
* limitations under the License.
*/
import { createTemplateAction } from './createTemplateAction';
import { z } from 'zod';
describe('createTemplateAction', () => {
it('should allow creating with jsonschema and use the old deprecated types', () => {
const action = createTemplateAction<{ repoUrl: string }, { test: string }>({
id: 'test',
schema: {
input: {
type: 'object',
required: ['repoUrl'],
properties: {
repoUrl: { type: 'string' },
},
},
output: {
type: 'object',
required: ['test'],
properties: {
test: { type: 'string' },
},
},
},
handler: async ctx => {
// @ts-expect-error - repoUrl is string
const a: number = ctx.input.repoUrl;
const b: string = ctx.input.repoUrl;
expect(b).toBeDefined();
const stream = ctx.logStream;
expect(stream).toBeDefined();
ctx.output('test', 'value');
// @ts-expect-error - not valid output type
ctx.output('test', 4);
// @ts-expect-error - not valid output name
ctx.output('test2', 'value');
},
});
expect(action).toBeDefined();
});
it('should allow creating with zod and use the old deprecated types', () => {
const action = createTemplateAction({
id: 'test',
schema: {
input: z.object({
repoUrl: z.string(),
}),
output: z.object({
test: z.string(),
}),
},
handler: async ctx => {
// @ts-expect-error - repoUrl is string
const a: number = ctx.input.repoUrl;
const b: string = ctx.input.repoUrl;
expect(b).toBeDefined();
const stream = ctx.logStream;
expect(stream).toBeDefined();
ctx.output('test', 'value');
// @ts-expect-error - not valid output type
ctx.output('test', 4);
// @ts-expect-error - not valid output name
ctx.output('test2', 'value');
},
});
expect(action).toBeDefined();
});
it('should allow creating with new first class zod support', () => {
const action = createTemplateAction({
id: 'test',
@@ -128,4 +51,54 @@ describe('createTemplateAction', () => {
expect(action).toBeDefined();
});
it('should allow creating with a function for input and output schema for more complex types', () => {
const action = createTemplateAction({
id: 'test',
schema: {
input: z =>
z.union([
z.object({
repoUrl: z.string(),
}),
z.object({
numberThing: z.number(),
}),
]),
output: z =>
z.object({
test: z.string(),
}),
},
handler: async ctx => {
ctx.output('test', 'value');
// @ts-expect-error - not valid output type
ctx.output('test', 4);
// @ts-expect-error - not valid output name
ctx.output('test2', 'value');
if ('repoUrl' in ctx.input) {
// @ts-expect-error - not valid input type
const a: number = ctx.input.repoUrl;
const b: string = ctx.input.repoUrl;
// eslint-disable-next-line jest/no-conditional-expect
expect(b).toBeDefined();
}
if ('numberThing' in ctx.input) {
const a: number = ctx.input.numberThing;
// eslint-disable-next-line jest/no-conditional-expect
expect(a).toBeDefined();
// @ts-expect-error - not valid input type
const b: string = ctx.input.numberThing;
}
},
});
expect(action).toBeDefined();
});
});
@@ -30,14 +30,18 @@ export type TemplateActionOptions<
TActionInput extends JsonObject = {},
TActionOutput extends JsonObject = {},
TInputSchema extends
| JsonObject
| z.ZodType
| { [key in string]: (zImpl: typeof z) => z.ZodType } = JsonObject,
| { [key in string]: (zImpl: typeof z) => z.ZodType }
| ((zImpl: typeof z) => z.ZodType) = {
[key in string]: (zImpl: typeof z) => z.ZodType;
},
TOutputSchema extends
| JsonObject
| z.ZodType
| { [key in string]: (zImpl: typeof z) => z.ZodType } = JsonObject,
TSchemaType extends 'v1' | 'v2' = 'v1' | 'v2',
| {
[key in string]: (zImpl: typeof z) => z.ZodType;
}
| ((zImpl: typeof z) => z.ZodType) = {
[key in string]: (zImpl: typeof z) => z.ZodType;
},
TSchemaType extends 'v2' = 'v2',
> = {
id: string;
description?: string;
@@ -62,109 +66,82 @@ type FlattenOptionalProperties<T extends { [key in string]: unknown }> = Expand<
[K in keyof T as undefined extends T[K] ? K : never]?: T[K];
}
>;
/**
* @public
* @deprecated migrate to using the new built in zod schema definitions for schemas
*/
export function createTemplateAction<
TInputParams extends JsonObject = JsonObject,
TOutputParams extends JsonObject = JsonObject,
TInputSchema extends JsonObject = JsonObject,
TOutputSchema extends JsonObject = JsonObject,
TActionInput extends JsonObject = TInputParams,
TActionOutput extends JsonObject = TOutputParams,
>(
action: TemplateActionOptions<
TActionInput,
TActionOutput,
TInputSchema,
TOutputSchema,
'v1'
>,
): TemplateAction<TActionInput, TActionOutput, 'v1'>;
/**
* @public
* @deprecated migrate to using the new built in zod schema definitions for schemas
*/
export function createTemplateAction<
TInputParams extends JsonObject = JsonObject,
TOutputParams extends JsonObject = JsonObject,
TInputSchema extends z.ZodType = z.ZodType,
TOutputSchema extends z.ZodType = z.ZodType,
TActionInput extends JsonObject = z.infer<TInputSchema>,
TActionOutput extends JsonObject = z.infer<TOutputSchema>,
>(
action: TemplateActionOptions<
TActionInput,
TActionOutput,
TInputSchema,
TOutputSchema,
'v1'
>,
): TemplateAction<TActionInput, TActionOutput, 'v1'>;
/**
* This function is used to create new template actions to get type safety.
* Will convert zod schemas to json schemas for use throughout the system.
* @public
*/
export function createTemplateAction<
TInputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType },
TOutputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType },
TInputSchema extends
| { [key in string]: (zImpl: typeof z) => z.ZodType }
| ((zImpl: typeof z) => z.ZodType),
TOutputSchema extends
| { [key in string]: (zImpl: typeof z) => z.ZodType }
| ((zImpl: typeof z) => z.ZodType),
>(
action: TemplateActionOptions<
{
[key in keyof TInputSchema]: z.infer<ReturnType<TInputSchema[key]>>;
},
{
[key in keyof TOutputSchema]: z.infer<ReturnType<TOutputSchema[key]>>;
},
TInputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }
? {
[key in keyof TInputSchema]: z.infer<ReturnType<TInputSchema[key]>>;
}
: TInputSchema extends (zImpl: typeof z) => z.ZodType
? z.infer<ReturnType<TInputSchema>>
: never,
TOutputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }
? {
[key in keyof TOutputSchema]: z.infer<ReturnType<TOutputSchema[key]>>;
}
: TOutputSchema extends (zImpl: typeof z) => z.ZodType
? z.infer<ReturnType<TOutputSchema>>
: never,
TInputSchema,
TOutputSchema,
'v2'
>,
): TemplateAction<
FlattenOptionalProperties<{
[key in keyof TInputSchema]: z.output<ReturnType<TInputSchema[key]>>;
}>,
FlattenOptionalProperties<{
[key in keyof TOutputSchema]: z.output<ReturnType<TOutputSchema[key]>>;
}>,
FlattenOptionalProperties<
TInputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }
? {
[key in keyof TInputSchema]: z.output<ReturnType<TInputSchema[key]>>;
}
: TInputSchema extends (zImpl: typeof z) => z.ZodType
? z.output<ReturnType<TInputSchema>>
: never
>,
FlattenOptionalProperties<
TOutputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }
? {
[key in keyof TOutputSchema]: z.output<
ReturnType<TOutputSchema[key]>
>;
}
: TOutputSchema extends (zImpl: typeof z) => z.ZodType
? z.output<ReturnType<TOutputSchema>>
: never
>,
'v2'
>;
export function createTemplateAction<
TInputParams extends JsonObject = JsonObject,
TOutputParams extends JsonObject = JsonObject,
TInputSchema extends
| JsonObject
| z.ZodType
| { [key in string]: (zImpl: typeof z) => z.ZodType } = JsonObject,
TOutputSchema extends
| JsonObject
| z.ZodType
| { [key in string]: (zImpl: typeof z) => z.ZodType } = JsonObject,
TActionInput extends JsonObject = TInputSchema extends z.ZodType<
any,
any,
infer IReturn
>
? IReturn
: TInputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }
TInputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType } = {
[key in string]: (zImpl: typeof z) => z.ZodType;
},
TOutputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType } = {
[key in string]: (zImpl: typeof z) => z.ZodType;
},
TActionInput extends JsonObject = TInputSchema extends {
[key in string]: (zImpl: typeof z) => z.ZodType;
}
? Expand<{
[key in keyof TInputSchema]: z.infer<ReturnType<TInputSchema[key]>>;
}>
: TInputParams,
TActionOutput extends JsonObject = TOutputSchema extends z.ZodType<
any,
any,
infer IReturn
>
? IReturn
: TOutputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }
: never,
TActionOutput extends JsonObject = TOutputSchema extends {
[key in string]: (zImpl: typeof z) => z.ZodType;
}
? Expand<{
[key in keyof TOutputSchema]: z.infer<ReturnType<TOutputSchema[key]>>;
}>
: TOutputParams,
: never,
>(
action: TemplateActionOptions<
TActionInput,
@@ -172,13 +149,7 @@ export function createTemplateAction<
TInputSchema,
TOutputSchema
>,
): TemplateAction<
TActionInput,
TActionOutput,
TInputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }
? 'v2'
: 'v1'
> {
): TemplateAction<TActionInput, TActionOutput, 'v2'> {
const { inputSchema, outputSchema } = parseSchemas(
action as TemplateActionOptions<any, any, any>,
);
+57 -127
View File
@@ -14,8 +14,6 @@
* limitations under the License.
*/
import { Logger } from 'winston';
import { Writable } from 'stream';
import { JsonObject, JsonValue } from '@backstage/types';
import { TaskSecrets } from '../tasks';
import { TemplateInfo } from '@backstage/plugin-scaffolder-common';
@@ -32,143 +30,75 @@ import {
export type ActionContext<
TActionInput extends JsonObject,
TActionOutput extends JsonObject = JsonObject,
TSchemaType extends 'v1' | 'v2' = 'v1',
> = TSchemaType extends 'v2'
? {
logger: LoggerService;
secrets?: TaskSecrets;
workspacePath: string;
input: TActionInput;
checkpoint<T extends JsonValue | void>(opts: {
key: string;
fn: () => Promise<T> | T;
}): Promise<T>;
output(
name: keyof TActionOutput,
value: TActionOutput[keyof TActionOutput],
): void;
/**
* Creates a temporary directory for use by the action, which is then cleaned up automatically.
*/
createTemporaryDirectory(): Promise<string>;
_TSchemaType extends 'v2' = 'v2',
> = {
logger: LoggerService;
secrets?: TaskSecrets;
workspacePath: string;
input: TActionInput;
checkpoint<T extends JsonValue | void>(opts: {
key: string;
fn: () => Promise<T> | T;
}): Promise<T>;
output(
name: keyof TActionOutput,
value: TActionOutput[keyof TActionOutput],
): void;
/**
* Creates a temporary directory for use by the action, which is then cleaned up automatically.
*/
createTemporaryDirectory(): Promise<string>;
/**
* Get the credentials for the current request
*/
getInitiatorCredentials(): Promise<BackstageCredentials>;
/**
* Get the credentials for the current request
*/
getInitiatorCredentials(): Promise<BackstageCredentials>;
/**
* Task information
*/
task: {
id: string;
};
/**
* Task information
*/
task: {
id: string;
};
templateInfo?: TemplateInfo;
templateInfo?: TemplateInfo;
/**
* Whether this action invocation is a dry-run or not.
* This will only ever be true if the actions as marked as supporting dry-runs.
*/
isDryRun?: boolean;
/**
* Whether this action invocation is a dry-run or not.
* This will only ever be true if the actions as marked as supporting dry-runs.
*/
isDryRun?: boolean;
/**
* The user which triggered the action.
*/
user?: {
/**
* The decorated entity from the Catalog
*/
entity?: UserEntity;
/**
* An entity ref for the author of the task
*/
ref?: string;
};
/**
* The user which triggered the action.
*/
user?: {
/**
* The decorated entity from the Catalog
*/
entity?: UserEntity;
/**
* An entity ref for the author of the task
*/
ref?: string;
};
/**
* Implement the signal to make your custom step abortable https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal
*/
signal?: AbortSignal;
/**
* Implement the signal to make your custom step abortable https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal
*/
signal?: AbortSignal;
/**
* Optional value of each invocation
*/
each?: JsonObject;
}
: /** @deprecated **/
{
// TODO(blam): move this to LoggerService
logger: Logger;
/** @deprecated - use `ctx.logger` instead */
logStream: Writable;
secrets?: TaskSecrets;
workspacePath: string;
input: TActionInput;
checkpoint<T extends JsonValue | void>(opts: {
key: string;
fn: () => Promise<T> | T;
}): Promise<T>;
output(
name: keyof TActionOutput,
value: TActionOutput[keyof TActionOutput],
): void;
/**
* Creates a temporary directory for use by the action, which is then cleaned up automatically.
*/
createTemporaryDirectory(): Promise<string>;
/**
* Get the credentials for the current request
*/
getInitiatorCredentials(): Promise<BackstageCredentials>;
/**
* Task information
*/
task: {
id: string;
};
templateInfo?: TemplateInfo;
/**
* Whether this action invocation is a dry-run or not.
* This will only ever be true if the actions as marked as supporting dry-runs.
*/
isDryRun?: boolean;
/**
* The user which triggered the action.
*/
user?: {
/**
* The decorated entity from the Catalog
*/
entity?: UserEntity;
/**
* An entity ref for the author of the task
*/
ref?: string;
};
/**
* Implement the signal to make your custom step abortable https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal
*/
signal?: AbortSignal;
/**
* Optional value of each invocation
*/
each?: JsonObject;
};
/**
* Optional value of each invocation
*/
each?: JsonObject;
};
/** @public */
export type TemplateAction<
TActionInput extends JsonObject = JsonObject,
TActionOutput extends JsonObject = JsonObject,
TSchemaType extends 'v1' | 'v2' = 'v1',
TSchemaType extends 'v2' = 'v2',
> = {
id: string;
description?: string;
+21 -19
View File
@@ -130,11 +130,7 @@ function checkRequiredParams(repoUrl: URL, ...params: string[]) {
}
}
const isZodSchema = (schema: unknown): schema is z.ZodType => {
return typeof schema === 'object' && !!schema && 'safeParseAsync' in schema;
};
const isNativeZodSchema = (
const isKeyValueZodCallback = (
schema: unknown,
): schema is { [key in string]: (zImpl: typeof z) => z.ZodType } => {
return (
@@ -144,23 +140,20 @@ const isNativeZodSchema = (
);
};
const isZodFunctionDefinition = (
schema: unknown,
): schema is (zImpl: typeof z) => z.ZodType => {
return typeof schema === 'function';
};
export const parseSchemas = (
action: TemplateActionOptions,
action: TemplateActionOptions<any, any, any>,
): { inputSchema?: Schema; outputSchema?: Schema } => {
if (!action.schema) {
return { inputSchema: undefined, outputSchema: undefined };
}
if (isZodSchema(action.schema.input)) {
return {
inputSchema: zodToJsonSchema(action.schema.input) as Schema,
outputSchema: isZodSchema(action.schema.output)
? (zodToJsonSchema(action.schema.output) as Schema)
: undefined,
};
}
if (isNativeZodSchema(action.schema.input)) {
if (isKeyValueZodCallback(action.schema.input)) {
const input = z.object(
Object.fromEntries(
Object.entries(action.schema.input).map(([k, v]) => [k, v(z)]),
@@ -169,7 +162,7 @@ export const parseSchemas = (
return {
inputSchema: zodToJsonSchema(input) as Schema,
outputSchema: isNativeZodSchema(action.schema.output)
outputSchema: isKeyValueZodCallback(action.schema.output)
? (zodToJsonSchema(
z.object(
Object.fromEntries(
@@ -181,9 +174,18 @@ export const parseSchemas = (
};
}
if (isZodFunctionDefinition(action.schema.input)) {
return {
inputSchema: zodToJsonSchema(action.schema.input(z)) as Schema,
outputSchema: isZodFunctionDefinition(action.schema.output)
? (zodToJsonSchema(action.schema.output(z)) as Schema)
: undefined,
};
}
return {
inputSchema: action.schema.input,
outputSchema: action.schema.output,
inputSchema: undefined,
outputSchema: undefined,
};
};