feat(routines): add workspace-aware routine runs

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-04-02 11:38:57 -05:00
parent 36376968af
commit 909e8cd4c8
38 changed files with 15468 additions and 250 deletions

View file

@ -1,4 +1,5 @@
import { z } from "zod";
import { routineVariableSchema } from "./routine.js";
export const portabilityIncludeSchema = z
.object({
@ -123,6 +124,7 @@ export const portabilityIssueRoutineTriggerManifestEntrySchema = z.object({
export const portabilityIssueRoutineManifestEntrySchema = z.object({
concurrencyPolicy: z.string().nullable(),
catchUpPolicy: z.string().nullable(),
variables: z.array(routineVariableSchema).nullable().optional(),
triggers: z.array(portabilityIssueRoutineTriggerManifestEntrySchema).default([]),
});

View file

@ -214,6 +214,7 @@ export {
updateRoutineSchema,
createRoutineTriggerSchema,
updateRoutineTriggerSchema,
routineVariableSchema,
runRoutineSchema,
rotateRoutineTriggerSecretSchema,
type CreateRoutine,

View file

@ -5,7 +5,44 @@ import {
ROUTINE_CONCURRENCY_POLICIES,
ROUTINE_STATUSES,
ROUTINE_TRIGGER_SIGNING_MODES,
ROUTINE_VARIABLE_TYPES,
} from "../constants.js";
import { issueExecutionWorkspaceSettingsSchema } from "./issue.js";
const routineVariableValueSchema = z.union([z.string(), z.number().finite(), z.boolean()]);
export const routineVariableSchema = z.object({
name: z.string().trim().regex(/^[A-Za-z][A-Za-z0-9_]*$/),
label: z.string().trim().max(120).optional().nullable(),
type: z.enum(ROUTINE_VARIABLE_TYPES).optional().default("text"),
defaultValue: routineVariableValueSchema.optional().nullable(),
required: z.boolean().optional().default(true),
options: z.array(z.string().trim().min(1).max(120)).max(50).optional().default([]),
}).superRefine((value, ctx) => {
if (value.type === "select" && value.options.length === 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["options"],
message: "Select variables require at least one option",
});
}
if (value.type !== "select" && value.options.length > 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["options"],
message: "Only select variables can define options",
});
}
if (value.type === "select" && value.defaultValue != null) {
if (typeof value.defaultValue !== "string" || !value.options.includes(value.defaultValue)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["defaultValue"],
message: "Select variable defaults must match one of the allowed options",
});
}
}
});
export const createRoutineSchema = z.object({
projectId: z.string().uuid(),
@ -18,6 +55,7 @@ export const createRoutineSchema = z.object({
status: z.enum(ROUTINE_STATUSES).optional().default("active"),
concurrencyPolicy: z.enum(ROUTINE_CONCURRENCY_POLICIES).optional().default("coalesce_if_active"),
catchUpPolicy: z.enum(ROUTINE_CATCH_UP_POLICIES).optional().default("skip_missed"),
variables: z.array(routineVariableSchema).optional().default([]),
});
export type CreateRoutine = z.infer<typeof createRoutineSchema>;
@ -62,8 +100,19 @@ export type UpdateRoutineTrigger = z.infer<typeof updateRoutineTriggerSchema>;
export const runRoutineSchema = z.object({
triggerId: z.string().uuid().optional().nullable(),
payload: z.record(z.unknown()).optional().nullable(),
variables: z.record(routineVariableValueSchema).optional().nullable(),
idempotencyKey: z.string().trim().max(255).optional().nullable(),
source: z.enum(["manual", "api"]).optional().default("manual"),
executionWorkspaceId: z.string().uuid().optional().nullable(),
executionWorkspacePreference: z.enum([
"inherit",
"shared_workspace",
"isolated_workspace",
"operator_branch",
"reuse_existing",
"agent_default",
]).optional().nullable(),
executionWorkspaceSettings: issueExecutionWorkspaceSettingsSchema.optional().nullable(),
});
export type RunRoutine = z.infer<typeof runRoutineSchema>;