Library
Stagehand
Scripts are phases, phases are steps. Steps have a handler and an optional rollback that fires when a later step fails, with a typed context threaded through the whole run.
npm install @michaelrwalker/stagehand // reference
One script, hover to learn it
Every highlighted token is real Stagehand API — hover or tab to it for what it does.
import { Script, fileStore } from "@michaelrwalker/stagehand";
import { z } from "zod";
const cache = fileStore("./.stagehand-cache.json");
const deploy = new Script({
name: "deploy",
description: "Build, upload and release a service",
})
.defineInput(z.object({ service: z.string(), environment: z.enum(["staging", "production"]) }))
.addPhase("Build", { cache })
.addStep({
name: "compile bundle",
handler: async ({ input, progress }) => {
const bar = progress({ total: 100, label: "compiling" });
const artifact = await compile(input.service, (pct) => bar.update(pct));
bar.done();
return { artifact: artifact.path, bytes: artifact.bytes };
},
})
.addPhase("Release")
.addStep({
name: "upload artifact",
cache,
handler: async ({ ctx, progress }) => {
const bar = progress({ total: ctx.bytes, label: "uploading" });
const uploadId = await upload(ctx.artifact, (sent) => bar.update(sent));
bar.done();
return { uploadId };
},
clean: ["bytes"],
rollbackKeys: ["artifact"],
rollback: async ({ ctx, output, log }) => {
await cdn.delete(output.uploadId);
log(`deleted artifact built from ${ctx.artifact}`);
},
})
.addStep({
name: "shift traffic",
handler: async ({ ctx, input, status }) => {
status(`routing ${input.environment} to ${ctx.uploadId}`);
const healthy = await shiftTraffic(ctx.uploadId);
if (!healthy) throw new Error("health check failed");
return { releaseId: crypto.randomUUID() };
},
});
const result = await deploy.run({
service: "api",
environment: "staging",
});