@zuke/core
Zuke — a code-first, strongly-typed build automation system for Deno.
@zuke/core on JSR ↗ 368 symbols
Symbols
function $(strings: TemplateStringsArray, ...values: Interpolatable[]): Command Run an external command, ergonomically.
function absolutePath(first: string, ...rest: string[]): AbsolutePath Build an AbsolutePath from one or more segments. The first segment (after joining) must be absolute — start with / or a drive letter, or build from an absolute base — otherwise an error is thrown.
const root = absolutePath("/app");
root("src", "main.ts").path; // "/app/src/main.ts"
root.join("..", "shared").path; // "/shared"
absolutePath("C:\\repo", "x").path; // "C:/repo/x"interface AbsolutePath An immutable, absolute filesystem path with a fluent API.
Build one with absolutePath. The value itself is callable — path(...segments) returns a new path with those segments appended — and the equivalent AbsolutePath.join method does the same. toString() yields the path string, so an AbsolutePath can be interpolated into the $ shell helper and passed straight to tool args().
| Member | Signature | Doc |
|---|---|---|
join | join(...segments: string[]): AbsolutePath | Append path segments, returning a new path. |
parent | parent(): AbsolutePath | The parent directory; a root is its own parent. |
relativeTo | relativeTo(base: AbsolutePath | string): string | This path expressed relative to base (e.g. "src/main.ts", "../lib"). |
equals | equals(other: AbsolutePath | string): boolean | Whether other resolves to the same normalised path. |
toString | toString(): string | The normalised path string. |
path | path: string | The normalised path string (forward slashes, ./.. resolved). |
name | name: string | The final segment, e.g. "main.ts" (or "" for a root). |
stem | stem: string | The final segment without its extension, e.g. "main" (".gitignore" has none). |
extension | extension: string | The extension including the dot, e.g. ".ts" (or "" if none). |
isRoot | isRoot: boolean | Whether this path is a filesystem root ("/", "C:/"). |
async function acquireLease(store: StateStore, prefix: string, runId: string, actor: string, now: unknown, ttlMs?: number, runUrl?: string): Promise<HeldLease | null> Take a lease named prefix over runId, or null if a live holder has it.
While held, the lease renews on a background heartbeat until HeldLease.release. That timer never keeps the process alive.
Only an explicit refusal counts as loss. A store reports false from a renewal when the claim is demonstrably somebody else's; it *throws* for a filesystem mutex it could not take in time, or an HTTP 503, or a DNS blip — none of which say anything about who holds the lease. Treating those as loss would abort a healthy build because the state service had a bad second, so they are swallowed and the next tick tries again. A store that stays unreachable lets the claim lapse at its TTL, which is the documented backstop and the honest outcome.
type ActorKind = human | service Whether a person or a machine asked for a run (see RunInitiator).
interface AffectedOptions Configure ExecuteOptions.affected: the base revision and diff seam.
| Member | Signature | Doc |
|---|---|---|
base? | base?: string | The git revision to diff against. Defaults to HEAD (uncommitted changes). |
changedFiles? | changedFiles?: ChangedFilesFn | How to list changed files. Defaults to gitChangedFiles. |
function affectedTargets(order: unknown, changed: unknown): Set<TargetBuilder> Compute the set of targets in order affected by the given changed files.
order must be a valid execution order (dependencies before dependents, as produced by plan/planGraph) so each target's dependencies are already decided when it is visited. A target is affected when its own inputs cover a changed file, when it declares no inputs (unprovable — treated as affected), when a dependency is affected, or when an affected target triggers it.
class AlreadyResumedError extends Error Raised when a run has already been resumed by another process.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The error name. |
class AnnounceError extends Error Raised when an announcement is run before it is fully configured.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The error name. |
interface Announcement A structured announcement assembled by an AnnouncementSettings.
| Member | Signature | Doc |
|---|---|---|
text | text: string | The main message body. |
title? | title?: string | An optional heading rendered above the message. |
level | level: AnnouncementLevel | The outcome level driving the accent colour and icon. |
fields? | fields?: AnnouncementField[] | Labelled details rendered beside the message. |
link? | link?: AnnouncementLink | A clickable action rendered with the announcement. |
interface AnnouncementField A labelled detail rendered beside the message (e.g. a version or environment).
| Member | Signature | Doc |
|---|---|---|
name | name: string | The field's label. |
value | value: string | The field's value. |
type AnnouncementLevel = success | failure | warning | info The outcome an announcement conveys. It drives the accent colour and the icon prepended to the message; defaults to "info".
interface AnnouncementLink A clickable action rendered with the announcement (e.g. a link to a release).
| Member | Signature | Doc |
|---|---|---|
text | text: string | The link's visible text. |
url | url: string | The link's target URL. |
class AnnouncementSettings Fluent settings shared by every announcement: the message content (a body, an optional title, a level, repeatable detail fields and an action link), an optional display name, the webhook destination, and a fetch seam for tests. All chainers return this. Subclasses add any platform-specific configuration and render the payload.
| Member | Signature | Doc |
|---|---|---|
text | text(text: string): this | Set the main message body. |
title | title(title: string): this | Set an optional heading shown above the body. |
level | level(level: AnnouncementLevel): this | Set the outcome the message conveys (default "info"). |
success | success(): this | Shorthand for .level("success"). |
failure | failure(): this | Shorthand for .level("failure"). |
warning | warning(): this | Shorthand for .level("warning"). |
info | info(): this | Shorthand for .level("info"). |
field | field(name: string, value: string): this | Add a labelled detail rendered beside the body. Repeatable. |
link | link(text: string, url: string): this | Set an action link rendered with the message. |
username | username(name: string): this | Override the display name the message is posted under. Honoured by Slack and Discord; ignored by Teams, which has no equivalent field. |
webhook | webhook(url: string): this | Set the incoming-webhook URL to post to. The URL embeds the secret, so source it from a secret parameter. |
fetch | fetch(impl: fetch): this | The fetch implementation to use. Defaults to the global fetch; override it to unit-test without network access. |
bot | bot(): this | Post through the platform's API with a bot/access token instead of an incoming webhook. Pair with token and channel. |
token | token(token: string): this | Set the bot/access token for bot mode (Slack xoxb-…, a Discord bot token, or a Microsoft Graph bearer token). Source it from a secret parameter; Zuke masks it in CI output. Implies bot. |
channel | channel(channel: string): this | Set the channel (id or name) to post to in bot mode. |
send | send(): Promise<void> | Send the announcement: through the platform's API when bot mode was requested, otherwise by posting the payload to the webhook. |
const AnnounceTasks: AnnounceTasksApi Announcement task functions for posting build status to chat platforms.
interface AnnounceTasksApi The shape of AnnounceTasks.
| Member | Signature | Doc |
|---|---|---|
slack | slack(configure?: Configure<SlackAnnouncementSettings>): Promise<void> | Announce to Slack. Configure a SlackAnnouncementSettings: set a .webhook(url) (or .bot().token(t).channel(c) for the Web API) and the message content. |
teams | teams(configure?: Configure<TeamsAnnouncementSettings>): Promise<void> | Announce to Microsoft Teams. Configure a TeamsAnnouncementSettings: set a .webhook(url) (or .bot().token(t).team(id).channel(c) to post through Microsoft Graph) and the message content. |
discord | discord(configure?: Configure<DiscordAnnouncementSettings>): Promise<void> | Announce to Discord. Configure a DiscordAnnouncementSettings: set a .webhook(url) (or .bot().token(t).channel(c) to post through the REST API with a bot token) and the message content. |
interface AnyParameter The non-generic view of a parameter, used by discovery and resolution.
| Member | Signature | Doc |
|---|---|---|
resolve_ | resolve_(raw: string | undefined): void | Resolve from a raw input (or undefined when none was supplied). |
isSet_ | isSet_(): boolean | Whether the parameter resolved to a defined value (used by .requires()). |
stringValue_ | stringValue_(): string | undefined | The resolved value as a string, or undefined if unset (for masking). |
name_? | name_?: string | Property name, assigned during discovery. Undefined until then. |
description_? | description_?: string | Human-readable description shown in --help/--list. |
kind_ | kind_: ParamKind | The runtime value kind. |
required_ | required_: boolean | Whether a value must be supplied (no default). |
options_? | options_?: unknown | The allowed string choices, if restricted with Parameter.options. |
envName_? | envName_?: string | An explicit environment variable name override. |
flagName_? | flagName_?: string | An explicit CLI flag name override, without the leading dashes. |
hasFallback_ | hasFallback_: boolean | Whether the parameter has a declared default value. |
secret_ | secret_: boolean | Whether the value is sensitive and should be masked in CI output. |
array_ | array_: boolean | Whether the value is a comma-separated / repeatable list (.array()). |
source_? | source_?: SecretSource | A provider that resolves the value when no flag/env supplied one. |
default_? | default_?: string | The declared default rendered as a string (an array default is joined with commas), or undefined when the parameter has no default or an empty-list one. For display in tool schemas and --list; never a secret value. |
function appendJobSummary(markdown: string): boolean Append markdown to the Actions job summary, returning whether it was written. Outside Actions (no GITHUB_STEP_SUMMARY) it is a no-op returning false, so the same code path works locally.
Best-effort by design: an unwritable summary file reports false rather than throwing. A report that could not be *displayed* must never fail the build that produced it — the build's own result is the signal that matters.
type Architecture = x86_64 | aarch64 The CPU architectures Zuke recognises.
type ArchiveFormat = tar.gz | zip A packed download format, unpacked after the checksum is verified.
async function archiveOutputs(outputs: unknown, host: OutputHost): Promise<Uint8Array> Archive a target's outputs into a gzipped tar of their current contents. A declared output that does not exist is skipped, as is anything under a .git or .zuke directory.
function assert(condition: unknown, message?: string): asserts condition Assert that condition is truthy, narrowing it for the rest of the scope. Throws an AssertionError with message otherwise.
async function assertDirectoryExists(path: PathLike): Promise<void> Assert that path exists and is a directory. Async (stats the filesystem).
function assertExists(value: T, message?: string): NonNullable<T> Assert that value is neither null nor undefined, returning it narrowed to its non-nullable type so it can be used inline.
const token = assertExists(Deno.env.get("TOKEN"), "TOKEN is required");async function assertFileExists(path: PathLike): Promise<void> Assert that path exists and is a file. Async (stats the filesystem).
class AssertionError extends Error Raised by the assertion helpers when an expectation fails.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The error name. |
function assertSafeEntryName(name: string): void Reject an archive entry whose name would escape the destination directory — an absolute path or one with a .. segment (a "zip slip"). A downloaded or poisoned archive must never place files outside where it is being unpacked.
function assertSafeLinkTarget(entryName: string, target: string): void Reject a symlink whose target would resolve outside the destination directory — an absolute target, or a relative one that climbs (with ..) above the extraction root once resolved against the link's own directory. A file entry's name is bounded by assertSafeEntryName; a symlink adds a second escape vector (its target), so a poisoned tarball can't plant bin/x -> ../../etc.
async function assertWrapperConformance(makeSettings: unknown, tool: string, options: WrapperConformanceOptions): Promise<void> Assert that a tool wrapper conforms: makeSettings() spawns tool, resolves it per options.resolution, and reports a missing binary as a "./tooling.ts".ToolNotFoundError.
makeSettings is called once per check, so each check gets a pristine instance. The resolution check runs against a throwaway temp directory holding a fake node_modules/.bin/<tool> shim, with ZUKE_TOOL_RESOLUTION unset for the duration and restored afterwards; no real subprocess is ever launched.
A wrapper whose run() resolves something at run time must have that pinned inside makeSettings — () => new DockerComposeUpSettings().usePlugin(), say — or the missing-binary check would probe the ambient host. It reports a ToolNotFoundError raised for any binary other than the planted one as a failure, so such a wrapper cannot pass by accident on a host that lacks the real tool.
function bearerChallenge(parts?: BearerChallenge): string A WWW-Authenticate: Bearer challenge built from parts.
Returns a bare Bearer when nothing usable is supplied, and drops any single part that survives filtering as empty, so a caller cannot produce a malformed header by passing an odd string.
interface BearerChallenge The parts of a WWW-Authenticate: Bearer challenge Zuke emits.
| Member | Signature | Doc |
|---|---|---|
metadataUrl? | metadataUrl?: string | Absolute URL of the protected resource metadata document (RFC 9728). |
scopes? | scopes?: unknown | Scopes required for the attempted operation — all of them, in one go. |
error? | error?: ChallengeError | The failure, omitted for a request that presented no credentials. |
description? | description?: string | Developer-facing explanation; never shown to an end user. |
function box(style: Style, content: string | unknown, options?: BoxOptions): string[] A bordered panel around content (a string, split on newlines, or an array of lines). Content may carry ANSI codes; padding is measured on the visible text so the border stays flush.
interface BoxOptions Options for box.
| Member | Signature | Doc |
|---|---|---|
title? | title?: string | A title embedded in the top border. |
padding? | padding?: number | Horizontal padding inside the border, in spaces. Defaults to 1. |
width? | width?: number | Force an inner width; widened automatically to fit content and title. |
border? | border?: unknown | Styles for the border characters. Defaults to ["dim"]. |
titleStyle? | titleStyle?: unknown | Styles for the title text. Defaults to ["bold"]. |
class BrowserOpenSettings extends ToolSettings Settings for BrowserTasks.open. The binary and argv are derived from the platform (ToolSettings.os_); the shared chainers (quiet, noThrow, toolPath, …) apply as on any tool.
| Member | Signature | Doc |
|---|---|---|
url | url: string | The validated URL this invocation opens. |
const BrowserTasks: BrowserTasksApi Task functions for the user's browser.
interface BrowserTasksApi The shape of BrowserTasks.
| Member | Signature | Doc |
|---|---|---|
open | open(url: string, configure?: Configure<BrowserOpenSettings>): Promise<CommandOutput> | Open url in the default browser. Resolves when the opener process exits (browsers detach, so this is launch, not page load). ``ts await BrowserTasks.open("https://github.com/zuke-build/zuke"); `` |
class Build Base class for user-defined builds. Provides no targets of its own; subclasses declare targets as properties. Optionally override the lifecycle hooks.
| Member | Signature | Doc |
|---|---|---|
onStart | onStart(): void | Promise<void> | Called once before any target runs. |
onFinish | onFinish(_result: BuildResult): void | Promise<void> | Called once after the run completes (success or failure). |
onTargetStart | onTargetStart(_name: string): void | Promise<void> | Called just before a target's body executes (not for skipped/cached). |
onTargetEnd | onTargetEnd(_name: string, _status: TargetStatus): void | Promise<void> | Called after each target settles, with its final status. |
recoverWith | recoverWith(): Remediation[] | Remediations applied to every target, running after each target's own "./target.ts".TargetBuilder.recoverWith when its body fails. Override to attach a global AI fixer once instead of repeating it per target; the default is none. Both styles compose — a target's own remediations run first, then these. ``ts class CI extends Build { key = parameter("OpenAI API key").secret(); override recoverWith() { return [aiFixer((f) => f.provider("openai").apiKey(this.key))]; } lint = target().executes(() => DenoTasks.lint()); // healed globally } `` |
remoteCache | remoteCache(): RemoteCacheStore | undefined | The "./remote_cache.ts".RemoteCacheStore that shares target "./target.ts".TargetBuilder.outputs across machines. Override to declare one in code; the default is none, and — unless overridden — the executor falls back to "./remote_cache.ts".envCacheStore (the ZUKE_REMOTE_CACHE_* environment variables). Applies to targets that declare both inputs and outputs. ``ts class CI extends Build { override remoteCache() { return new HttpCacheStore({ url: this.cacheUrl.value, token: this.cacheToken.value }); } build = target().inputs("src").outputs("dist").executes(...); } `` |
stateStore | stateStore(): StateStore | undefined | The "./state/store.ts".StateStore that persists this build's run records. Override to declare one in code; the default is none, and — unless overridden — the executor falls back to the ZUKE_STATE_URL / ZUKE_STATE_DIR environment variables, then (only when the run opts into durable state) a filesystem store under <root>/.zuke/runs. ``ts class CD extends Build { override stateStore() { return new HttpStateStore({ url: this.stateUrl.value, token: this.stateToken.value }); } deploy = target().executes(async (ctx) => { await ctx.state.set({ at: "sit-7" }); }); } `` |
deadline | deadline(): string | number | undefined | A wall-clock budget for a whole run, after which a reaping sweep settles it failed — a duration like "45m" or milliseconds. No deadline by default. It bounds *running*, not existing. A run parked at a .waitsFor(...) gate is not spending it — the deadline is pushed forward on resume by however long the run was parked, so a build with a 72-hour approval gate and a 45-minute deadline still has its 45 minutes when the approval arrives. The waiting is bounded by the gate's own .timeout(). A run with a live process working on it is never settled for time either: the sweep asks whether anyone is still there before it looks at the deadline. Nothing this is for escapes that — a hung process stops renewing its lease, and a run killed over and over has no holder at all. What it is really for is the run that stops making progress without failing — a process that hangs, or one killed so hard that its work is repeatedly picked up and abandoned again. Without a deadline such a run has no end state at all; with one it reaches a terminal status, which is what anything downstream is waiting for. ``ts class Ci extends Build { override deadline() { return "45m"; } } `` |
extraEdges | extraEdges(_targets: Map<string, TargetBuilder>): OrderingEdge[] | Extra soft ordering edges to impose on the plan, beyond the dependsOn / before / after declared on targets. Override to feed an external graph — e.g. a monorepo's dependency-graph.json — into scheduling without wiring every edge by hand. Return [before, after] pairs from the passed targets map (keyed by dotted name); each means before runs before after. Edges whose endpoints are not both in a run's execution set are ignored, and a cycle is reported with the usual friendly error. These are execution-ordering edges. Like .before() / .after(), they are *not* reflected in CI generated by cicd() — a fan-out job's needs: mirrors hard dependsOn only — so an ordering that CI must also honour has to be expressed as a dependsOn, not a soft edge. ``ts class Monorepo extends Build { web = target().executes(...); api = target().executes(...); override extraEdges(t: Map<string, Target>) { // api must build before web, per the external dependency graph. const edges: OrderingEdge[] = []; const api = t.get("api"), web = t.get("web"); if (api && web) edges.push([api, web]); return edges; } } `` |
orderWith | orderWith(_targets: Map<string, TargetBuilder>): OrderingEdge[] | Promise<OrderingEdge[]> | A lazy, per-run provider of soft ordering edges, merged with extraEdges. Unlike extraEdges — synchronous, evaluated at construction — this may be async and is evaluated when a run plans, so it can read an external source (a monorepo's dependency-graph.json, an API) to decide ordering at run time. The consumer keeps ownership of that graph; Zuke only binds it in. Return [before, after] edges over the run's targets; an edge whose endpoints are not both in the execution set is ignored, and cycles are reported with the usual friendly error. Like extraEdges, these are execution-ordering edges only: they are honoured by a run and by zuke cancel (the compensation order), but not by the static graph/--list views (which never run the provider) nor by cicd()-generated CI (whose needs: mirrors hard dependsOn). ``ts override async orderWith(t: Map<string, Target>): Promise<OrderingEdge[]> { const graph = await loadDependencyGraph(); // e.g. dependency-graph.json return graph.edges.flatMap(([before, after]) => { const from = t.get(before), to = t.get(after); return from && to ? [[from, to] as OrderingEdge] : []; }); } `` |
registry | registry(): BuildRegistry | undefined | The "./registry/registry.ts".BuildRegistry this build registers itself in (zuke register) and that a registry-backed zuke mcp server discovers pipelines from. Override to declare one in code; the default is none, and — unless overridden — the resolution falls back to the ZUKE_REGISTRY_URL / ZUKE_REGISTRY_DIR environment variables, then (for zuke register) a filesystem registry under <root>/.zuke/builds. Kept a separate concern from stateStore (a run history and a build catalog are different things), so a consumer can host a richer catalog as a plugin. ``ts class CD extends Build { override registry() { return new HttpBuildRegistry({ url: this.registryUrl.value, token: this.registryToken.value }); } } `` |
mcpIdentity | mcpIdentity(): McpIdentityHook | undefined | A per-request identity hook for zuke mcp — resolve a trusted caller from the request context (an authenticating reverse proxy's header) so a shared, multi-user server attributes each call to the real engineer rather than a client-self-reported label. When set, the resolved actor overrides --actor, the environment, and the client label for that call, and flows to the audit trail, run records, lock holders, and (for a registry-spawned build) the child's ZUKE_ACTOR; a throwing hook rejects the request before anything runs. Default: none — stdio/local use is unchanged. ``ts class ControlPlane extends Build { override mcpIdentity() { return (ctx: McpRequestContext) => { // The proxy strips any client copy of this header and injects its own. const sub = ctx.headers.get("x-forwarded-user"); if (!sub) throw new Error("no identity from proxy"); return { actor: sub, via: "oauth-proxy" }; }; } } `` |
mcpAuth | mcpAuth(): McpAuthenticator | undefined | An authenticator for zuke mcp — the general form of Build.mcpIdentity, for a server callers reach directly rather than through a proxy that has already identified them. It runs before any dispatch, may be asynchronous (verifying a signature is), and refuses by returning an McpAuthReject rather than by throwing — so over HTTP the refusal is answered with its own status and WWW-Authenticate challenge, which is how an MCP client discovers where to authenticate. The identity it resolves overrides --actor, the environment, and the client label for that call, and flows to the audit trail, run records, lock holders, and (for a registry-spawned build) the child's ZUKE_ACTOR, ZUKE_ACTOR_KIND and ZUKE_ACTOR_ROLES. Throwing still refuses the request: the seam is fail-closed. Default: none. Declare either this or Build.mcpIdentity; declaring both is refused when the server starts, rather than letting one silently win. ``ts class ControlPlane extends Build { override mcpAuth(): McpAuthenticator { return { authenticate: async (ctx: McpRequestContext) => { const claims = await verifyBearer(ctx.headers.get("authorization")); if (claims === null) { return { status: 401, error: "invalid_token", challenge: "Bearer" }; } return { actor: claims.sub, kind: "human", roles: claims.roles }; }, }; } } `` |
mcpProtectedResource | mcpProtectedResource(): ProtectedResourceSettings | undefined | Declares this MCP endpoint an OAuth 2.0 protected resource, so a client that has never authenticated can find out where to get a token. zuke mcp --http then publishes the RFC 9728 metadata document and names it in every WWW-Authenticate challenge, which is the whole of what claude mcp add --transport http <url> needs to open a browser and authenticate with nothing pasted. Zuke issues no tokens and hosts no /authorize, /token or /register endpoint — those belong to the identity provider named here, and Build.mcpAuth is where the tokens it mints are verified. Default: none, and the server behaves exactly as before. The one thing to get right is that three strings must agree byte for byte: the resource identifier below, the resource parameter the client sends, and the audience the identity provider puts in the token. When they differ every token fails validation, and nothing in the error says why. ``ts class ControlPlane extends Build { override mcpProtectedResource(): ProtectedResourceSettings { return protectedResource("https://build.example.com/mcp") .authorizationServer("https://acme.eu.auth0.com") .scopes("zuke:run") .name("Acme build server"); } } `` |
unforceable | unforceable(): TargetBuilder[] | Targets an operator may not force with zuke force — the steps whose body must actually run, whatever a live incident looks like. Forcing settles a target without executing it: skipped takes a step off the plan, succeeded records that a person did it by hand. That is the right tool for a step that cannot succeed and the wrong one for a step whose whole purpose is to be the thing that happened — a production apply, a signing step, a migration. Naming those here refuses the force rather than trusting an operator under pressure to remember which is which. Returns target references, not names, so renaming a target keeps the list correct instead of silently emptying it. ``ts class CD extends Build { applyProduction = target().executes(() => applyTerraform()); override unforceable() { return [this.applyProduction]; } } `` |
mcpAuthorize | mcpAuthorize(identity: McpIdentity, call: McpCall): McpAuthorization | Decide whether an authenticated MCP caller may make one call — the build-level half of [authorization](../../docs/mcp.md#authorization). The default implementation is defaultMcpAuthorize: read tools need read, running a target needs run (or whatever the target's requiresRole asks for), and a run-scoped mutation needs the run's initiator or operator. Override to express what the engine cannot know — a change window, team ownership, a freeze. Only consulted when the server authenticates its callers. With no mcpAuth()/mcpIdentity() every caller is treated as holding every role, so --allow-run, --protect and the operator token remain exactly the gates they were; a local stdio server is unchanged. Called after the allow-list and operator-token checks, so it can only narrow what those already permit — an override cannot open a target the server was not started to expose. ``ts class ControlPlane extends Build { override mcpAuthorize(identity: McpIdentity, call: McpCall) { if (call.tool === "run:promote" && !inChangeWindow()) { return { allow: false, reason: "outside the change window" }; } return defaultMcpAuthorize(identity, call); } } `` |
interface BuildCache The incremental cache used by the executor to skip up-to-date targets.
| Member | Signature | Doc |
|---|---|---|
upToDate | upToDate(target: TargetBuilder): Promise<boolean> | Whether target is up-to-date: it declares inputs, their fingerprint matches the last successful run, and every declared output still exists. |
record | record(target: TargetBuilder): Promise<void> | Record target's current fingerprint after a successful run. |
save | save(): Promise<void> | Persist the store if anything changed. |
interface BuildDescriptor A versioned snapshot of one registered build. Persisted as JSON; a registry's opaque version (an ETag / content hash) drives compare-and-swap writes so two registrations racing at the same version cannot both win.
| Member | Signature | Doc |
|---|---|---|
id | id: string | Stable id of the build (its class name, unless overridden). |
name | name: string | Human-facing build name (the build class name). |
location | location: BuildLocation | Where the build lives, so a runner can launch it. |
surface | surface: CliDescription | The build's CLI surface, exactly as "../describe.ts".describeCli produces it. |
actor | actor: string | Who registered the build (a resolved actor; secrets never appear here). |
createdAt | createdAt: string | ISO-8601 timestamp when the build was first registered. |
updatedAt | updatedAt: string | ISO-8601 timestamp of the last registration write. |
type BuildLocation = unknown | unknown Where a registered build lives, so a runner can launch it. Two forms: a module (the entry file deno run executes — the form zuke register writes) or an explicit command (a launch argv, for a build fronted by a wrapper script). Both carry the working directory and, in CI, the repository.
interface BuildQuery Filters for "./registry.ts".BuildRegistry.listBuilds; all fields optional.
| Member | Signature | Doc |
|---|---|---|
name? | name?: string | Keep only builds whose name equals this. |
since? | since?: string | Keep only builds registered at or after this ISO-8601 timestamp. |
interface BuildRegistry Pluggable persistence for BuildDescriptors. version is an opaque token (an ETag or content hash) used for optimistic concurrency: a write only lands if the stored version still matches the one the writer last read, so two registrations racing at the same version cannot both win.
| Member | Signature | Doc |
|---|---|---|
getBuild | getBuild(id: string): Promise<unknown | null> | Fetch a build and its current version, or null if it is not registered. |
register | register(descriptor: BuildDescriptor, expectedVersion: string | null): Promise<PutBuildResult> | Write descriptor only if the stored version equals expectedVersion (null meaning "must not exist yet"). Returns the new version, or a conflict when the stored version has moved on — the caller re-reads and retries. |
deregister | deregister(id: string): Promise<void> | Remove a registered build by id; a missing build is not an error. |
listBuilds | listBuilds(query: BuildQuery): Promise<BuildSummary[]> | List registered builds matching query, newest first (by createdAt, then id). |
type BuildRegistryFactory = unknown A () => factory the kit calls once to obtain the registry under test.
interface BuildResult Result passed to the Build.onFinish lifecycle hook.
| Member | Signature | Doc |
|---|---|---|
ok | ok: boolean | Whether every executed target succeeded (also true for a suspended run). |
executed | executed: string[] | Names of the targets that ran, in execution order. |
error? | error?: unknown | The error that aborted the run, if any. |
suspended? | suspended?: boolean | True when the run suspended at a .waitsFor(...) gate rather than finishing — its state is saved and it can be resumed later. The process still exits 0. |
cancelled? | cancelled?: boolean | True when the run was cancelled (via options.signal / Ctrl-C, or by another process running zuke cancel) rather than failing on its own. Its compensations have run and the record is cancelled. ok is false. |
runId? | runId?: string | The run's id, when a run identity was established (always, in practice — every "./executor.ts".execute generates one). Lets the caller point a follow-up (zuke runs show, zuke cancel) at this run. |
interface BuildSummary A compact registry listing row, returned by "./registry.ts".BuildRegistry.listBuilds.
| Member | Signature | Doc |
|---|---|---|
id | id: string | The build id. |
name | name: string | The build name. |
actor | actor: string | Who last registered the build. |
createdAt | createdAt: string | ISO-8601 first-registration timestamp. |
updatedAt | updatedAt: string | ISO-8601 timestamp of the last registration write. |
interface CancelOptions Options for cancelRun.
| Member | Signature | Doc |
|---|---|---|
runId | runId: string | The id of the run to cancel. |
stateStore? | stateStore?: StateStore | false | Durable store the run lives in. Defaults to the same resolution as a normal run (explicit → stateStore() override → env → .zuke/runs); cancel always needs one. |
actor? | actor?: string | Who to attribute the cancellation to in the audit trail. |
readEnv? | readEnv?: unknown | Reads an environment variable (secrets re-resolve from here for compensations). |
silent? | silent?: boolean | Suppress progress output. |
reporter? | reporter?: Reporter | Custom reporter; overrides silent. |
also? | also?: string[] | Extra compensation target names to run first (a timed-out wait whose onTimeout names a specific compensation target routes through here). |
expiredWait? | expiredWait?: unknown | The wait whose expired deadline caused this cancellation, when one did: the target's name and the message describing the miss. Recorded on that target's row as it settles, so the terminal record still says why. |
interface CancelResult The outcome of cancelRun.
| Member | Signature | Doc |
|---|---|---|
runId | runId: string | The run that was cancelled. |
status | status: RunStatus | The run's status after cancelling (cancelled, or the terminal status on a no-op). |
noop | noop: boolean | True when the run was already terminal and nothing was done. |
compensated | compensated: string[] | Names of compensation targets whose bodies ran. |
failures | failures: CompensationFailure[] | Compensations that threw (recorded, non-fatal). |
async function cancelRun(build: Build, options: CancelOptions): Promise<CancelResult> Cancel the run options.runId for build: transition it to cancelling (exactly one canceller drives the walk; a live owning process observes the change and aborts), run the compensations of every succeeded target in reverse order, and settle the record as cancelled. Idempotent — cancelling an already-terminal run is a friendly no-op.
type ChallengeError = invalid_token | insufficient_scope How a bearer challenge names the failure, when there is one to name.
type ChangedFilesFn = unknown Lists the files changed since base (a git revision), each path relative to the repository root. The seam behind ExecuteOptions.affected; defaults to gitChangedFiles and is overridable so the affected plan can be tested without a real git repository.
async function checkBuildRegistry(make: BuildRegistryFactory): Promise<ConformanceResult[]> Run the build-registry conformance scenarios against the registry make builds.
const CHECKOUT_ACTION: actions/checkout The action a CiCheckout is generated from when pins are resolved.
async function checkStateStore(make: StateStoreFactory, options?: ConformanceOptions): Promise<ConformanceResult[]> Run the state-store conformance scenarios against the store make builds.
interface CiActionRef A pinned action reference, and the version its commit corresponds to.
The version is emitted as a trailing # v1.2.3 comment, which is not decoration: Dependabot reads it to know which version a pinned SHA is, and rewrites both together when it bumps. A generated workflow that dropped it would leave automated bumps with no version to track.
| Member | Signature | Doc |
|---|---|---|
ref | ref: string | The pinned reference, owner/repo@<sha>. |
version? | version?: string | The version the SHA corresponds to, e.g. v7.0.1. |
interface CiBootstrap The single step every GitHub job starts with: the zuke-build/zuke composite action, which hardens the runner, checks the repository out, and installs Deno if asked — the three steps a Zuke job used to spell out separately.
It is the default because those three are not three decisions. They are one prelude whose parts only work in one order, and writing them out three times per workflow meant three pinned SHAs to keep current in every generated file rather than one, inside an action that is itself versioned and tested.
The CiHardenRunner and CiCheckout options are still how a job configures it — they become the action's inputs. What changes is how many steps that renders, not what a build declares. A job that opts out of either with harden: false or checkout: false falls back to the separate steps, since one action cannot do half of itself.
| Member | Signature | Doc |
|---|---|---|
action? | action?: CiUses | The pinned action reference. Defaults to the release this version of Zuke was built against, or to whatever a pins resolver returns for ZUKE_ACTION — which is the form to prefer, because a pin baked into a published package goes stale between releases and a bot's bump to the generated file would be reverted by the next regeneration. |
denoVersion? | denoVersion?: string | Install this Deno version, for a job that runs deno directly rather than through the ./zuke launcher (which bootstraps its own). |
name? | name?: string | The step name. Defaults to "Harden and check out with Zuke". |
function cicd(spec: CiFileSpec): CiFile Declare a CI file as a build field. Running the build regenerates it (and the generate-ci command writes it on demand), so the committed configuration is generated from code rather than hand-maintained.
The provider is the only required field: cicd({ provider: "github" }) declares a workflow at .github/workflows/ci.yml that runs the build on push/PR to main. Override only what else you need.
class MyBuild extends Build {
ci = cicd({ provider: "github" }); // sensible default workflow
// …or customise:
gitlab = cicd({ provider: "gitlab", pipeline: { jobs: [{ steps: [...] }] } });
}interface CiCheckout The repository checkout, emitted as an actions/checkout step after any CiHardenRunner and before the job's own steps. Like hardening, the pinned action reference is required.
| Member | Signature | Doc |
|---|---|---|
action? | action?: CiUses | The pinned action reference. Omit it when the file supplies a pins resolver. |
persistCredentials? | persistCredentials?: boolean | Keep the token in git config so a later step can push. Defaults to false: a job that does not push should not leave a credential behind. |
ref? | ref?: string | The ref to check out. Defaults to the one that triggered the run. |
fetchDepth? | fetchDepth?: number | How much history to fetch. 0 means the full history — needed by anything that walks past commits, such as a secret scan. |
name? | name?: string | The step name. Defaults to "Checkout". |
interface CiConcurrency A concurrency group: at most one run per group, optionally cancelling the prior one.
| Member | Signature | Doc |
|---|---|---|
group | group: string | The group key (often interpolated, e.g. ci-${{ github.ref }}). |
cancelInProgress? | cancelInProgress?: boolean | Cancel an in-progress run in the same group when a new one starts. |
class CiFile A declared CI file. Assign one (via cicd) to a build field and Zuke keeps the file on disk in sync with the definition when the build runs.
| Member | Signature | Doc |
|---|---|---|
derived | derived(): boolean | Whether this file's jobs are derived from the build rather than declared. |
pipelineFor | pipelineFor(targets: Map<string, TargetBuilder>): CiPipeline | The pipeline this file renders. Jobs come from the invoked targets, or from a full fan-out of the graph, or — failing both — from the declared pipeline. |
at | at(path: string): CiFile | The same file bound to path — used to name a file from its field. |
render | render(): string | Render the file's YAML content (the base pipeline; fan-out is resolved at discovery). |
provider | provider: CiProvider | The provider this file renders for. |
path | path: string | The output path, once resolved. |
explicitPath | explicitPath: boolean | Whether path came from the spec rather than a default. |
pipeline | pipeline: CiPipeline | The base pipeline (pipeline-level fields, and the jobs unless fanning out). |
fanOut? | fanOut?: FanOutOptions | Fan-out options, when this file expands the build's targets into jobs. |
invokes? | invokes?: unknown | The targets this file runs as jobs, when declared with invokes. |
pins? | pins?: CiPinResolver | Resolves pinned action references, so a SHA is stated once per repository. |
interface CiFileSpec A CI configuration file declared on a build: a pipeline bound to a path.
| Member | Signature | Doc |
|---|---|---|
provider? | provider?: CiProvider | The provider to render for. Defaults to "github", which is what the .github/workflows default path assumes anyway. |
pins? | pins?: CiPinResolver | Resolves each action's pinned reference by name, so hardening and checkout can be requested without restating a SHA. With a resolver, every job is hardened and checked out by default — the prelude nearly every job needs — and a job opts out with harden: false or adjusts the policy without naming the action again. |
path? | path?: string | The output path (relative to the working directory). Defaults to the field name the file is declared on, in the provider's conventional directory: releaseWorkflow = cicd({...}) writes .github/workflows/release.yml. A trailing Workflow/Ci/Yaml is dropped, and camelCase becomes kebab-case. Recovering the name from the field is how target() works too, so a workflow needs no more ceremony than a target. Falls back to the provider's single conventional file (.github/workflows/ci.yml, .gitlab-ci.yml, …) when the name is not available — a file built outside a build class. |
pipeline? | pipeline?: CiPipeline | The pipeline to render. Defaults to a single build job that runs the build. |
fanOut? | fanOut?: boolean | FanOutOptions | Fan the build's targets out into one CI job per target, wired by their dependencies (see fanOutPipeline). true uses the defaults; pass FanOutOptions to customise. When set, pipeline supplies the pipeline-level fields (name, triggers, …) and its jobs are ignored. |
invokes? | invokes?: unknown | The targets this workflow runs — one job each, in place of hand-written CiPipeline.jobs. This is the intended way to declare a workflow. A job is almost entirely implied by its target, so naming the targets is usually the whole declaration: the id, the display name, the ./zuke <target> command, and the needs: edges between jobs all come from the build graph. Pass a CiInvocation instead of a bare target only for what the runner decides rather than the build — a matrix, token scopes, an egress policy. Each job runs its target's *whole* subgraph in one process, exactly as ./zuke <target> does locally — so dependencies inside a target run in-process and need no cache to share their output. Use fanOut instead to give every target in the graph its own job, which does need a remote cache. Targets are passed as references (this.ci), not names, so a rename is a compile error rather than a workflow that silently runs nothing. As with dependsOn, that means the declaration must appear below the targets it invokes — class fields initialise top-to-bottom, so a forward reference is undefined. Declaring workflows last is the simplest way to satisfy it. |
interface CiHardenRunner Runner hardening, emitted as a step-security/harden-runner step before anything else in the job.
This cannot move into the build: the point of the step is to install an egress control *before* build code runs, so a build that set it up itself would be the very code it is meant to contain. Generating it is the next best thing — the policy is declared in one place, in code, next to the job it protects.
The pinned action reference is required rather than defaulted. A default would mean either a floating tag (which supply-chain scanners reject as an unpinned use) or a commit SHA baked into @zuke/core that goes stale between releases. Passing it makes the pin the caller's — and lets a build source it from wherever its bumps are automated.
| Member | Signature | Doc |
|---|---|---|
action? | action?: CiUses | The pinned action reference, e.g. step-security/harden-runner@<sha>. Omit it when the file supplies a CiFileSpec.pins resolver, which is the better arrangement: the SHA is then stated once for the repository rather than at every use. |
egress? | egress?: audit | block | "audit" records outbound connections; "block" drops everything outside allowedEndpoints. Defaults to "audit" — the safe choice for a job with no secrets, where a false block would be worse than an unrecorded call. |
allowedEndpoints? | allowedEndpoints?: string[] | The hosts a "block" policy permits, as host:port. Ignored when auditing. Every entry should be traceable to something the build actually reaches. |
name? | name?: string | The step name. Defaults to "Harden the runner". |
function ciHost(env?: unknown): string A short identifier for the detected CI host, or "local" when not on CI. Recognises GitHub Actions, GitLab CI, Azure Pipelines, Bitbucket Pipelines, and — as the generic "ci" — the common markers other systems set, including Jenkins, Buildkite, CircleCI, Travis and TeamCity.
Prefer detectCiHost for new code: its values match CiProvider. This function is kept for compatibility and uses longer, host-specific names.
type CiHost = github | gitlab | azure | bitbucket | local The CI host a build is running on, or "local" when not on CI. The names match CiProvider so they compose with CI generation and per-host integrations (e.g. posting a review to the right pull-request API).
interface CiInvocation One job's worth of a workflow, derived from a target.
A job's shape is almost entirely implied by the target it runs: the id and display name come from the target, the command is ./zuke <target>, and the needs: edges come from the target's dependsOn. So an invoked target usually needs nothing said about it at all — pass the target and the job is generated.
The fields here are the residue that genuinely cannot be inferred, because they are properties of the *runner* rather than of the work: which OS matrix to fan out over, which token scopes the job needs, how much egress to permit, how long to allow. Set one only when the default is wrong.
| Member | Signature | Doc |
|---|---|---|
target | target: TargetBuilder | The target this job runs. |
id? | id?: string | Override the job id (defaults to the target's name, CI-sanitised). |
name? | name?: string | Override the display name (defaults to the target's description, else its name). |
runsOn? | runsOn?: string | The runner, when it differs from the pipeline default. |
matrix? | matrix?: Record<string, Array<string | number>> | A build matrix — fanning one target out over several OSes, say. |
failFast? | failFast?: boolean | Let the other matrix legs finish when one fails. |
permissions? | permissions?: Record<string, string> | The token permissions this job needs (see CiJob.permissions). |
timeoutMinutes? | timeoutMinutes?: number | Fail the job after this many minutes. |
harden? | harden?: CiHardenRunner | false | Harden this job's runner, overriding the pipeline default. |
checkout? | checkout?: CiCheckout | false | Check out in this job, overriding the pipeline default. |
bootstrap? | bootstrap?: CiBootstrap | false | The prelude action for this job, overriding the pipeline default. |
if? | if?: string | A condition gating the job. |
env? | env?: Record<string, string> | Environment variables for the target's own step — where a secret is mapped in, e.g. { GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" }. |
after? | after?: unknown | Extra needs: edges beyond those implied by the target's dependsOn. Use it to order two invoked targets that are independent in the build graph but must not run concurrently in CI. |
before? | before?: CiStep[] | Steps to run before the target, for something no target can do (see below). |
then? | then?: CiStep[] | Steps to run after the target. |
steps? | steps?: CiStep[] | Replace the generated ./zuke <target> step entirely. The escape hatch of last resort — prefer before/then, and prefer moving the work into the target over either. |
type CiInvokes = TargetBuilder | CiInvocation A target to invoke, bare when the derived job needs no adjustment.
interface CiJob A job: a named unit of work with steps, optionally fanned out by a matrix.
| Member | Signature | Doc |
|---|---|---|
id? | id?: string | Stable identifier, used as the job key and as a dependency target. Defaults to "build". |
name? | name?: string | Human-readable job name. |
runsOn? | runsOn?: string | The runner. Interpreted per provider: a GitHub runner label and Azure vmImage (default ubuntu-latest), or a GitLab Docker image (runner default when omitted). Ignored when a matrix defines os on GitHub. |
needs? | needs?: string[] | Other jobs (by id) that must finish before this one. |
matrix? | matrix?: Record<string, Array<string | number>> | A build matrix: each key fans out over its values. |
failFast? | failFast?: boolean | Let the other matrix legs finish when one fails (fail-fast: false). Default GitHub behaviour cancels them, which hides whether a failure is platform-specific — the thing a cross-OS matrix exists to answer. |
permissions? | permissions?: Record<string, string> | The token permissions this job's GITHUB_TOKEN carries. Set it per job rather than pipeline-wide so a job holds only what it needs — the isolation that lets one job push commits while another only reads. GitHub only. |
harden? | harden?: CiHardenRunner | false | Harden the runner before this job's steps. Overrides CiPipeline.harden; pass false to opt this job out of a pipeline-wide default. |
checkout? | checkout?: CiCheckout | false | Check the repository out before this job's steps. Overrides CiPipeline.checkout; pass false to opt out. |
bootstrap? | bootstrap?: CiBootstrap | false | The prelude action for this job. Overrides CiPipeline.bootstrap; pass false to render hardening and checkout as separate steps instead. |
env? | env?: Record<string, string> | Environment variables for the job. |
if? | if?: string | A condition gating the job. A raw provider expression: GitHub if:, Azure condition:. Ignored on GitLab. Use it to e.g. skip forked pull requests. |
timeoutMinutes? | timeoutMinutes?: number | Fail the job if it runs longer than this many minutes. |
concurrency? | concurrency?: CiConcurrency | A concurrency group for this job alone (GitHub only), as opposed to the pipeline-level CiPipeline.concurrency. A job the if: skips never enters its group, whereas a run whose every job is skipped still enters the pipeline's — and, with cancelInProgress, cancels the run it shares the group with. Use it when a trigger can fire runs that do no work. |
steps? | steps?: CiStep[] | The steps to run, in order. Defaults to a single step that runs the build. |
type CiPinResolver = unknown Resolves an action's pinned reference by name, e.g. "actions/checkout".
Supplying one is what lets a workflow declare hardening and checkout by *intent* rather than by repeating a SHA at every use. Without it each CiHardenRunner and CiCheckout must carry its own action.
interface CiPipeline A complete, provider-agnostic CI pipeline.
| Member | Signature | Doc |
|---|---|---|
name? | name?: string | The pipeline name. Defaults to "CI". |
triggers? | triggers?: CiTriggers | When it runs. Defaults to push and pull request on main; pass an empty object ({}) for a pipeline triggered only by external means. |
permissions? | permissions?: Record<string, string> | Workflow-level token permissions (GitHub only), e.g. { contents: "read", "pull-requests": "write" }. Ignored elsewhere. Defaults to { contents: "read" } — least privilege, and what a workflow that only reads the repository needs. A job that needs more declares it, so the wider scope sits next to the job that justifies it. Pass {} for no permissions at all, which is stricter than the default rather than absent. |
concurrency? | concurrency?: CiConcurrency | Limit concurrent runs (GitHub only). Ignored elsewhere. |
harden? | harden?: CiHardenRunner | Harden every job's runner, unless a job overrides it or opts out with harden: false. Declared once here rather than repeated per job, since the policy is usually uniform across a workflow. GitHub only. |
checkout? | checkout?: CiCheckout | Check the repository out in every job, unless a job overrides it or opts out with checkout: false. GitHub only. |
bootstrap? | bootstrap?: CiBootstrap | false | The prelude action every job starts with, unless a job says otherwise with bootstrap: false. Defaults to the zuke-build/zuke action; see CiBootstrap. GitHub only. |
jobs? | jobs?: CiJob[] | The jobs to run. Defaults to a single build job that runs the build. |
type CiProvider = github | gitlab | azure | bitbucket The CI providers generateCi can target.
interface CiStep A single step in a job.
| Member | Signature | Doc |
|---|---|---|
name? | name?: string | Human-readable step name. |
id? | id?: string | A stable identifier for the step, so later steps can read its outputs (${{ steps.<id>.outputs.x }}). GitHub only. |
if? | if?: string | A condition gating this step — a raw provider expression, e.g. runner.os == 'Windows' or always(). GitHub only. |
shell? | shell?: string | The shell to run run with (bash, pwsh, sh, …). Omit for the runner's default, which differs per OS. GitHub only. |
continueOnError? | continueOnError?: boolean | Continue the job even when this step fails (continue-on-error). GitHub only. |
run? | run?: string | A shell command to run. Portable across all providers. |
uses? | uses?: CiUses | A GitHub Action reference (e.g. actions/checkout@v4). Rendered only for GitHub; skipped for GitLab and Azure. |
with? | with?: Record<string, string> | Inputs for a uses Action (GitHub only). |
env? | env?: Record<string, string> | Environment variables for this step. Rendered as env: on GitHub Actions and on Azure Pipelines script steps; ignored on GitLab (which sources variables from project settings, not the job YAML). |
interface CiSyncOptions Filesystem seams for syncCiFiles (overridable for tests).
| Member | Signature | Doc |
|---|---|---|
check? | check?: boolean | Verify instead of write: report an out-of-date file as stale rather than overwriting it. Intended for CI, where committed config must match the build. |
read? | read?: unknown | Read a file's contents, or null when it does not exist. |
write? | write?: unknown | Write a file, creating parent directories as needed. |
interface CiSyncResult The outcome of syncing one CiFile.
| Member | Signature | Doc |
|---|---|---|
path | path: string | The file's path. |
status | status: CiSyncStatus | Whether it was written, already current, or (in check mode) out of date. |
type CiSyncStatus = written | unchanged | stale What syncCiFiles did to a file.
interface CiTriggers When the pipeline runs.
| Member | Signature | Doc |
|---|---|---|
push? | push?: string[] | Branches whose pushes trigger the pipeline. An empty array means every branch (no filter); omit the field to disable the push trigger. |
pullRequest? | pullRequest?: string[] | Branches whose pull/merge requests trigger the pipeline. An empty array means every branch (no filter); omit the field to disable the trigger. |
pullRequestTypes? | pullRequestTypes?: string[] | Which pull-request activity types fire the pipeline, on top of the branch filter — GitHub's default is opened, synchronize, reopened. Add edited when a gate reads the pull request's own description, since editing it changes what a check should see without pushing a commit. GitHub only. |
manual? | manual?: boolean | Allow manual runs (workflow dispatch / web). |
issueComment? | issueComment?: string[] | Run when a comment is created, edited, or deleted on an issue or a pull request (issue_comment), filtered to these activity types — an empty array means every type. A comment on a pull request arrives as an issue comment too, which is what lets a maintainer's comment start a job; the job runs on the default branch, so its if: must decide who may start it. GitHub only. |
branchProtectionRule? | branchProtectionRule?: boolean | Run when a branch protection rule is created, edited, or deleted (branch_protection_rule) — a supply-chain scan wants to re-score when the repository's own protections change. GitHub only. |
schedule? | schedule?: ScheduleEntry[] | Timezone-aware scheduled runs. Each entry is a 5-field cron in an optional IANA timezone ({ cron: "30 9 * * 1-5", tz: "Europe/Sofia" }). Fully supported on GitHub (compiled to UTC crons, with a generated guard step for daylight-saving zones) and Azure (native schedules:, UTC/fixed offset only); ignored on GitLab and Bitbucket, whose schedules are configured in the provider UI, not in-file. See "./ci_schedule.ts". |
type CiUses = string | CiActionRef A step's uses: value — a bare reference, or one carrying its version.
interface CliCommandInfo A reserved command (graph, generate-ci, completions).
| Member | Signature | Doc |
|---|---|---|
name | name: string | The command word. |
description | description: string | One-line summary. |
interface CliDescription A build's full CLI surface, suitable for JSON serialization.
| Member | Signature | Doc |
|---|---|---|
commands | commands: CliCommandInfo[] | The reserved positional commands. |
flags | flags: CliFlagInfo[] | The built-in option flags. |
targets | targets: CliTargetInfo[] | The build's targets, in declaration order. |
parameters | parameters: CliParameterInfo[] | The build's declared parameters, in declaration order. |
interface CliFlagInfo A built-in option flag.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The flag, with leading dashes. |
description | description: string | One-line summary. |
interface CliParameterInfo A parameter declared on the build.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The parameter's property name — the key an MCP tool call and execute's params map use (e.g. skipE2e). Distinct from flag, which is its kebab-case form. |
flag | flag: string | The CLI flag (without leading dashes), e.g. skip-e2e. |
description | description: string | The parameter's description, or "" when none was set. |
required | required: boolean | Whether a value is required. |
kind | kind: string | number | boolean | The parameter's value kind. |
boolean | boolean: boolean | Whether the flag is a value-less boolean. |
array | array: boolean | Whether repeated flags accumulate into a list. |
options | options: string[] | The allowed values, when the parameter is constrained to a set. |
default? | default?: string | The declared default rendered as a string, when the parameter has one. |
const cliReporter: Reporter The sink every command-surface module writes through, neutralised for a GitHub Actions runner.
These commands echo arguments the invoker chose — a run id, a target name, a force reason — and read values another process wrote into the shared state store. Under Actions those commonly come from a workflow-dispatch input or an issue body rather than from a person at a terminal, and a thrown message quotes them straight back.
A sink rather than a guard at each of the sixty-odd writes, because none of them should be exempt: unlike the run's reporter, none of these modules emits a workflow command of its own, so there is no half to carve out. The MCP command writes only diagnostics here — its protocol never goes through the console — so escaping cannot corrupt it.
The decision is made per line, not once when this module loads. A top-level constant would read the environment at import time, before an embedder or a test has set it, and the escaping would then silently not apply.
Public so a companion command surface — @zuke/cli is one — can write through the same sink instead of composing a copy of it. It is the sink for a command's own diagnostics and output, not the reporter handed to execute: the run's renderer writes ::group:: and ::endgroup:: of its own and wraps only third-party text in escapingReporter beside them, and a sink that neutralises every leading :: would break that grouping.
interface CliTargetInfo A target declared on the build.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The target's name (its field name on the build). |
description | description: string | The target's description, or "" when none was set. |
dependsOn | dependsOn: string[] | The names of its direct dependencies, in declaration order. |
default | default: boolean | Whether this is the conventional default target. |
unlisted | unlisted: boolean | Whether the target is hidden from --list (still runnable by name). |
class Command A lazily-executed command. Built by the $ tagged template. The process does not start until the command is awaited or a terminal method (text, lines, code) is called; the result is memoised so repeated reads are cheap.
| Member | Signature | Doc |
|---|---|---|
env | env(record: Record<string, string>): this | Merge additional environment variables. |
cwd | cwd(path: PathLike): this | Set the working directory for the process. |
noThrow | noThrow(): this | Do not throw on a non-zero exit; combine with code. |
quiet | quiet(): this | Suppress live stdout/stderr streaming to the terminal. |
killAfter | killAfter(ms: number): this | Kill the process if it runs longer than ms milliseconds, raising a CommandTimeoutError. Fires even under noThrow. |
stdin | stdin(text: string): this | Write text to the child's standard input, then close it. The reason this exists is credentials. A tool that takes a password or token as an argument puts it in the process table, where any local user reading /proc or running ps can see it, and in whatever transcript the command line is echoed into. Tools that care offer a stdin form instead — docker login --password-stdin is the canonical one — and without this there was no way to use it: the secret had nowhere to go but argv. The text never reaches Command.commandLine, because it is not part of the command line. That is the point, not a side effect. Only affects the run paths. A Command.spawned long-running process keeps its inherited stdin, since a service that reads from the terminal is a different thing from a one-shot that takes a secret. |
maxCapturedBytes | maxCapturedBytes(bytes: number): this | Cap how much of each captured stream is kept in memory, in bytes (default 8 MiB). Capture keeps the newest bytes: once the cap is reached the oldest are dropped, CommandOutput.truncated is set, and CommandOutput.text prefixes a notice. Raise it for a command whose whole output you must parse; lower it to bound a chatty one. Live streaming to the terminal is never capped — every byte still reaches it. |
signal | signal(signal: AbortSignal): this | Terminate the process (via SIGTERM) when signal aborts — for example when the enclosing run is cancelled. Overrides the executor's ambient run signal for this command. Composes with killAfter: either the timeout or the abort kills the process, whichever fires first. |
commandLine | commandLine(): string | The command line, for diagnostics — argv joined by spaces, with the resolved value of every secret parameter of the enclosing run masked. This is the only rendered form of the command (the echo under --dry-run, a CommandError message), so a secret passed as an argv token cannot leak through one. The argv given to the operating system is unchanged. |
then | then(onfulfilled?: unknown | null, onrejected?: unknown | null): PromiseLike<TResult1 | TResult2> | Await support: run the command and resolve to a CommandOutput. |
text | text(): Promise<string> | Run and resolve to trimmed stdout — prefixed with a truncation notice if the capture cap was hit. Throws on non-zero unless noThrow. |
lines | lines(): Promise<string[]> | Run and resolve to stdout split into lines (trailing blank dropped). |
code | code(): Promise<number> | Run and resolve to the numeric exit code. Never throws on non-zero. |
spawn | spawn(): SpawnedProcess | Start the command as a long-lived process without waiting for it to exit, returning a SpawnedProcess handle. Use this for a service — a dev server, a database, docker compose up — that must keep running while other targets execute; stop it with SpawnedProcess.stop. stdout/stderr are inherited so the process's output is visible. |
class CommandError extends Error Raised when a command exits non-zero and throwing was not suppressed.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The error name. |
class CommandOutput The resolved result of a command, available when awaiting a Command.
| Member | Signature | Doc |
|---|---|---|
text | text(): string | Trimmed stdout, prefixed with a one-line notice when truncated — so a caller reading the output cannot mistake a tail for the whole of it. |
class CommandTimeoutError extends Error Raised when a command is killed for exceeding its Command.killAfter budget. Thrown regardless of Command.noThrow, since a timeout is a distinct, exceptional outcome from a normal non-zero exit.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The error name. |
interface CompensationFailure A compensation that threw during the cancel walk (recorded, non-fatal).
| Member | Signature | Doc |
|---|---|---|
target | target: string | The compensation target that failed. |
forTarget | forTarget: string | The original target whose compensation this was. |
error | error: string | The failure message. |
type Condition = unknown A predicate gating whether a target runs; may be synchronous or async.
Receiving the context is optional — a zero-argument .onlyWhen(() => …) stays valid, since a zero-argument function is assignable to this one-parameter type.
interface ConditionContext The context a condition receives.
Deliberately narrower than a TargetContext. A condition on a .whenSkipped("skip-dependencies") target is evaluated before the run starts, to decide what the run prunes — at which point the run's identity, its durable state handles, and its cancellation signal do not exist yet. This type carries only what is available at every moment a condition can be called.
| Member | Signature | Doc |
|---|---|---|
plan | plan(): RunPlan | The resolved shape of this run — which targets it plans, and how they relate. Lets a condition gate on the graph ("only when deploy was asked for") rather than only on the environment. Reports the plan, not the outcome, which matters most here: a condition can be one of the things *deciding* what runs, so the plan deliberately does not claim to know what will execute. See "./run_plan.ts".RunPlan. |
target | target: string | Dotted name of the target this condition gates. |
const CONFIG_FILE: zuke.json The Zuke config file name; its presence marks a repository root.
type Configure = unknown A lambda that configures a settings instance and returns it.
interface ConformanceCliDeps Injectable dependencies for runConformanceCli (tests override them).
| Member | Signature | Doc |
|---|---|---|
makeStateStore? | makeStateStore?: unknown | Build the StateStore for a url/token (default HttpStateStore). |
makeBuildRegistry? | makeBuildRegistry?: unknown | Build the BuildRegistry for a url/token (default HttpBuildRegistry). |
log? | log?: unknown | Emit a line of output (default console.log). |
readEnv? | readEnv?: unknown | Read an environment variable (default the process environment). |
interface ConformanceOptions Tuning options for the conformance scenarios.
| Member | Signature | Doc |
|---|---|---|
lockTtlMs? | lockTtlMs?: number | The lock TTL (ms) the takeover scenario acquires with; it then waits a bit longer than this for the lock to expire. Raise it for a slow backend. Default 200. |
interface ConformanceResult The outcome of one conformance scenario.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The scenario's name. |
ok | ok: boolean | Whether the backend satisfied it. |
error? | error?: string | The failure detail when ok is false. |
interface CopyOptions Options for FileTasksApi.copy.
| Member | Signature | Doc |
|---|---|---|
overwrite? | overwrite?: boolean | Overwrite an existing destination file (default true). |
interface CreateDirectoryOptions Options for FileTasksApi.createDirectory.
| Member | Signature | Doc |
|---|---|---|
recursive? | recursive?: boolean | Create parent directories as needed (default true). |
async function createTarGzip(files: PathLike[], dest: PathLike, options?: unknown): Promise<void> Read files (relative to cwd), pack them into a tar archive named by their path relative to cwd, gzip it, and write the result to dest.
interface DeclaredEffect One effect declared on a target: its name and its body.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The effect's name, unique within the target and stable across runs. |
fn | fn: EffectFn | The body run once the intent is durably recorded. |
const DEFAULT_POLL_INTERVAL_MS: 200 How often ServiceBuilder.readyWhen is polled while waiting.
const DEFAULT_READY_TIMEOUT_MS: 30000 The default time a service is given to become ready before it fails.
const DEFAULT_TOOLS_DIR: .zuke/tools The default directory a Toolchain (and ToolTasks) installs into.
function defaultMcpAuthorize(identity: McpIdentity, call: McpCall): McpAuthorization The shipped default policy.
| Call | Needs | | --- | --- | | list_* / show_* / describe_* | read | | run:<target> | run, and the target's requiresRole when it declares one | | a run-scoped mutation | the run's initiator, or operator | | a sweep over every run | operator |
A run whose initiator is a service may be steered by any caller holding run: a scheduler's run has no person to ask, and treating the scheduler as its owner would mean nobody could intervene. Documented, and overridable by Build.mcpAuthorize.
const defaultRenderer: Renderer The built-in renderer: Zuke's ruled headers and summary table.
const defaultStateHost: StateHost The real, Deno-backed StateHost.
function defineTool(tool: string, options?: DefineToolOptions): ToolTask Define a fluent task for a CLI that has no dedicated @zuke wrapper. Returns a task that runs the tool, configured through a DynamicToolSettings lambda — the same settings-lambda style as the built-in wrappers, with arg/flag/option for argv and the shared cwd/env/noThrow/… chainers.
import { defineTool } from "jsr:@zuke/core/tooling";
const terraform = defineTool("terraform");
await terraform((s) => s.arg("plan").option("out", "plan.tfplan"));
// → terraform plan --out plan.tfplan
const helmUpgrade = defineTool("helm", { subcommand: "upgrade" });
await helmUpgrade((s) => s.arg("api", "./chart").flag("install"));
// → helm upgrade api ./chart --installinterface DefineToolOptions Options for defineTool.
| Member | Signature | Doc |
|---|---|---|
subcommand? | subcommand?: string | string[] | Leading subcommand token(s) prepended to every invocation. |
function denoExecutable(standalone?: boolean): string The executable that runs Deno here — what to spawn when a build needs deno itself, rather than a tool it installed.
Under deno run the executable running this code is Deno, so Deno.execPath() names it and nothing has to be looked up: a project whose Deno came from the ./zuke launcher rather than from PATH still works, and the subprocess is the same version as the parent.
A build compiled with deno compile is not Deno. There Deno.execPath() is the compiled binary, so spawning it re-enters the build instead of running Deno — deno test becomes the build running itself with test as a target, and deno doc <spec> becomes the build being asked for a target named doc. The compiled case therefore resolves the bare name and lets the OS find a real Deno on PATH. Deno.build.standalone is what tells the two apart.
import { denoExecutable } from "jsr:@zuke/core";
const deno = new Deno.Command(denoExecutable(), { args: ["doc", "jsr:@zuke/core"] });
@zuke/cli answers the same question with one more step — a compiled global zuke falls back to the launchers' bootstrap directory when PATH has no Deno — because it is the command a user installs on a machine that may have no Deno at all. That fallback is the CLI's, and this is the decision the two share.
function describeCli(build: Build, options?: DescribeCliOptions): CliDescription Describe a build's full CLI surface — reserved commands, option flags, targets (with descriptions and dependencies), and declared parameters — as a plain object ready for JSON. This is the same data zuke --list --json prints, made available to tooling and agents that introspect a build in code.
import { describeCli } from "jsr:@zuke/core";
const surface = describeCli(new MyBuild());
console.log(surface.targets.map((t) => t.name));
Pass { omitSecrets: true } to drop .secret() parameters from the result — the posture the build registry uses, so a secret never becomes a spawnable MCP input or crosses the run boundary (zuke register writes this form).
interface DescribeCliOptions Options for describeCli.
| Member | Signature | Doc |
|---|---|---|
omitSecrets? | omitSecrets?: boolean | Drop .secret() parameters from the surface (used for registry descriptors). |
function detectCiHost(env?: unknown): CiHost Detect the CI host from the environment. Recognises GitHub Actions (GITHUB_ACTIONS), GitLab CI (GITLAB_CI), Azure Pipelines (TF_BUILD), and Bitbucket Pipelines (BITBUCKET_BUILD_NUMBER); anything else is "local". The reader is injectable so detection can be unit-tested hermetically.
function detectWidth(): number Read the terminal width if available, clamped to a sane range.
class DiscordAnnouncementSettings extends AnnouncementSettings Fluent settings for AnnounceTasksApi.discord. Bot mode (.bot().token(t).channel(c)) posts through the REST API with a bot token.
function discoverCiFiles(build: Build): CiFile[] Find every CiFile declared on a build instance. A fan-out file is resolved here — its jobs are expanded from the build's targets — so the returned files render the same whether they fan out or not.
function discoverGroups(build: Build): Map<string, Group> Discover all parallel Group batches declared on a build instance, binding each its property path (for labelling, e.g. in the graph). Groups that are not assigned to a build property simply stay unnamed.
function discoverParameters(build: object): Map<string, AnyParameter> Discover all parameters declared on a build instance: scan its fields (recursing into plain-object component bundles) for Parameter values, bind each its dotted property path, and return a name → parameter map preserving declaration order.
function discoverTargets(build: Build): Map<string, TargetBuilder> Discover all targets declared on a build instance.
Scans the instance's fields (recursing into plain-object component bundles) for TargetBuilder values, assigns each its dotted property path, and returns a name → target map preserving declaration order.
type DownloadFn = unknown A download function: fetch url into the file at dest.
type DownloadFormat = raw | ArchiveFormat How a downloaded artifact is treated: "raw" is the binary itself, an ArchiveFormat is unpacked and one path taken from inside.
class DynamicToolSettings extends ToolSettings Fluent settings for a defineTool tool: build the argv with DynamicToolSettings.arg/DynamicToolSettings.flag/DynamicToolSettings.option (in call order), plus all the shared chainers (cwd, env, noThrow, quiet, toolPath, args).
| Member | Signature | Doc |
|---|---|---|
arg | arg(...values: Array<string | number>): this | Append raw positional/argument tokens. |
flag | flag(name: string): this | Append a boolean flag, e.g. flag("verbose") → --verbose (or -v). |
option | option(name: string, value: string | number): this | Append a flag and its value as two tokens, e.g. --output dist. |
interface EffectContext The context an effect body receives: the target's own context, plus which effect this is and whether it has been driven before.
| Member | Signature | Doc |
|---|---|---|
effect | effect: string | The effect's declared name. |
redriven | redriven: boolean | True when a previous attempt at this effect already committed its intent — so its side effect may have happened, wholly or partly, and this run is repeating it. Effects are at-least-once. A body that can tell the difference should say so in what it writes, rather than assume it is the first to get here. |
type EffectFn = unknown The body of a declared effect (see TargetBuilder.effect).
interface EffectState The durable intent-and-completion row for one of a target's effects.
There is no idempotency key here. An effect is identified by where it sits — the run, the target, and its declared name — which the record already spells out structurally, so a key would be a second spelling of the same fact and a place for a secret to end up.
| Member | Signature | Doc |
|---|---|---|
status | status: EffectStatus | How far the effect got. |
intentAt | intentAt: string | ISO-8601 time the intent was committed — always *before* the body ran. |
settledAt? | settledAt?: string | ISO-8601 time it settled, if it has. |
error? | error?: string | The failure message when status is failed. |
attempts | attempts: number | How many times the body has been driven. Above one means it was re-driven. |
type EffectStatus = pending | done | failed Where one declared effect has got to (see .effect(...)).
pending is the load-bearing one: it means the intent was committed and the body may or may not have run. A process that dies mid-effect leaves exactly that, which is what tells a later resume to drive it again.
function envBuildRegistry(readEnv: unknown, host: StateHost): BuildRegistry | undefined Resolve a BuildRegistry from the environment, or undefined when none is configured. ZUKE_REGISTRY_URL (with an optional ZUKE_REGISTRY_TOKEN) selects an HttpBuildRegistry; otherwise ZUKE_REGISTRY_DIR selects a FileSystemBuildRegistry.
function envCacheStore(readEnv: unknown): RemoteCacheStore | undefined Resolve a RemoteCacheStore from the environment, or undefined when none is configured. ZUKE_REMOTE_CACHE_URL (with an optional ZUKE_REMOTE_CACHE_TOKEN) selects an HttpCacheStore; otherwise ZUKE_REMOTE_CACHE_DIR selects a FileSystemCacheStore.
function envStateStore(readEnv: unknown, host: StateHost): StateStore | undefined Resolve a StateStore from the environment, or undefined when none is configured. ZUKE_STATE_URL (with an optional ZUKE_STATE_TOKEN) selects an HttpStateStore; otherwise ZUKE_STATE_DIR selects a FileSystemStateStore.
function envVarName(name: string): string The environment variable for a parameter: its path in SCREAMING_SNAKE_CASE.
function escapeData(value: string): string Escape a value interpolated into the body of a GitHub Actions workflow command (::error::<data>).
A workflow command is terminated by the end of its line, so a value carrying a newline continues into what the runner parses as a *fresh* command. A target's failure message embeds a subprocess's stderr verbatim, which is not ours to trust: a tool that writes ::stop-commands:: on a line of its own would otherwise suspend the runner's command processing, and one that writes ::error:: would forge an annotation. Percent-encoding is the escape the Actions spec defines for exactly this, and % is encoded first so the encoding cannot be spoofed by a literal %0A in the input.
function escapeLine(text: string): string Neutralise workflow commands in text that is *printed as itself* on a stream the GitHub Actions runner parses — a failure message, a target name, a summary row — rather than interpolated into a command's body.
escapeData is the wrong tool there. It answers the same threat, but by encoding every newline, which would fold a multi-line compiler dump into one unreadable %0A-joined line: correct, and useless to the person reading the log. This keeps the text as it was written and disarms only the two sequences the runner acts on.
Both forms are covered, because the runner accepts both. A line whose first non-blank characters are :: opens a command, and leading whitespace is trimmed before that test, so indenting the text defends nothing. The legacy ##[command] form is recognised *anywhere* in a line, so it needs no newline to reach at all.
"Blank" is the runner's idea of it, not this language's. The two sets differ by exactly one character in the direction that matters: NEXT LINE (U+0085), which the runner trims and \s does not match. It is not a line terminator for the reader the runner uses, so it travels inside a line and disappears only when the command is parsed, which would let :: reach the front of a line that looked indented here. It is matched explicitly for that reason.
Ordinary output is returned unchanged; only text that would have been executed as a command comes back visibly encoded.
function escapeLineIf(github: boolean, text: string): string escapeLine, applied only when something is parsing this process's output for workflow commands.
The condition is not a security gate — the runner parses every line a step writes, whatever this process believes about its own style. It is a readability one: escapeLine leaves ordinary text alone, but a compiler dump that legitimately begins a line with :: would come back encoded, and on a developer's terminal that is noise protecting against nothing.
It exists so the decision has one implementation. It was written out at more than a dozen call sites, in three shapes that had already drifted apart — two asking the style, one asking the environment — which is how a site gets added without it.
function escapeProperty(value: string): string Escape a value interpolated into a workflow command's property list (::error title=<property>::). Properties are comma-separated and colon-terminated, so those two characters need encoding on top of what escapeData handles.
function execSecret(configure: Configure<ExecSecretSettings>): SecretSource A SecretSource that runs a command and takes its standard output as the secret value. Configure it through an ExecSecretSettings lambda.
parameter("Vault token").secret().from(
execSecret((s) => s.command("vault").arg("kv", "get", "-field=token", "secret/ci")),
);class ExecSecretSettings Fluent settings for execSecret: a command whose standard output is the secret. Configure the binary with ExecSecretSettings.command, arguments with ExecSecretSettings.arg, and optionally the environment and working directory. Output is trimmed of surrounding whitespace unless ExecSecretSettings.trim is turned off (some values are whitespace-sensitive).
| Member | Signature | Doc |
|---|---|---|
command | command(binary: PathLike): this | The binary to run (e.g. op, vault, gcloud). Required. |
arg | arg(...values: Array<string | number | AbsolutePath>): this | Append one or more arguments to the command. |
env | env(record: Record<string, string>): this | Merge additional environment variables for the process. |
cwd | cwd(path: PathLike): this | Set the working directory for the process. |
trim | trim(on?: boolean): this | Whether to trim surrounding whitespace from stdout (default true). |
resolve_ | resolve_(): Promise<string> | Run the command and return its captured stdout as the secret. Streaming is suppressed (quiet) so the value is never echoed to the terminal, and a non-zero exit throws a SecretError naming the command. |
async function execute(build: Build, root: TargetBuilder, options?: ExecuteOptions): Promise<BuildResult> Execute the requested target and its transitive dependencies.
Runs the build's onStart/onFinish lifecycle hooks around the plan. By default targets run sequentially in deterministic order; with parallel, independent targets run concurrently while dependencies still complete first. Stops launching after the first failure, marks unreached targets as skipped, and returns a failing result.
interface ExecuteOptions Options for execute.
| Member | Signature | Doc |
|---|---|---|
silent? | silent?: boolean | Suppress all banner/summary output (used by tests). |
reporter? | reporter?: Reporter | Custom reporter; overrides silent. |
plugins? | plugins?: Plugin[] | Lifecycle observers invoked alongside the build's own hooks, in order. Lets third-party packages report/time/notify without subclassing the build. |
skip? | skip?: string[] | Target names to skip even if they appear in the plan (CLI --skip). |
parallel? | parallel?: boolean | number | Run independent targets concurrently. false/omitted runs sequentially in deterministic order; true uses the host's CPU count; a number sets the maximum concurrency. Dependencies still complete before their dependents. |
cache? | cache?: boolean | BuildCache | Incremental caching: skip targets whose declared TargetBuilder.inputs are unchanged since the last successful run (and whose outputs still exist). Defaults to on; pass false to disable (CLI --no-cache). A BuildCache may be supplied directly (used in tests). |
remoteCache? | remoteCache?: RemoteCacheStore | false | A RemoteCacheStore that shares target TargetBuilder.outputs across machines: a local cache miss restores outputs from it, and a successful run uploads them. false disables it (CLI --no-remote-cache). When omitted, the build's remoteCache() override is used, falling back to the ZUKE_REMOTE_CACHE_* environment variables. Ignored when cache is a supplied BuildCache or is false. |
params? | params?: Record<string, string> | Raw parameter values from the command line, keyed by parameter (property) name. Each declared Parameter is resolved from this map, then the environment, then its declared default before any target runs. |
readEnv? | readEnv?: unknown | Reads an environment variable as a parameter fallback. Defaults to Deno.env.get (returning undefined when env access is unavailable); overridable so parameter resolution can be tested hermetically. |
prompt? | prompt?: unknown | Prompt for a missing required parameter, returning the entered value (or undefined to leave it unset). Defaults to an interactive terminal prompt when stdin is a TTY and the build is not on CI; overridable for testing. |
dryRun? | dryRun?: boolean | Plan only: resolve and print every target that *would* run (honouring --skip and onlyWhen conditions) without executing any body or touching the cache (CLI --dry-run). |
affected? | affected?: AffectedOptions | Restrict the run to the targets affected by files changed since a base git revision (CLI --affected[=<base>]). A target is affected when a changed file falls inside its declared TargetBuilder.inputs or a dependency is affected; a target that declares no inputs is always considered affected. Unaffected targets are skipped. The base revision defaults to HEAD; supply changedFiles to inject the diff (used in tests). |
github? | github?: boolean | Force GitHub Actions output formatting on or off. Auto-detected from the GITHUB_ACTIONS environment variable when omitted. |
color? | color?: boolean | Force ANSI colour on or off. Auto-detected (a TTY with NO_COLOR unset, outside GitHub Actions) when omitted; off by default with a custom reporter. |
renderer? | renderer?: Renderer | Renderer for the per-target banners and the end-of-build summary. Defaults to Zuke's built-in "./renderer.ts".defaultRenderer; @zuke/console exports an alternative a build can inject to restyle its output. |
signal? | signal?: AbortSignal | Cancel the run when this signal aborts (wired to Ctrl-C/SIGTERM by the CLI, or fired by another process running zuke cancel). Every target body's "./target.ts".TargetContext signal mirrors it, and it is applied as the shell's ambient default so an in-flight $ command is terminated (SIGTERM) on cancellation. When the run is cancelled, the compensations of every target that had succeeded run in reverse order (see "./target.ts".TargetBuilder.onCancel) and the result is a non-ok cancelled outcome. A body that ignores its signal still runs to completion, so promptly-cancellable work should pass ctx.signal to its shell commands. |
stateStore? | stateStore?: StateStore | false | Durable run state (see "./state/store.ts".StateStore). A supplied store is used directly; false disables state entirely. When omitted, the build's stateStore() override is used, falling back to ZUKE_STATE_URL / ZUKE_STATE_DIR, and finally — only when state is set — a filesystem store under <root>/.zuke/runs. |
state? | state?: boolean | Opt a plain build into durable state (CLI --state): fall back to a .zuke/runs filesystem store when nothing else is configured. Ignored when a store is resolved from stateStore, the build, or the environment. |
actor? | actor?: string | Who to attribute the run to in its state record (CLI --actor). Falls back to ZUKE_ACTOR, then the CI actor, then "anonymous". |
actorKind? | actorKind?: ActorKind | Whether a person or a machine asked for the run (CLI --actor-kind). Falls back to ZUKE_ACTOR_KIND, else "human". Recorded on the run's immutable initiator, never inferred from the actor's name. |
resume? | resume?: ResumeState | Continue a suspended run instead of starting a fresh one. Set by "./resume.ts".resumeRun after it has transitioned the run to running; carries the existing record, its store version, and the targets already succeeded (which are not re-run). Not for direct use — call resumeRun. |
function executionSet(root: TargetBuilder): Set<TargetBuilder> Compute the execution set for a requested target: the target plus the transitive closure of its hard dependencies.
function externalSignal(name: string): WaitTrigger A trigger satisfied when a signal named name has been delivered to the run (via zuke resume <id> --signal <name>). The signal's payload is exposed to target bodies through "./target.ts".TargetContext signals.
interface ExtractOptions Options common to extractTarGzip and extractZip.
| Member | Signature | Doc |
|---|---|---|
strip? | strip?: number | Drop this many leading path components from every entry (like tar's --strip-components). An entry left with no path — e.g. the archive's single top-level directory — is skipped. Defaults to 0. Use 1 to unpack a release tarball that wraps everything in a tool-v1.2.3/ directory. |
async function extractTarGzip(src: PathLike, destDir: PathLike, options?: ExtractOptions): Promise<void> Read the .tar.gz at src, gunzip and unpack it, and write each entry under destDir (creating parent directories as needed). Symlink entries are recreated as symlinks and directory entries as directories; pass ExtractOptions.strip to drop leading path components.
async function extractZip(src: PathLike, destDir: PathLike, options?: ExtractOptions): Promise<void> Read the .zip at src, unpack it, and write each entry under destDir (creating parent directories as needed) — the zip counterpart of extractTarGzip. Entry names are validated so a malicious archive cannot escape destDir.
function fail(message: string): never Throw an AssertionError with message. Never returns.
interface FanOutOptions Options for fanOutPipeline: how a build's targets become parallel CI jobs.
| Member | Signature | Doc |
|---|---|---|
command? | command?: unknown | The command a job runs for its target, given the target name. Defaults to the ./zuke <target> launcher (which bootstraps Deno). Each job runs only its own target; its dependencies run in their own jobs and are shared via the remote cache, so pair fan-out with one. |
setupSteps? | setupSteps?: CiStep[] | Steps prepended to every job — tool setup, cache restore. None by default: the prelude action every job starts with already hardens the runner and checks the repository out (and GitLab and Azure check out on their own), so a job's steps are just Run <target> unless you add to them. Provide env for ZUKE_REMOTE_CACHE_* here or via env. |
runsOn? | runsOn?: string | The runner for every job (see CiJob.runsOn). |
includeUnlisted? | includeUnlisted?: boolean | Include targets hidden from --list via .unlisted(). Defaults to false. |
env? | env?: Record<string, string> | Environment variables set on every job (e.g. the remote-cache config). |
function fanOutPipeline(targets: Map<string, TargetBuilder>, base?: CiPipeline, options?: FanOutOptions): CiPipeline Expand a build's target graph into a fanned-out pipeline: one CI job per runnable target, wired together with needs: edges that mirror the targets' dependsOn dependencies — so independent targets run in parallel and a target's job waits for its prerequisites. Each job runs just its own target; upstream outputs are shared through the remote cache, so configure one (e.g. ZUKE_REMOTE_CACHE_* on the jobs) to avoid rebuilding dependencies in every job.
base contributes the pipeline-level fields (name, triggers, permissions, concurrency); its jobs are ignored in favour of the generated ones. Targets with no body, and (unless FanOutOptions.includeUnlisted) unlisted targets, are omitted, and needs edges to omitted targets are dropped.
function fileSecret(configure: Configure<FileSecretSettings>): SecretSource A SecretSource that reads a file and takes its content as the secret value — for a mounted Kubernetes/Docker secret or a CI-provided file. Configure it through a FileSecretSettings lambda.
parameter("Registry password").secret().from(
fileSecret((s) => s.path("/run/secrets/registry_password")),
);class FileSecretSettings Fluent settings for fileSecret: read a secret from a file. Set the path with FileSecretSettings.path; the content is trimmed of surrounding whitespace unless FileSecretSettings.trim is turned off.
| Member | Signature | Doc |
|---|---|---|
path | path(path: PathLike): this | The file to read the secret from. Required. |
trim | trim(on?: boolean): this | Whether to trim surrounding whitespace from the content (default true). |
resolve_ | resolve_(): Promise<string> | Read the file and return its content as the secret. A missing or unreadable file throws a SecretError naming the path. |
class FileSystemBuildRegistry A BuildRegistry that writes one <id>.json file per build under a directory.
Security. dir is *trusted configuration* — the location you choose to store the build catalog (from ZUKE_REGISTRY_DIR or an explicit registry), the same posture as "../state/fs_store.ts".FileSystemStateStore. The only untrusted value that reaches a path is the build id, validated at every point a path is built, so a traversal cannot be smuggled in via an id.
| Member | Signature | Doc |
|---|---|---|
getBuild | getBuild(id: string): Promise<unknown | null> | Fetch a build and the content-hash version of its stored file. |
register | register(descriptor: BuildDescriptor, expectedVersion: string | null): Promise<PutBuildResult> | Publish descriptor under an exclusive lock, guarding the expected version. |
deregister | deregister(id: string): Promise<void> | Remove a registered build under an exclusive lock; a missing file is a no-op. |
listBuilds | listBuilds(query: BuildQuery): Promise<BuildSummary[]> | List builds matching query, newest first. Unreadable files are skipped. |
class FileSystemCacheStore A RemoteCacheStore backed by a shared or mounted directory.
| Member | Signature | Doc |
|---|---|---|
get | get(key: string): Promise<Uint8Array | null> | Fetch the archived outputs stored under key, or null if there are none. |
put | put(key: string, artifact: Uint8Array): Promise<void> | Store artifact (a gzipped tar of a target's outputs) under key. |
class FileSystemStateStore A StateStore that writes one <id>.json file per run under a directory.
Security. dir is *trusted configuration* — the location you choose to store run state (from ZUKE_STATE_DIR, --state, or an explicit store), the same posture as "../remote_cache.ts".FileSystemCacheStore. The only untrusted value that reaches a path is the run id, which is validated at every point a path is built, so a traversal cannot be smuggled in through an id.
| Member | Signature | Doc |
|---|---|---|
getRun | getRun(id: string): Promise<unknown | null> | Fetch a run and the content-hash version of its stored file. |
putRun | putRun(record: RunRecord, expectedVersion: string | null): Promise<PutResult> | Publish record under an exclusive lock, guarding the expected version. |
listRuns | listRuns(query: RunQuery): Promise<RunSummary[]> | List runs matching query, newest first. Unreadable files are skipped. |
deleteRun | deleteRun(id: string): Promise<void> | Delete a run's file (under its lock); a missing run is a no-op. The run's lock records are deliberately left alone. It is tempting to take them with the run — they are named after it, so once it is gone nothing can look them up again — but "expired" does not mean "abandoned" in this store: renewLock extends a lock whenever the token matches, whatever its expiry, so a lapsed claim is still the holder's until somebody *acquires* it. Deleting the record instead makes the next renewal answer false, which the holder reads as the lease being lost, and a run that is merely slow — the exact case the lease exists to tell apart from a dead one — stops. Pruning must never be able to do that. The litter is small and bounded in practice: releaseLock removes a lock's file, and a run releases its lease whenever it settles, so only a holder that dies without releasing leaves one behind. Clearing those safely belongs to whoever can prove the holder is gone — a reaping sweep, which proves it by acquiring — not to a command deleting old records. |
acquireLock | acquireLock(key: string, holder: LockHolder, ttlMs: number): Promise<LockResult> | Atomically acquire the lock key for holder, taking over if expired. |
renewLock | renewLock(key: string, token: string, ttlMs: number): Promise<boolean> | Extend the lock key held under token; false if the token lost it. |
releaseLock | releaseLock(key: string, token: string): Promise<void> | Release the lock key if still held under token; a no-op otherwise. |
listLocks | listLocks(): Promise<HeldLockEntry[]> | Every live lock in the locks/ directory, ordered by key. <key>.acq mutex markers and anything else in there are skipped; a record that fails to parse is skipped too, since one corrupt file must not hide every other lock from someone trying to see who holds what. |
const FileTasks: FileTasksApi Filesystem task functions for build scripts.
interface FileTasksApi The shape of FileTasks.
| Member | Signature | Doc |
|---|---|---|
exists | exists(path: PathLike): Promise<boolean> | Whether path exists. |
homeDirectory | homeDirectory(): string | The current user's home directory, read from $HOME (falling back to $USERPROFILE on Windows). Throws a clear error when neither is set or environment access is unavailable, so callers get a path or a useful failure — never an undefined to thread through. |
createDirectory | createDirectory(path: PathLike, options?: CreateDirectoryOptions): Promise<void> | Create the directory at path. Creates parents by default (CreateDirectoryOptions.recursive); a recursive create is a no-op when the directory already exists. |
cleanDirectory | cleanDirectory(path: PathLike): Promise<void> | Remove everything inside the directory at path, leaving an empty directory. A no-op if path does not exist (it is *not* created). |
remove | remove(path: PathLike, options?: RemoveOptions): Promise<boolean> | Remove path, tolerating a missing target the way rm -f does: a NotFound resolves to false instead of throwing. Any other error (e.g. a non-empty directory removed without RemoveOptions.recursive) is rethrown. |
copy | copy(source: PathLike, destination: PathLike, options?: CopyOptions): Promise<void> | Copy a file or directory tree from source to destination (directories are copied recursively). |
move | move(source: PathLike, destination: PathLike): Promise<void> | Move (rename) source to destination. |
symlink | symlink(target: PathLike, path: PathLike, options?: SymlinkOptions): Promise<void> | Create a symbolic link at path pointing to target. target is stored in the link verbatim, so a relative one resolves against the link's own directory — which is what makes a link between two sibling checkouts survive both being moved together. With SymlinkOptions.force an entry already at path is replaced atomically, which is the ln -sfn case a re-run of an idempotent target needs; without it an existing entry is an AlreadyExists error. A directory at path is never replaced. |
readLink | readLink(path: PathLike): Promise<string> | The target of the symbolic link at path, exactly as stored in the link — relative if it was created relative, and not checked for existence. Throws if path is not a symbolic link, which is the same answer Deno.readLink gives. |
readText | readText(path: PathLike): Promise<string> | Read the UTF-8 text content of the file at path. |
writeText | writeText(path: PathLike, content: string): Promise<void> | Write content to the file at path, creating or truncating it. |
readJson | readJson(path: PathLike): Promise<T> | Read and parse the JSON file at path. |
function findCycle(targets: Map<string, TargetBuilder>): string[] | null Detect a cycle in the hard-dependency (dependsOn) graph across all targets.
type ForceDenial = unknown_run | run_terminal | unknown_target | already_settled | unforceable | has_effects | foreign_run | write_failed Why a force was refused. Each maps to a message naming the target, so an operator learns which rule stopped them rather than that "it failed".
type ForcedOutcome = skipped | succeeded What an operator may force a target to, without running it.
interface ForceOptions Options for forceTarget.
| Member | Signature | Doc |
|---|---|---|
runId | runId: string | The run to act on. |
target | target: string | The dotted target name to force. |
outcome | outcome: ForcedOutcome | What the target should settle to without running. |
reason? | reason?: string | Why — recorded on the override and shown by zuke runs show. |
actor? | actor?: string | Who to attribute the decision to (--actor); resolved as for a run. |
stateStore? | stateStore?: StateStore | false | The durable store the run lives in; resolved as for a run when absent. |
readEnv? | readEnv?: unknown | Reads an environment variable (injectable for tests). |
interface ForceResult The result of a forceTarget call.
| Member | Signature | Doc |
|---|---|---|
ok | ok: boolean | Whether the override was recorded. |
denial? | denial?: ForceDenial | Why it was refused, when it was. |
message | message: string | A message naming the target and the rule, suitable for an operator. |
override? | override?: TargetOverride | The override as recorded, when it was. |
async function forceTarget(build: Build, options: ForceOptions): Promise<ForceResult> Record an operator's forced outcome for one target of a run.
The write is a compare-and-swap, retried against a re-read record, so two operators forcing different targets at once cannot lose each other's decision. Every refusal is re-checked on each attempt, because the thing that beat us to the record may be the target settling.
type ForEachFactory = unknown Builds one item's ordered pipeline of sub-targets for TargetBuilder.forEach. The returned record's keys are stage names and its values are targets; each stage implicitly depends on the one declared before it, so an item's stages run in insertion order.
interface ForEachItem One materialised fan-out item: a unique label plus its pipeline stages.
| Member | Signature | Doc |
|---|---|---|
key | key: string | A label unique within the fan-out, used to name the item's sub-targets. |
stages | stages: Record<string, TargetBuilder> | The item's ordered pipeline stages, keyed by stage name. |
class ForEachSettings Fluent configuration for TargetBuilder.forEach, in the settings-lambda style: .forEach(items, factory, (s) => s.concurrency(3).continueOnItemFailure()). Sets the concurrency cap and whether one item's failure isolates it or stops the whole batch.
| Member | Signature | Doc |
|---|---|---|
concurrency | concurrency(limit: number): this | Cap how many item pipelines run concurrently (default: the host CPU count). Clamped to at least 1; 1 runs items one at a time. |
continueOnItemFailure | continueOnItemFailure(on?: boolean): this | Keep running the other items when one item's pipeline fails (the failed item's later stages are still skipped). The fan-out target still fails at the end if any item failed. Without this, the first item failure stops the batch — the default. |
concurrency_? | concurrency_?: number | Max item pipelines in flight at once; set by concurrency. |
continueOnItemFailure_ | continueOnItemFailure_: boolean | Isolate a failed item from its siblings; set by continueOnItemFailure. |
interface ForEachSpec The internal fan-out spec stored by TargetBuilder.forEach. Its ForEachSpec.materialize closure captures the item type, so the runtime list and factory are erased to concrete ForEachItems the executor can run without knowing the item type.
| Member | Signature | Doc |
|---|---|---|
materialize | materialize: unknown | Produce the per-item sub-target pipelines from the runtime list. |
configure? | configure?: Configure<ForEachSettings> | Optional fan-out settings (concurrency, per-item failure isolation). |
class ForeignRunError extends Error Thrown when a recovery path is handed a run that a different build owns: the run's recorded origin and this process's disagree.
A sweep treats it as "not mine" and moves on rather than counting a failure, the same way it treats a run another process has already resumed. A command that named one run reports it, because the operator asked about a run that is not this build's to touch.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The error name. |
function formatDuration(ms: number): string Format a duration in milliseconds as 1.2s.
function generateCi(pipeline: CiPipeline, provider: CiProvider): string Render pipeline as the YAML configuration for provider: .github/workflows/*.yml, .gitlab-ci.yml, azure-pipelines.yml, or bitbucket-pipelines.yml. The pipeline may be empty ({}) to accept every default.
async function gitChangedFiles(base?: string, run?: unknown): Promise<string[]> List the files changed since base (default HEAD) via git: tracked changes versus base plus untracked files not covered by .gitignore. run invokes git and returns stdout (defaults to a real git subprocess); override it to test without a repository.
async function glob(pattern: string, options?: GlobOptions): Promise<string[]> Expand a glob pattern to the matching paths, sorted for determinism. The walk starts at the pattern's static prefix, so anchor patterns (e.g. src/**\/*.ts) to avoid scanning the whole tree. Symlinked directories are not followed.
A relative pattern is resolved against cwd and its matches are returned relative to it. An absolute pattern (a leading /, or a C:-style drive) names its own root: cwd plays no part and the matches come back absolute.
interface GlobOptions Options for glob.
| Member | Signature | Doc |
|---|---|---|
cwd? | cwd?: string | Directory to resolve the pattern against (default: Deno.cwd()). Ignored for an absolute pattern, which names its own root. |
function globToRegExp(pattern: string): RegExp Compile a glob pattern into an anchored RegExp that matches a full path. Exposed (and pure) for testing and custom matching.
class GraphError extends Error Raised when the build graph is invalid (cycle or unknown dependency).
| Member | Signature | Doc |
|---|---|---|
name | name: string | The error name. |
function group(): Group Create a parallel Group. Targets join it with TargetBuilder.partOf, and a downstream target can depend on the whole batch by passing the group to TargetBuilder.dependsOn.
checks = group();
lint = target().partOf(this.checks).executes(...);
format = target().partOf(this.checks).executes(...);
deploy = target().dependsOn(this.checks).executes(...);class Group A parallel batch of targets, created with group. Targets join it via TargetBuilder.partOf; its members run concurrently with one another (each still awaiting its own dependencies) regardless of the global parallel setting. Passing a group to TargetBuilder.dependsOn depends on every member at once.
| Member | Signature | Doc |
|---|---|---|
members_ | members_: TargetBuilder[] | Members that declared themselves part of this group, in declaration order. |
name_? | name_?: string | Property name, assigned during discovery. Undefined until then. |
async function gunzip(data: Uint8Array): Promise<Uint8Array> Gunzip-decompress data using the platform DecompressionStream.
async function gzip(data: Uint8Array): Promise<Uint8Array> Gzip-compress data using the platform CompressionStream.
const HARDEN_RUNNER_ACTION: step-security/harden-runner The action a CiHardenRunner is generated from when pins are resolved.
interface HeldLease A held lease. Release it when the work it covers is over.
| Member | Signature | Doc |
|---|---|---|
release | release(): Promise<void> | Stop the heartbeat and release the claim (best-effort). |
lost | lost: AbortSignal | Aborts if the lease is lost — the store reports the claim is no longer this holder's, which means something else has taken the work over. A signal rather than a callback because a holder is not always ready to receive one at the moment it acquires: a resume takes the lease before the run it will drive exists. A signal can be read late and still be true. |
interface HeldLockEntry One live lock, as reported by "./store.ts".StateStore.listLocks — the key, who holds it, and when it lapses if the holder disappears.
Deliberately not the stored record: the acquisition token is the holder's proof of ownership, and a read-only listing has no business handing it out.
| Member | Signature | Doc |
|---|---|---|
key | key: string | The lock key, as it was acquired. |
holder | holder: LockHolder | Who holds it. |
expiresAt | expiresAt: number | Epoch-millisecond expiry: when it frees itself if the holder is gone. |
function hostPlatform(): Platform The current host's Platform (from Deno.build, with the OS normalised) — the analogue of "./host.ts".isCI for "what machine am I running on". Its os is a Zuke OperatingSystem (macos, not darwin); use the osLabel/archLabel helpers to name it for a download URL.
const p = hostPlatform();
p.os; // "linux" | "macos" | "windows"
const cpu = p.archLabel({ x86_64: "amd64", aarch64: "arm64" });class HttpBuildRegistry A BuildRegistry backed by HTTP.
Security. The url and token are *trusted configuration* — build descriptors (structural CLI metadata plus a launch location) are sent to that host, so point it only at a service you control and prefer a secret parameter or environment variable over a hard-coded value.
| Member | Signature | Doc |
|---|---|---|
getBuild | getBuild(id: string): Promise<unknown | null> | GET /builds/:id → descriptor + ETag; a 404 is a miss. |
register | register(descriptor: BuildDescriptor, expectedVersion: string | null): Promise<PutBuildResult> | PUT /builds/:id guarded by If-Match / If-None-Match; 412 → conflict. |
deregister | deregister(id: string): Promise<void> | DELETE /builds/:id; a missing build (404) is not an error. |
listBuilds | listBuilds(query: BuildQuery): Promise<BuildSummary[]> | GET /builds?name=&since= → an array of BuildSummary. |
interface HttpBuildRegistryOptions Configuration for an HttpBuildRegistry.
| Member | Signature | Doc |
|---|---|---|
url | url: string | The base URL build endpoints are built under (any trailing slash is ignored). |
token? | token?: string | A bearer token sent as Authorization: Bearer <token>, if set. |
fetch? | fetch?: fetch | The fetch implementation; defaults to the global. Overridable for tests. |
class HttpCacheStore A RemoteCacheStore backed by HTTP: GET <url>/<key> fetches an artifact (a 404 means a miss) and PUT <url>/<key> stores one. Works with any object store or cache server that speaks plain HTTP GET/PUT — an S3, GCS, or R2 bucket behind a URL, or a self-hosted cache endpoint.
Security. The url (and token) are *trusted configuration*: outputs are uploaded to that host and archives are extracted from it, so point it only at a cache you control, and prefer a secret parameter or an environment variable over a hard-coded value. On CI, restrict egress to the cache host so a misconfigured or overridden URL can't exfiltrate artifacts. A restored archive cannot name a path outside the workspace, cannot carry a link or directory entry, and is refused if the path it would land on passes through a symlink the workspace already holds — so a poisoned store cannot write outside the workspace (see restoreOutputs). An artifact larger than HttpCacheStoreOptions.maxArtifactBytes is refused before it is buffered, and restoreOutputs separately bounds what it decompresses to.
| Member | Signature | Doc |
|---|---|---|
get | get(key: string): Promise<Uint8Array | null> | Fetch the archived outputs stored under key, or null if there are none. |
put | put(key: string, artifact: Uint8Array): Promise<void> | Store artifact (a gzipped tar of a target's outputs) under key. |
interface HttpCacheStoreOptions Configuration for an HttpCacheStore.
| Member | Signature | Doc |
|---|---|---|
url | url: string | The base URL keys are appended to (any trailing slash is ignored). |
token? | token?: string | A bearer token sent as Authorization: Bearer <token>, if set. |
fetch? | fetch?: fetch | The fetch implementation; defaults to the global. Overridable for tests. |
maxArtifactBytes? | maxArtifactBytes?: number | The most a fetched artifact may weigh on the wire, in bytes, before it is refused. Defaults to 512 MiB. Raise it for a target whose compressed outputs are genuinely larger; a refusal is a warned rebuild, not a build failure. What the bytes *decompress* to is bounded separately, by restoreOutputs. |
async function httpDownload(url: string, dest: PathLike, options?: HttpOptions): Promise<void> Download url to dest, streaming the response body to the file. Creates or truncates dest. Throws HttpError on a non-2xx status.
class HttpError extends Error Raised when an HTTP request returns a non-2xx status. The URL appears in the message and on url, so it is passed through redactUrl first — userinfo and credential query params never reach a log.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The error name. |
status | status: number | The HTTP status code of the failing response. |
url | url: string | The requested URL, with any credentials redacted. |
async function httpJson(url: string, options?: HttpOptions): Promise<T> Fetch url and parse its body as JSON. Throws HttpError on non-2xx.
interface HttpOptions Options shared by the HTTP helpers.
| Member | Signature | Doc |
|---|---|---|
headers? | headers?: Record<string, string> | Extra request headers (e.g. an Authorization token). |
fetch? | fetch?: fetch | The fetch implementation to use. Defaults to the global fetch; override it to unit-test without network access. |
class HttpStateStore A StateStore backed by HTTP.
Security. The url and token are *trusted configuration* — run records (which include resolved non-secret parameters and target metadata) are sent to that host, so point it only at a service you control and prefer a secret parameter or environment variable over a hard-coded value.
| Member | Signature | Doc |
|---|---|---|
getRun | getRun(id: string): Promise<unknown | null> | GET /runs/:id → record + ETag; a 404 is a miss. |
putRun | putRun(record: RunRecord, expectedVersion: string | null): Promise<PutResult> | PUT /runs/:id guarded by If-Match / If-None-Match; 412 → conflict. |
listRuns | listRuns(query: RunQuery): Promise<RunSummary[]> | GET /runs?status=&target=&since= → an array of RunSummary. |
deleteRun | deleteRun(id: string): Promise<void> | DELETE /runs/:id; a missing run (404) is not an error. |
acquireLock | acquireLock(key: string, holder: LockHolder, ttlMs: number): Promise<LockResult> | POST /locks/:key → 201 { token }, or 409 with the current holder. |
renewLock | renewLock(key: string, token: string, ttlMs: number): Promise<boolean> | PUT /locks/:key renews; a 409/404 means the token lost the lock. |
listLocks | listLocks(): Promise<HeldLockEntry[]> | GET /locks → the live locks the server holds. A server that has not implemented the endpoint (404/501) is told apart from one that holds nothing: an empty listing is an answer, and a missing endpoint is not. |
releaseLock | releaseLock(key: string, token: string): Promise<void> | DELETE /locks/:key releases; a missing lock (404) is not an error. |
interface HttpStateStoreOptions Configuration for an HttpStateStore.
| Member | Signature | Doc |
|---|---|---|
url | url: string | The base URL run endpoints are built under (any trailing slash is ignored). |
token? | token?: string | A bearer token sent as Authorization: Bearer <token>, if set. |
fetch? | fetch?: fetch | The fetch implementation; defaults to the global. Overridable for tests. |
async function httpText(url: string, options?: HttpOptions): Promise<string> Fetch url and return its body as text. Throws HttpError on non-2xx.
function initiatorOf(run: RunRecord | RunSummary): string Who a run is attributed to for a reader that wants its *owner* rather than its last writer: the recorded initiator, else the actor.
The fallback is not a guess — on a record written before the initiator existed, and on any run that was never resumed, actor still holds exactly the value the initiator would have been stamped with.
async function installNpmTool(spec: NpmToolSpec, options?: InstallNpmToolOptions): Promise<AbsolutePath> Provision an npm-registry package as a version-pinned, cached tool and return the installed bin's AbsolutePath — hand it straight to a wrapper's .toolPath(...).
The package installs under <destDir>/npm/<name>@<version> via npm install --prefix <dir> --no-save <name>@<version>; a marker file records the pinned { name, version }, so a later run whose marker matches and whose bin is still present is reused without invoking npm again. npm must be on PATH (it resolves and downloads the package).
Throws — without recording a marker — if spec is malformed (an unsafe name, version, or bin), if npm fails, or if npm succeeds but the expected bin is absent (a typo'd bin, or a package that ships no executable), so a bad install fails loudly here instead of at a later .toolPath(...).
The marker is written only after the bin is verified present, so a matching marker always has its bin — a reader never sees a half-written install. Concurrent installs of the same pin into the same directory are not isolated; they just do redundant work (the documented ceiling — a build resolves its toolchain once, and distinct pins use distinct directories).
interface InstallNpmToolOptions Options for installNpmTool.
| Member | Signature | Doc |
|---|---|---|
destDir? | destDir?: PathLike | The root tools directory; the package installs under <destDir>/npm/<name>@<version>. Defaults to "./tool.ts".DEFAULT_TOOLS_DIR (.zuke/tools). |
run? | run?: NpmRunner | The npm-install runner. Defaults to the ambient npm; a test seam. |
os? | os?: OperatingSystem | The OS whose bin-shim filename to return (.cmd on Windows). Defaults to the host; a test seam for the Windows shim path. |
interface InstallPlatform The host identity: a Zuke OperatingSystem and Architecture.
| Member | Signature | Doc |
|---|---|---|
os | os: OperatingSystem | The operating system (normalised: macos, not darwin). |
arch | arch: Architecture | The CPU architecture. |
async function installRelease(options: InstallReleaseOptions): Promise<AbsolutePath> Download and install a release binary, returning its AbsolutePath. The path is ready to hand to a wrapper's .toolPath(...) (or CmdTasks).
With a InstallReleaseOptions.checksum, the download is verified before anything is installed, and a matching prior install is reused without downloading again — so pinning a checksum makes the install both hermetic (tamper-evident) and cached.
interface InstallReleaseOptions Options for installRelease.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The tool name; also the installed binary's filename (.exe on Windows). |
url | url: unknown | Resolve the download URL for the target Platform. |
destDir | destDir: PathLike | The directory to install the binary into (created if missing). |
archive? | archive?: DownloadFormat | unknown | The download format. "raw" (default) treats the download as the binary itself; "tar.gz" and "zip" unpack it and take binaryPath from inside. Many release assets ship one or the other. Like url and checksum this accepts a resolver, because the format is routinely per-platform: a Go or Rust project typically publishes .tar.gz for Linux and macOS and .zip for Windows. Pass (p) => p.os === "windows" ? "zip" : "tar.gz" rather than declaring one format that is wrong on a third of the platforms. |
binaryPath? | binaryPath?: string | unknown | For a "tar.gz" or "zip" archive, the binary's path within the archive. Defaults to name. Also resolver-friendly, for the same reason: the same release usually names the binary tool inside its Unix archive and tool.exe inside its Windows one, so (p) => p.os === "windows" ? "tool.exe" : "tool" is the common shape. (The *installed* filename gets its .exe automatically — this is the path to copy out of the archive.) |
platform? | platform?: InstallPlatform | The platform to resolve the URL for. Defaults to hostPlatform. Override it to install a foreign binary or to unit-test URL resolution. |
download? | download?: DownloadFn | The download implementation. Defaults to httpDownload; override it to unit-test without network access. |
checksum? | checksum?: string | unknown | The expected SHA-256 (hex) of the downloaded artifact — the .tar.gz for an archive, or the binary itself for a "raw" download; this is what release pages publish as the checksum. When set, the download is verified against it (a mismatch throws and nothing is installed) and the checksum doubles as a cache key: a prior install whose recorded checksum matches is reused without downloading again. Omit it and the tool is downloaded every time and not verified. Because url resolves a different artifact per platform, each has its own hash — so pass a resolver (platform) => string (like url) to pin a checksum per platform, or a plain string when a single artifact is installed. |
async function installTree(options: InstallTreeOptions): Promise<AbsolutePath> Download and unpack a whole archive tree — a multi-file runtime such as Node.js, which ships bin/node, bin/npm, bin/npx, and lib/node_modules/** in one tarball — and return the AbsolutePath of its (stripped) root. installRelease extracts a single binary; installTree keeps the entire directory, symlinks included.
Because AbsolutePath is callable, the root doubles as an accessor: root("bin", "node") is the node binary and root("bin") is the directory to put on PATH (with prependPath). Declared InstallTreeOptions.bins are marked executable on POSIX. With a InstallTreeOptions.checksum the archive is verified before unpacking and a matching prior install is reused.
interface InstallTreeOptions Options for installTree.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The tool name; the extracted tree lands in <destDir>/<name>. |
url | url: unknown | Resolve the download URL for the target Platform. |
destDir | destDir: PathLike | The directory the tree is installed under (created if missing). |
archive | archive: ArchiveFormat | unknown | The archive format — a multi-file runtime always ships packed. Accepts a per-platform resolver for the usual .tar.gz on Unix / .zip on Windows split (see InstallReleaseOptions.archive). |
strip? | strip?: number | Leading path components to drop while unpacking (tar's --strip-components). A release tarball wraps everything in a tool-v1.2.3/ directory, so 1 unwraps it; bins and the returned tree root are then relative to the stripped tree. Defaults to 0. |
bins? | bins?: unknown | Paths (relative to the stripped tree root) to mark executable on POSIX — the tar reader does not preserve mode bits, so a runtime's bin/node, bin/npm, … need it. Chmod follows a symlink to its real target, so listing a symlinked bin makes the script it points at executable too. |
platform? | platform?: InstallPlatform | The platform to resolve for. Defaults to hostPlatform. |
download? | download?: DownloadFn | The download implementation. Defaults to httpDownload; a test seam. |
checksum? | checksum?: string | unknown | The expected SHA-256 (hex) of the downloaded archive — verified before anything is unpacked, and used as the cache key (see InstallReleaseOptions.checksum). Omit it and the tree is downloaded every time and not verified. |
type Interpolatable = string | number | AbsolutePath | Array<string | number | AbsolutePath> A value that may be interpolated into a $ template.
const INVALID_TOKEN: McpAuthReject The refusal for a request that presented a token which did not hold up — expired, wrong signature, wrong audience.
Distinct from UNAUTHORIZED on purpose: OAuth 2.1 §5.3.1 says a challenge SHOULD NOT carry error information when the request had no credentials at all, because there is nothing yet to have been wrong. Sending invalid_token to a client that simply has not logged in tells it its stored token was rejected, which is a different and misleading thing.
function isCI(env?: unknown): boolean Whether the build appears to be running in a CI environment.
Broader than detectCiHost(env) !== "local", which answers only whether the host is one of the four Zuke names: a system it has no specific support for is still CI, and this says so.
function isStyleName(name: string): name is StyleName Whether a string names one of the SGR styles.
type JsonValue = string | number | boolean | null | JsonValue[] | unknown A JSON-serialisable value — the only thing that may be persisted in a target's TargetStateHandle, since run state is stored as JSON.
function line(style: Style, options?: LineOptions): string A horizontal rule spanning the style's width (dimmed by default).
interface LineOptions Options for line.
| Member | Signature | Doc |
|---|---|---|
char? | char?: string | The character to repeat. Defaults to ═. |
width? | width?: number | The rule width. Defaults to the style's width. |
style? | style?: unknown | Styles applied to the whole rule. Defaults to ["dim"]. |
function listStoreLocks(store: StateStore): Promise<HeldLockEntry[]> The locks store holds, or a friendly failure when the backend cannot enumerate them.
An empty array reads as "nobody holds anything", which is the worst possible answer to give someone looking at a wedged resource, so a store with no StateStore.listLocks says so rather than answering.
class LockConflictError extends Error Raised when a target's lock is already held by another run. Its message is the rendered guidance (from the target's onConflict, else a default), so it surfaces verbatim in the CLI failure footer and the run record; holder carries the structured identity for programmatic surfaces (e.g. MCP).
| Member | Signature | Doc |
|---|---|---|
name | name: string | The error name. |
interface LockHolder Who holds a lock — surfaced to the loser of a conflict so it can act.
| Member | Signature | Doc |
|---|---|---|
actor | actor: string | The actor that acquired the lock. |
runId | runId: string | The run that holds it (zuke cancel <runId> releases it). |
since | since: string | ISO-8601 timestamp when it was acquired. |
runUrl? | runUrl?: string | A link to the holding run (e.g. its CI job), when known. |
function lockKey(...parts: Array<string | number>): string Join parts into a lock key that is safe to use as a filename and URL segment. Each part is sanitised (non-[A-Za-z0-9._-] runs become _) and empty parts are dropped, so lockKey("deploy", repo) is stable and injection-free.
type LockResult = unknown | unknown The result of StateStore.acquireLock: a token proving ownership, or the current holder when the lock is already held.
class LockSettings Fluent configuration for TargetBuilder.lock, in the settings-lambda style: .lock((s) => s.lockKey("deploy", repo).withTtl("4h")). Set the key (composed from sanitised parts with LockSettings.lockKey, or directly with LockSettings.key), the TTL, and an optional LockSettings.onConflict message. The lambda runs after parameters resolve, so the key may read this.<param>.value.
| Member | Signature | Doc |
|---|---|---|
lockKey | lockKey(...parts: Array<string | number>): this | Set the lock key from parts, sanitised and joined via "./state/lock.ts".lockKey — e.g. s.lockKey("deploy", repo). |
key | key(key: string): this | Set the lock key directly (must be filename-safe; prefer lockKey). |
withTtl | withTtl(ttl: string | number): this | How long the lock survives a killed holder — a duration string like "4h" / "30m" (see the duration parser) or raw milliseconds. A live holder renews it while it runs, so it never expires under it. |
waitUpTo | waitUpTo(duration: string | number): this | Wait up to this long for a held lock instead of failing at once — a duration string like "30m" or raw milliseconds. The target queues, and takes the lock when the run holding it finishes; it fails with a "./state/lock.ts".LockConflictError only once the wait is spent. Set this for a shared resource a developer wants to *use* — one dev environment, one database, one port — where failing fast just makes them run the command again. Leave it off for a resource where a second run is a mistake worth reporting immediately, which stays the default. Waiting runs retry independently, so a queue of them is not served in arrival order: a run that has waited longer has no claim over one that arrived a moment ago. |
pollEvery | pollEvery(duration: string | number): this | How often to retry while waitUpTo waits (default "5s"). Alone it does nothing — without a wait there is no retry to pace. |
onConflict | onConflict(render: unknown): this | Render the guidance shown to a run that loses the lock. Receives the current "./state/lock.ts".LockHolder; the returned string becomes the failure message. Defaults to a generic "held by … then retry" line. |
key_? | key_?: string | The resolved lock key; set by key or lockKey. |
ttl_? | ttl_?: string | number | The TTL (a duration string or milliseconds); set by withTtl. |
onConflict_? | onConflict_?: unknown | The conflict-guidance renderer; set by onConflict. |
waitUpTo_? | waitUpTo_?: string | number | How long to wait for a held lock; set by waitUpTo. |
pollEvery_? | pollEvery_?: string | number | How often to retry while waiting; set by pollEvery. |
interface McpAuthenticator Authenticates one request for the MCP server.
Invoked once per message, before any dispatch: a rejection stops the request outright, so nothing executes and nothing is written to state. Returning an McpIdentity accepts the caller; returning an McpAuthReject refuses it. Throwing also refuses it — the seam is fail-closed, so a bug in an authenticator denies rather than admits.
Configure one with override mcpAuth() on the build.
| Member | Signature | Doc |
|---|---|---|
authenticate | authenticate(ctx: McpRequestContext): Promise<McpIdentity | McpAuthReject> | McpIdentity | McpAuthReject | Resolve the caller's identity from the request, or refuse the request. |
type McpAuthorization = unknown | unknown A policy's verdict on one call.
interface McpAuthReject Why a request was refused, and how the transport should say so.
The status and challenge are what make OAuth discovery work: an MCP client learns where to authenticate from a 401 carrying WWW-Authenticate, which a JSON-RPC error inside a 200 can never tell it.
| Member | Signature | Doc |
|---|---|---|
status | status: number | The HTTP status to answer with — a client error, 401 or 403 in practice. |
error | error: string | A short reason, machine-readable where there is a standard code for it (an OAuth authenticator's "invalid_token", say). It becomes the JSON-RPC error message on the refusal, so it is read by people too. |
detail? | detail?: string | A short human-readable detail. Never a secret: it is returned to the caller. |
challenge? | challenge?: string | The WWW-Authenticate header value to challenge with, when one applies. A value a header cannot carry (a newline, a NUL, a character outside Latin-1) is dropped rather than sent, so it cannot turn the refusal into a fault. |
interface McpCall What is being authorized — one MCP tool call, described for a policy.
| Member | Signature | Doc |
|---|---|---|
tool | tool: string | The tool name, e.g. run:deploy, cancel_run, list_runs. |
target? | target?: string | The target a run: call names, when it names one. |
requiredRoles? | requiredRoles?: unknown | Every role declared with .requiresRole(...) anywhere in the plan this call would execute — not just on the target it names. Invoking a target runs its dependencies, so a requirement that only guarded the entry point would be bypassed by invoking anything that depends on it. This mirrors --protect, which is enforced across the whole plan for the same reason. The caller must satisfy all of them. |
run? | run?: RunSummary | The run a run-scoped call acts on, when it acts on one. Absent for a sweep over every run, which no single run's owner can authorize. |
interface McpIdentity A trusted caller identity, resolved per request by an McpAuthenticator. Its McpIdentity.actor is the highest-precedence attribution — it overrides --actor, the environment, and the client's self-reported label for the call.
| Member | Signature | Doc |
|---|---|---|
actor | actor: string | The authenticated actor — an OAuth subject, a GitHub login, a service name. |
kind? | kind?: human | service | Whether a person or a machine is calling. Absent is read as "human", the conservative default: a policy that treats service callers differently must see the claim stated rather than inferred. |
roles? | roles?: unknown | The roles this caller holds. Omitting it means this authenticator does not speak roles: the [role policy](../../docs/mcp.md#authorization) then does not constrain the caller, and the server's allow-list, --protect globs and operator token remain the only gates — what a server did before roles existed. An empty list is the opposite claim: the question was considered and nothing was granted, so the policy denies. Role names are passed through as given; the comma-separated environment variable a registry-spawned child reads escapes them rather than this dropping them, so sanitisation cannot turn a granted set into an empty one. |
via? | via?: string | How the identity was established (e.g. "oauth-proxy"); informational. |
type McpIdentityHook = unknown Resolve a trusted McpIdentity from a request's context. The original, synchronous identity seam, kept as sugar for the common case of trusting a header an authenticating reverse proxy injected: throwing rejects the whole request. authenticatorFromHook adapts one onto McpAuthenticator, which is what the server actually runs.
interface McpRequestContext The per-request context a transport hands the message handler, and the only thing an authenticator sees of the request. Empty on the stdio transport, which has no request to describe.
| Member | Signature | Doc |
|---|---|---|
headers | headers: Headers | The request headers; an empty Headers on stdio. |
request? | request?: Request | The HTTP request the caller arrived on, when it did — so an authenticator can read its method and URL, not just its headers. Absent on the stdio transport, which has no request. Its body is deliberately empty: the body belongs to the transport, which reads it once to parse the JSON-RPC message, and a credential never lives there. Everything else — method, URL, headers — is the real request's. |
function metadataDocument(settings: ProtectedResourceSettings): Record<string, unknown> The metadata document itself, as the JSON object RFC 9728 §2 defines.
Fields with no values are omitted rather than serialised empty — §3.2 makes that a MUST, and an empty scopes_supported would in any case advertise "this resource accepts no scopes".
function metadataPath(resource: string): string The path the metadata document is published at: the well-known suffix with the resource's own path inserted after it.
For a resource that is a bare origin this is the root well-known path, which is then the conformant location for that identifier. A trailing slash is dropped before insertion, as RFC 9728 §3.1 requires.
function metadataUrl(resource: string): string The absolute URL a WWW-Authenticate challenge points at: always the path-inserted location, which is the one a client can validate under both halves of RFC 9728 §3.3.
function missingTool(settings: S): S Point settings at a binary that cannot exist, so running it raises a "./tooling.ts".ToolNotFoundError without ever launching a real process — the way a wrapper test proves each of its task functions reaches execution.
The platform is pinned to linux so the assertion reads the same on every runner, rather than depending on how the host reports a missing binary:
await assertRejects(() => BiomeTasks.check(missingTool), ToolNotFoundError);type NpmRunner = unknown Runs npm install <args> — the injectable subprocess seam. Defaults to spawning the ambient npm; a test injects a fake that records the argv and plants the expected bin, so provisioning stays hermetic and network-free.
interface NpmToolSpec A specification of an npm-registry package to provision as a tool.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The npm package to install, e.g. "vitest" or "@nestjs/cli". |
version | version: string | The exact version to pin, e.g. "4.1.9" — installed as name@version. |
bin? | bin?: string | The bin to resolve, when it differs from the package name — @nestjs/cli publishes the nest bin, so { name: "@nestjs/cli", bin: "nest" }. Defaults to name. |
type OnCancel = TargetBuilder | unknown A compensation registered with TargetBuilder.onCancel: either a sibling target directly, or a thunk returning one. The thunk form defers evaluation so a compensation declared *below* the target it cleans up (class fields initialise top-to-bottom) can still be referenced.
type OnTimeout = unknown What a timed-out wait does — resolved from WaitSettings.onTimeout.
interface OpenCacheOptions Optional extras for openCache: a remote store and a warning sink.
| Member | Signature | Doc |
|---|---|---|
remote? | remote?: RemoteCacheStore | A RemoteCacheStore to restore outputs from (on a local miss) and upload them to (after a successful run). Applies only to targets that declare TargetBuilder.outputs. |
warn? | warn?: unknown | Report a non-fatal remote-cache error (a get/put failure never fails the build). |
function operatingSystem(os?: Deno.build.os): OperatingSystem The operating system as a Zuke OperatingSystem: darwin becomes macos, windows stays windows, and every other Unix (linux, the BSDs, solaris, …) is reported as linux. Pass a raw Deno.build.os value to normalise it; defaults to the running host — the platform analogue of isCI.
import { operatingSystem } from "jsr:@zuke/core";
if (operatingSystem() === "macos") { ... }type OperatingSystem = linux | macos | windows The operating systems Zuke recognises — Deno's raw Deno.build.os values normalised to a friendly set (notably darwin → macos). Used across the ecosystem so builds branch on "macos" rather than the surprising "darwin".
type OrderingEdge = unknown A soft ordering edge [before, after]: before must run before after, with no data dependency. Returned by "./build.ts".Build.extraEdges to feed a consumer's dependency graph (e.g. a monorepo's dependency-graph.json) into planning; an edge whose endpoints are not both in the run's execution set is ignored, and cycles are reported like any other.
interface OutputHost Filesystem effects used to archive and restore a target's outputs.
| Member | Signature | Doc |
|---|---|---|
readFile | readFile(path: string): Promise<Uint8Array | null> | File contents, or null if the path does not exist. |
stat | stat(path: string): Promise<unknown | null> | Whether a path exists and is a directory, or null if it is missing. |
lstat | lstat(path: string): Promise<unknown | null> | Describe a path *without* following a final symlink, or null if it is missing. Distinct from OutputHost.stat, which resolves a link and so cannot see one: restoreOutputs refuses to write *through* a link, which is a question only an lstat can answer. |
readDir | readDir(path: string): Promise<string[]> | The entry names within a directory. |
writeFile | writeFile(path: string, bytes: Uint8Array): Promise<void> | Write a file, creating parent directories as needed. |
function ownsRun(record: RunRecord, buildId: string | undefined): boolean Whether a process whose origin is buildId may recover record.
True unless both origins are known and differ — see the module documentation for why an absent origin abstains rather than refusing.
function pad(text: string, width: number, align?: left | right): string Pad text to width visible columns, aligning left (default) or right.
function paint(color: boolean, codes: string, text: string): string Wrap text in ANSI codes when colour is enabled, otherwise return it as-is.
function parameter(description?: string): Parameter<string, string | undefined> Create a new build parameter (a string by default). Configure it fluently: .number()/.boolean() change the kind, .options(...) restricts a string, .default(v)/.required() set optionality, and .env(name) overrides the environment variable.
class Parameter A typed build parameter. Declare one with parameter and configure it with the fluent methods; each method returns a new parameter whose value type reflects the configuration (string, number, boolean, and whether it can be undefined).
K is the underlying value kind; T is the exposed value type, which is K for required/defaulted parameters and K | undefined for optional ones.
| Member | Signature | Doc |
|---|---|---|
value | value(): T | The resolved value. Throws if read before the build resolves parameters. |
isSet_ | isSet_(): boolean | Whether the parameter resolved to a defined value (used by .requires()). |
stringValue_ | stringValue_(): string | undefined | The resolved value as a string, or undefined if unset (for masking). |
secret | secret(): Parameter<K, T> | Mark the value as sensitive: it is masked in CI output (::add-mask::) and redacted from all of Zuke's reporter output. Pair with Parameter.from to resolve the value from a secret manager rather than the environment. |
from | from(source: SecretSource): Parameter<K, T> | Resolve the value from a SecretSource (see execSecret / fileSecret) when neither a --flag nor an environment variable supplied one — the source is a fallback provider, consulted before the declared default. Typically paired with Parameter.secret so the resolved value is redacted. |
number | number(this: Parameter<string, string | undefined>): Parameter<number, number | undefined> | Parse the value as a number (e.g. --workers 4). |
boolean | boolean(this: Parameter<string, string | undefined>): Parameter<boolean, boolean> | Treat the parameter as a boolean flag (e.g. --verbose); defaults to false. |
options | options(this: Parameter<string, string | undefined>, ...values: string[]): Parameter<string, string | undefined> | Restrict a string parameter to a fixed set of choices. |
default | default(this: Parameter<K, K | undefined>, value: K): Parameter<K, K> | Provide a default, making value non-optional (K). |
required | required(this: Parameter<K, K | undefined>): Parameter<K, K> | Require a value, making value non-optional (K); errors if unsupplied. |
env | env(name: string): Parameter<K, T> | Override the environment variable read as a fallback for this parameter. |
flag | flag(name: string): Parameter<K, T> | Override the CLI flag this parameter is set by. The leading -- is optional, so .flag("--skip-e2e") and .flag("skip-e2e") are the same. The declared spelling replaces the derived one: only it is accepted on the command line, and it is what --help, the JSON build surface, shell completions and the registry descriptor all show. Reach for this when the name-to-flag rule produces something you would not have chosen — a name containing an initialism that ends in a digit is the usual case, since the digit ends the run of capitals and skipE2E derives --skip-e2-e. The environment variable is derived separately and is unaffected; override it with env. |
array | array(this: Parameter<E, E | undefined>): Parameter<E, E[]> | Accept a comma-separated list (or a repeated flag), exposing value as an array. --tags a,b and --tags a --tags b both yield ["a", "b"]; blank entries are dropped, and an unsupplied *optional* list defaults to [] (a required one is reported missing — see below). Each element is parsed by this parameter's own element parser, so it composes: .options("a", "b").array() validates every element against the choices, and .number().array() yields a number[], rejecting a non-numeric entry. (Apply .options()/.number() before .array().) .array() composes last, after .required() too: a .required().array() list stays required, so an unsupplied value is reported as missing rather than silently resolving to the empty-list default. An optional (non-required) list defaults to []. |
resolve_ | resolve_(raw: string | undefined): void | Resolve from a raw input (or undefined when none was supplied). |
name_? | name_?: string | Property name, assigned during discovery. Undefined until then. |
description_? | description_?: string | Human-readable description shown in --help/--list. |
kind_ | kind_: ParamKind | The runtime value kind. |
required_ | required_: boolean | Whether a value must be supplied (no default). |
options_? | options_?: unknown | The allowed string choices, if restricted with Parameter.options. |
envName_? | envName_?: string | An explicit environment variable name override. |
flagName_? | flagName_?: string | An explicit CLI flag name override, without the leading dashes. |
hasFallback_ | hasFallback_: boolean | Whether the parameter has a declared default value. |
secret_ | secret_: boolean | Whether the value is sensitive and should be masked in CI output. |
array_ | array_: boolean | Whether the value is a comma-separated / repeatable list (.array()). |
source_? | source_?: SecretSource | A provider that resolves the value when no flag/env supplied one. |
default_? | default_?: string | The declared default rendered as a string (an array default is joined with commas), or undefined when the parameter has no default or an empty-list one. For display in tool schemas and --list; never a secret value. |
class ParameterError extends Error Raised when a parameter value is invalid or read before resolution.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The error name. |
type ParamKind = string | number | boolean A parameter's runtime kind tag.
type ParamValue = string | number | boolean The value kinds a parameter can hold.
function parseDuration(value: string | number): number Parse a duration to milliseconds. Accepts a number (already milliseconds) or a string of a non-negative amount and a unit — ms, s, m, h, or d (e.g. "90s", "4h", "1.5h"). Throws a friendly error on anything else.
type PathLike = string | AbsolutePath A filesystem path accepted by Zuke APIs: either a plain string or an AbsolutePath. Anywhere a tool wrapper or build helper takes a path, it accepts a PathLike and coerces it to a string.
function plan(root: TargetBuilder, extra?: unknown): TargetBuilder[] Topologically sort the execution set for root, honouring hard dependencies and the soft before/after ordering hints (the latter only between nodes that are both in the set).
interface Platform A platform with helpers to name it the way a tool's downloads do. osLabel and archLabel map the os/arch to a tool's own naming, falling back to the value itself for anything not in the alias map — so a url callback reads p.osLabel({ macos: "darwin" }) (for a tool that spells macOS "darwin") instead of a hand-written os === … ternary. This is what the InstallReleaseOptions.url and InstallReleaseOptions.checksum callbacks receive.
| Member | Signature | Doc |
|---|---|---|
osLabel | osLabel(aliases?: Partial<Record<OperatingSystem, string>>): string | The OS named for downloads: aliases[os], else the InstallPlatform.os itself. |
archLabel | archLabel(aliases?: Partial<Record<Architecture, string>>): string | The arch named for downloads: aliases[arch], else the InstallPlatform.arch itself. |
interface Plugin A lifecycle observer. Every hook is optional; implement only the ones you need. Hooks may be async — the executor awaits each before continuing.
| Member | Signature | Doc |
|---|---|---|
onStart? | onStart?(run: RunInfo): void | Promise<void> | Called once before any target runs, with the run's RunInfo. |
onTargetStart? | onTargetStart?(target: string, run: RunInfo): void | Promise<void> | Called just before a target's body executes (not for skipped/cached), with the target name and the run's RunInfo. |
onTargetEnd? | onTargetEnd?(target: string, status: TargetStatus, timing: TargetTiming): void | Promise<void> | Called after each target settles, with its final status and its TargetTiming (run id + duration). |
onFinish? | onFinish?(result: BuildResult, run: RunInfo): void | Promise<void> | Called once after the run completes (success or failure), with the result and the run's RunInfo. |
onRunStateChange? | onRunStateChange?(record: RunRecord): void | Promise<void> | Called on each run-level durable status change — the run going running, suspended, succeeded, failed, cancelling, or cancelled — with the current "./state/types.ts".RunRecord. It carries the full record (per-target timings, waits, the audit trail), so a metrics exporter can derive spans, wait durations, and counters from a single source. Only fires when a state store is configured (the record's home); a plain build with no store never produces one, and this hook stays silent. The record is the secret-free projection: secret() parameters are omitted, and ctx.state metadata, target errors, and audit arguments are run through the redactor before they reach it — the same data already persisted to the store and shown by zuke runs show. It is safe to export. A run cancelled in-process (Ctrl-C / its signal) is observed as running → cancelling → cancelled. When another process cancels the run (zuke cancel), this process observes it through cancelling and stops — the canceller's process owns the final cancelled — so treat cancelling as run-ended for the owning process. |
name? | name?: string | A name for diagnostics (optional). |
function prependPath(dir: PathLike, os?: Deno.build.os): string Prepend dir to the process PATH, and return the new value. A tool provisioned into dir (e.g. the bin directory of an "./install.ts".installTree runtime) then resolves for the rest of the build: the shell $, Command, and every tool wrapper spawn subprocesses that inherit Deno.env, so the node_modules/.bin shims and NpmTasks that assume a node/npm on PATH find the provisioned one.
Idempotent — a directory already on PATH is left in place, not duplicated — and uses the platform separator (; on Windows, : elsewhere).
function protectedResource(resource: string): ProtectedResourceSettings Begin a protected-resource declaration for resource, the canonical URI of this MCP endpoint (https://build.example.com/mcp).
import { Build, protectedResource } from "jsr:@zuke/core";
class CI extends Build {
override mcpProtectedResource() {
return protectedResource("https://build.example.com/mcp")
.authorizationServer("https://acme.eu.auth0.com")
.scopes("zuke:run")
.name("Acme build server");
}
}class ProtectedResourceError extends Error Raised when a protected-resource declaration cannot produce a valid document.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The error name. |
class ProtectedResourceSettings A build's protected-resource declaration, configured fluently.
The resource identifier is the single value everything else has to agree with, so it is a direct argument to protectedResource; the rest are setters. It must be the canonical URI of this MCP endpoint — the same string the client sends as its RFC 8707 resource parameter, and the same string the identity provider mints into the token's aud. Those three agreeing byte for byte is the whole contract; when they disagree every token fails validation, and the error says nothing about why.
| Member | Signature | Doc |
|---|---|---|
authorizationServer | authorizationServer(issuer: string): this | Add an authorization server by its issuer identifier — https://acme.eu.auth0.com, not its metadata URL. It must string-match the issuer in that server's own metadata document, or a client is required to reject it. At least one is required: RFC 9728 marks the field optional, but MCP raises it to required. |
scopes | scopes(...values: string[]): this | Declare the scope values a caller may request. These are advertised to clients, which request them wholesale when a challenge names none, so keep the list to what this server actually distinguishes rather than the whole catalogue of a shared identity provider. |
name | name(value: string): this | Set the human-readable resource name shown on a consent screen. |
documentation | documentation(url: string): this | Set the URL of human-readable documentation for this resource. |
resource_ | resource_: string | The canonical resource identifier of this MCP endpoint. |
authorizationServers_ | authorizationServers_: string[] | Issuer identifiers of the authorization servers that issue for it. |
scopes_ | scopes_: string[] | Scope values a caller may request for this resource. |
resourceName_? | resourceName_?: string | Human-readable name of the resource, for a consent screen. |
documentation_? | documentation_?: string | URL of human-readable documentation for the resource. |
type PutBuildResult = unknown | unknown The result of a BuildRegistry.register compare-and-swap write.
type PutResult = unknown | unknown The result of a StateStore.putRun compare-and-swap write.
const REDACTED: [redacted] The placeholder a Redactor substitutes for each secret value.
class Redactor Collects secret values and masks them in text. Register a value with Redactor.add and rewrite a line with Redactor.redact; empty strings are ignored (they would match everywhere) and duplicates are recorded once. Longer secrets are applied first so a secret that contains another is masked whole rather than partially.
| Member | Signature | Doc |
|---|---|---|
add | add(value: string): void | Register a secret value to mask. Ignores empty strings and duplicates. A multi-line value registers each of its lines as well as the whole string, because redaction runs a line at a time and a whole-value pattern can never match one line of it. Lines are trimmed, and a very short one is skipped so it cannot mask ordinary text wherever it appears. |
redact | redact(line: string): string | Replace every registered secret in line with REDACTED. |
size | size(): number | The number of distinct patterns registered. A single-line secret contributes one; a multi-line secret contributes the whole value plus each of its qualifying lines. |
interface Remediation A recovery step plugged into a target with TargetBuilder.recoverWith. It runs only after the target body fails, receives the failure, and may attempt to repair it — returning { retry: true } to ask the executor to re-run the body (the real build command is the verifier). Implemented, for example, by the AI fixer in @zuke/ai, but any object with a remediate method qualifies.
| Member | Signature | Doc |
|---|---|---|
remediate | remediate(context: RemediationContext): RemediationResult | Promise<RemediationResult> | Inspect (and optionally repair) the failure; report whether to retry. |
name? | name?: string | A name for diagnostics (optional). |
interface RemediationContext Context passed to a Remediation after a target body fails.
| Member | Signature | Doc |
|---|---|---|
redact | redact(text: string): string | Mask every resolved secret parameter in text. A remediation that publishes anywhere — a pull-request comment, a job summary, a file it writes — has to run its output through this first. What it is publishing is typically a model's response to a prompt built from the failure, and that prompt carries the failed command and its output, so a secret the build holds can come back in the reply. Zuke's own reporter and run record redact what *they* emit, but a remediation that posts over the network is not going through either of them: this is the only thing between such a value and a comment that cannot be taken back. Masks the same values "./params.ts".parameter marked secret, so a credential the build never declared is not covered — declare it. |
target | target: string | The name of the failed target. |
attempt | attempt: number | The 1-based recovery attempt (the body has already failed attempt times). |
error | error: unknown | The failure being remediated. When a target fails through the shell this is a CommandError carrying the failed command and its captured stderr. |
interface RemediationResult The outcome of one Remediation attempt.
| Member | Signature | Doc |
|---|---|---|
retry | retry: boolean | Re-run the target body after this remediation? true asks the executor to retry (the remediation changed something — e.g. applied a fix); false leaves the failure standing (e.g. a diagnose-only remediation that only explained the failure). |
summary? | summary?: string | A one-line description of what was diagnosed or done, for diagnostics. |
function remoteCacheKey(name: string, fingerprint: string): string The store key for a target's outputs: its name and input fingerprint. The name is sanitised so the key is safe as a filename and a URL path segment.
interface RemoteCacheStore A content-addressed store for archived target outputs, keyed by remoteCacheKey. Both operations are best-effort from the build's point of view: the executor never fails a build because the store is unreachable — it just rebuilds and, where it can, re-uploads.
| Member | Signature | Doc |
|---|---|---|
get | get(key: string): Promise<Uint8Array | null> | Fetch the archived outputs stored under key, or null if there are none. |
put | put(key: string, artifact: Uint8Array): Promise<void> | Store artifact (a gzipped tar of a target's outputs) under key. |
interface RemoveOptions Options for FileTasksApi.remove.
| Member | Signature | Doc |
|---|---|---|
recursive? | recursive?: boolean | Remove a directory and its contents recursively, like rm -r. |
interface Renderer How the executor renders a build's output. Each method is pure — it returns the lines to print rather than writing them — so a custom renderer stays unit-testable and the executor keeps control of the output streams.
| Member | Signature | Doc |
|---|---|---|
targetHeader | targetHeader(style: Style, name: string): string[] | The banner that opens a target's section (a ::group:: under Actions). |
targetPassFooter | targetPassFooter(style: Style, name: string, ms: number): string[] | The footer printed after a target body succeeds. |
targetFailFooter | targetFailFooter(style: Style, name: string, ms: number, error: unknown): unknown | The footer printed after a target body fails, split into info (stdout) and error (stderr) so the caller can fan the lines out correctly. |
targetDryRunFooter | targetDryRunFooter(style: Style, name: string): string[] | The footer printed for a dry-run target that was never executed. |
summaryBlock | summaryBlock(style: Style, reports: TargetReport[], totalMs: number, ok: boolean): string[] | The end-of-build summary block: the aligned table and closing verdict. |
jobSummaryMarkdown | jobSummaryMarkdown(reports: TargetReport[], totalMs: number, ok: boolean): string | The GitHub Actions job-summary Markdown mirroring the terminal summary. |
function repoRoot(...segments: string[]): AbsolutePath The absolute path of the repository root — the directory containing CONFIG_FILE — with any segments appended. The returned value is an AbsolutePath, so it is itself callable for further joining.
repoRoot(); // <root>
repoRoot("src", "main.ts"); // <root>/src/main.ts
repoRoot().join("dist"); // <root>/dist
The root is located by walking up from the current working directory, so the path is resolved at runtime and never hard-coded into a committed file.
interface Reporter Sink for executor output, defaulting to the console. Overridable in tests.
| Member | Signature | Doc |
|---|---|---|
info | info(line: string): void | Write an informational line. |
error | error(line: string): void | Write an error line. |
function reportSummary(pairs: SummaryPairs): void Report key: value notes into the running target's row of the end-of-build summary — the ambient form of "./target.ts".TargetContext.reportSummary, for code that has no context in hand: a tool wrapper reporting the counts its tool printed, or a helper called from a body.
reportSummary({ Tests: 837, Passed: 837, Failed: 0 });
Notes accumulate across calls in the same target, and reporting a key again replaces its value. Outside a running target (a wrapper called from a plain script, a compensation) there is no row to report into, so the call is a no-op rather than an error — a wrapper never has to ask where it runs.
function reportTestCounts(counts: TestCounts): void Report a test run's counts into the running target's row of the end-of-build summary, in the shape every test-runner wrapper shares:
test Succeeded 8.1s // Tests: 837 · Passed: 835 · Failed: 0 · Skipped: 2
Tests is the sum of every category; Passed and Failed always appear, and Skipped, Todo and Flaky only when non-zero — mirroring the runners, which print their optional counts the same way. The ambient form of reporting applies (see reportSummary): a wrapper calls this from its onOutput hook with what it parsed, and outside a running target the call is a no-op.
function resolveBuildId(readEnv: unknown): string | undefined The origin of the build running in this process — ZUKE_BUILD_ID, else GITHUB_REPOSITORY, else undefined when neither is set.
Recorded on a run at creation and compared by every recovery path. An empty value counts as unset, so an exported-but-empty variable does not become an origin that matches nothing.
function resolveBuildRegistry(option: BuildRegistry | false | undefined, declared: BuildRegistry | undefined, options: ResolveRegistryOptions): BuildRegistry | undefined Pick the build registry by precedence: an explicit option wins (false disables the registry entirely), then a declared registry (a build's registry() override), then the envBuildRegistry environment fallback, then — only when ResolveRegistryOptions.enableDefault — a filesystem registry under <root>/.zuke/builds.
interface ResolveRegistryOptions Inputs resolveBuildRegistry needs to build the default filesystem registry.
| Member | Signature | Doc |
|---|---|---|
readEnv | readEnv: unknown | Reads an environment variable (injectable for tests). |
host | host: StateHost | Filesystem effects for the default/env filesystem registry. |
defaultDir | defaultDir: string | Directory the default filesystem registry writes to (<root>/.zuke/builds). |
enableDefault | enableDefault: boolean | Fall back to the default filesystem registry when nothing else is configured. zuke register sets this so the command works out of the box. |
function resolveRemoteStore(option: RemoteCacheStore | false | undefined, declared: RemoteCacheStore | undefined, readEnv: unknown): RemoteCacheStore | undefined Pick the remote store for a run by precedence: an explicit option wins (false disables the remote cache entirely), then a declared store (a build's remoteCache() override), then the envCacheStore environment fallback.
interface ResolveStateOptions Inputs resolveStateStore needs to build the default filesystem store.
| Member | Signature | Doc |
|---|---|---|
readEnv | readEnv: unknown | Reads an environment variable (injectable for tests). |
host | host: StateHost | Filesystem effects for the default/env filesystem store. |
defaultDir | defaultDir: string | Directory the default filesystem store writes to (<root>/.zuke/runs). |
enableDefault | enableDefault: boolean | Fall back to the default filesystem store when nothing else is configured. Set when the run opts into durable state (--state, or — from a later milestone — a durable feature like a lock or a wait). |
function resolveStateStore(option: StateStore | false | undefined, declared: StateStore | undefined, options: ResolveStateOptions): StateStore | undefined Pick the state store for a run by precedence: an explicit option wins (false disables state entirely), then a declared store (a build's stateStore() override), then the envStateStore environment fallback, then — only when ResolveStateOptions.enableDefault — a filesystem store under <root>/.zuke/runs. A plain build with no durable feature and no configuration gets undefined, so it carries zero overhead.
async function restoreOutputs(artifact: Uint8Array, host: OutputHost, outputs?: unknown, maxBytes?: number): Promise<string[]> Restore the files in artifact (a gzipped tar produced by archiveOutputs) to disk, returning the paths written.
Every entry is validated before anything is written, so a rejected archive leaves no half-written, partially-trusted output tree. An entry is refused when its name is absolute or escapes the workspace with .., when it is a symlink or directory entry (which archiveOutputs never produces), when it lands under .git or .zuke, when — given outputs — it falls outside the target's declared outputs, and when the path it would be written to passes through, or is, a symlink that already exists on disk.
That last refusal is what makes the confinement real rather than lexical. The archive cannot plant a link, but a workspace can already hold one at a declared output — dist -> /tmp/build, a checked-out bazel-bin, a Windows junction — and writeFile follows it. Such a workspace no longer restores from the remote cache and rebuilds instead; the link is left alone, because the layout is the owner's and silently replacing it would be its own surprise.
async function resumeCheck(build: Build, options: Omit<ResumeOptions, runId | signal | data> & unknown): Promise<unknown> Re-attempt every suspended run in the store (or just runId): predicate-based waits are re-evaluated and expired waits time out. Signal-based waits with no new signal simply re-suspend. Returns the number of runs that ended in failure. This is the sweep a cron or webhook drives (zuke resume --check).
A run whose record is "./state/types.ts".RunRecord.degraded is counted as failed on every sweep until an operator resolves it: it cannot be advanced without deciding whether its targets are safe to repeat, and a non-zero result is the only channel a cron watches. Its refusal is reported through the reporter (the console unless silenced) so the cause is visible, and it stays suspended, so a later sweep with ResumeOptions.resumeDegraded still picks it up.
interface ResumeOptions Options for resumeRun.
| Member | Signature | Doc |
|---|---|---|
runId | runId: string | The id of the suspended run to resume. |
stateStore? | stateStore?: StateStore | false | Durable store the run lives in. Defaults to the same resolution as a normal run (explicit → stateStore() override → env → .zuke/runs); resume always needs one. |
signal? | signal?: string | Deliver a signal by this name before resuming (satisfies externalSignal). |
data? | data?: JsonValue | The signal's JSON payload (defaults to {}); ignored without signal. |
params? | params?: Record<string, string> | Non-secret parameter overrides; the rest come from the record. |
readEnv? | readEnv?: unknown | Reads an environment variable (secrets re-resolve from here). |
actor? | actor?: string | Who to attribute the resumption to (stamped on the run). |
forceGraph? | forceGraph?: boolean | Continue even if the build graph changed since the run was suspended. |
resumeDegraded? | resumeDegraded?: boolean | Resume even though the record is "./state/types.ts".RunRecord.degraded — a state write was permanently lost, so a target that succeeded may still be recorded running or pending. The resume trusts the record as written, which means such a target runs again; passing this accepts that risk, on the grounds that the operator — not Zuke — knows whether the target is safe to repeat. |
silent? | silent?: boolean | Suppress banner/summary output. |
reporter? | reporter?: Reporter | Custom reporter; overrides silent. |
plugins? | plugins?: Plugin[] | Lifecycle observers for the resumed run. Because a resume keeps the original run id, a plugin sees the continuation under the same identity — so an exporter's spans join one trace across the suspend/resume boundary. |
async function resumeRun(build: Build, options: ResumeOptions): Promise<BuildResult> Resume the suspended run options.runId for build. Transitions it to running (exactly one resumer wins), optionally delivers a signal, checks the graph still matches, and continues via "./executor.ts".execute, re-running only the not-yet-succeeded targets.
interface ResumeState The continuation state resumeRun hands to execute on a resume.
| Member | Signature | Doc |
|---|---|---|
record | record: RunRecord | The run being continued (already transitioned to running). |
version | version: string | Its current store version, for the writer to continue from. |
done | done: ReadonlySet<string> | Names of targets recorded succeeded — seeded as done, never re-run. |
lease? | lease?: HeldLease | The lease the resumer took before moving the record out of suspended. Held by the resumer rather than acquired here, because the record must never read running in the store without its lease already held — that pairing is what tells a sweep the difference between a live run and an abandoned one. |
function resumeWhen(check: unknown, options?: ResumeWhenOptions): WaitTrigger A trigger satisfied when an async check predicate returns true. Zuke does not poll on its own — the predicate is evaluated when the target is reached and on each zuke resume <id> --check, so a cron or webhook nudging --check drives it. Use it to wait on state Zuke can query (a row, a file, an API).
interface ResumeWhenOptions Options for resumeWhen.
| Member | Signature | Doc |
|---|---|---|
interval? | interval?: string | number | How often zuke resume --check should re-evaluate the predicate. |
async function run(BuildClass: unknown, options?: RunOptions): Promise<void> Public entry point. Instantiate the build, parse arguments, run, and set the process exit code.
Call it at the bottom of your build file — no import.meta.main guard needed. run acts only when its module is the program's entry point; when the file is imported instead (for example under test) it does nothing.
await run(MyBuild);
// …with plugins:
await run(MyBuild, { plugins: [timing] });const RUN_LEASE_PREFIX: zuke-run The lease name a run's own claim is taken under.
Named once because two places have to agree on it exactly: the process claiming a run, and any sweep deciding whether that run still has an owner.
const RUN_LEASE_TTL_MS: 60000 How long a lease lives before a crashed holder's claim lapses.
The holder renews at half this interval, so a live process keeps its claim indefinitely while a dead one becomes reclaimable within the TTL. Sixty seconds trades promptness for tolerance: long enough that an ordinary pause — a slow step, a busy host, a paused container — does not look like death, short enough that a genuinely dead run is picked up on the next sweep rather than hours later.
async function runConformanceCli(args: string[], deps?: ConformanceCliDeps): Promise<number> Run the conformance kit as a CLI: --url <base> (required) names the backend and both suites run against it. Prints a PASS/FAIL line per scenario and resolves to a process exit code — 0 when every scenario passes, 1 when any fails, --url is missing, or a credential was passed as an argument.
The bearer token comes from the environment, ZUKE_STATE_TOKEN and ZUKE_REGISTRY_TOKEN, exactly as every other consumer of these stores reads it. It used to be a --token argument, which put a credential that can forge run records, the audit trail and lock exclusivity into the process table for any local user to read, and into the transcript of whatever invoked it.
--token is now refused rather than ignored: an invocation that still passes one would otherwise authenticate as anonymous and fail somewhere less obvious, and the credential would already have been exposed by the time it did.
interface RunEvent One entry in a run's audit trail: an MCP tool call, who made it, and how it ended. Appended (never mutated) so the trail is a chronological record. The MCP server records a RunEvent for every mutating or denied tool call; zuke runs show prints them.
| Member | Signature | Doc |
|---|---|---|
at | at: string | ISO-8601 time the call was recorded. |
tool | tool: string | The tool called (e.g. run:deploy, signal_run). |
actor | actor: string | Who made the call (a resolved actor; see "./record.ts".resolveActor). |
outcome | outcome: RunEventOutcome | Whether the call ran, was denied by authorization, or errored. |
args | args: Record<string, string> | The call's arguments, redacted — secret values masked, tokens dropped. |
detail? | detail?: string | A short, redacted human detail (e.g. a denial reason), when present. |
roles? | roles?: unknown | The roles the caller held, when the server authenticated them. What makes a denial answerable afterwards: the actor and the reason say who was refused and by which rule, and this says what they were carrying at the time. Absent on a server with no authenticator, which knows of no roles. |
type RunEventOutcome = ok | denied | error The outcome recorded for an audited MCP tool call (see RunEvent).
interface RunGraphNode One entry of a run's graph-shape snapshot.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The target's dotted name. |
dependsOn | dependsOn: string[] | The dotted names of its direct dependencies. |
interface RunInfo Run identity passed to a plugin's lifecycle hooks, so an observer can group a run's events (e.g. under one trace id) — stable across a suspend/resume boundary, since a resumed run keeps the original id.
| Member | Signature | Doc |
|---|---|---|
runId | runId: string | The run id, stable for every target in the run (and across a resume). |
dryRun | dryRun: boolean | True when the run is a dry run (no target body executes). |
interface RunInitiator Who asked for a run, stamped once when it is created and never rewritten.
Distinct from RunRecord.actor, which every resume overwrites with whoever picked the run up — so on a run that suspended and was resumed by a sweep, actor is the sweep's service account and this is still the engineer who started it. That is the subject a run-scoped authorization rule means by "whoever started this run", and the difference a notification needs to tell a person's deploy from a scheduler's.
| Member | Signature | Doc |
|---|---|---|
actor | actor: string | Who asked for the run, resolved once at creation. |
kind | kind: ActorKind | Whether a person or a machine asked. Stated, never inferred from the actor. |
at | at: string | ISO-8601 time this attribution was fixed. The run's createdAt for a run stamped at creation, and the *original* run's createdAt for one backfilled when a resume was about to overwrite the evidence — so it dates the attribution, not the write that recorded it. |
interface RunningService A started service the executor holds until it tears it down.
| Member | Signature | Doc |
|---|---|---|
stop | stop(): Promise<void> | Stop the service; never rejects (failures are the registry's concern). |
name | name: string | The service's target name, for diagnostics. |
class RunNotSuspendedError extends Error Raised when a run is no longer suspended by the time a resume reaches it — it has been settled, or a cancellation is in progress.
The counterpart to AlreadyResumedError, which covers a run another process is *currently* driving. This covers one that already finished, and a sweep treats it the same way: not its run to advance, and not a failure. Two sweeps racing the same run is the normal case — one wins, and the loser reading succeeded has discovered a success, not a fault. Counting it would put a false alarm in the exit code a cron watches.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The error name. |
interface RunOptions Options for run.
| Member | Signature | Doc |
|---|---|---|
args? | args?: string[] | Command-line arguments. Defaults to Deno.args. |
plugins? | plugins?: Plugin[] | Lifecycle observers to run alongside the build's own hooks. |
renderer? | renderer?: Renderer | Renderer for the per-target banners and end-of-build summary. Defaults to Zuke's built-in look; inject consoleRenderer from @zuke/console (or a custom Renderer) to restyle a build's output. |
interface RunPlan The resolved shape of this run: the class-field targets it plans, in order, and the dependencies between them.
This describes the plan, not the outcome. A target is in the plan because the graph put it there; it may still be skipped by a condition, by --affected, or by an operator's forced outcome, and a run that fails early never reaches its later targets at all. Ask RunPlan.includes what the run set out to do, and ctx.outcomeOf(...) what actually became of a target once it settled.
Two things the plan is not.
It is not every target that will appear in the run record. A .forEach() fan-out expands into its sub-targets while the run executes, long after the graph is planned, so fan[us].prep has an outcome and a summary row but is absent from the plan — only the fan target that produced it is in there. For a fan-out, outcomeOf sees more than includes does.
It is not a promise that holds across processes. The plan is the graph as this process resolved it, and it is fixed for the whole of this process: two bodies, and both evaluations of a condition, always agree. A second process — a resume, or a zuke cancel running compensations — re-resolves the graph from the build class it was given, so it can legitimately differ: the class may have changed since the run was suspended (which is what --force-graph is for), and a lazy orderWith provider may answer differently or, if it is unreachable, be degraded to the base topological order.
| Member | Signature | Doc |
|---|---|---|
includes | includes(target: string): boolean | Whether target is part of this run's planned set. The question a body asks to decide whether its work is needed by something else in the run — "am I building for a deploy that was actually asked for?". An unknown name is false, never an error, so probing for an optional target does not need a guard. |
dependenciesOf | dependenciesOf(target: string): unknown | The targets that must complete before target may start: everything the planner treats as a predecessor — declared dependsOn dependencies, the targets this one triggers, and the soft before/after, extraEdges and orderWith edges that apply within this run's set. Empty for a target with no predecessors and for a name that is not in the plan — use RunPlan.includes to tell those apart. |
targets | targets: unknown | Every planned target's dotted name, in the run's deterministic execution order. The build summary can list more rows than this: a .forEach() fan-out's sub-targets are created during execution and get their own rows, but are not part of the planned graph. |
interface RunQuery Filters for "./store.ts".StateStore.listRuns; all fields are optional.
| Member | Signature | Doc |
|---|---|---|
status? | status?: RunStatus | Keep only runs with this status. |
target? | target?: string | Keep only runs whose graph contains a target with this dotted name. |
since? | since?: string | Keep only runs created at or after this ISO-8601 timestamp. |
limit? | limit?: number | Return at most this many runs (the newest, since listing is newest-first). Applied server-side so a large store stays listable; 0 returns none. |
interface RunRecord A versioned snapshot of one run. Persisted as JSON; a store's opaque version (an ETag / content hash) drives compare-and-swap writes.
| Member | Signature | Doc |
|---|---|---|
id | id: string | Unique run ID (matches "../target.ts".TargetContext runId). |
build | build: string | The build class name. |
buildId? | buildId?: string | Which build instance this run belongs to — ZUKE_BUILD_ID, else GITHUB_REPOSITORY, resolved once at creation. Absent when neither was set (and on every record written before this field existed). The class name above cannot identify a build: a zuke.ts templated across a dozen services shares its name, its target names and its graph shape, so every shape-based check passes and one service's recovery sweep would drive another's runs with its own target bodies. This is what a recovery path compares; see "../ownership.ts". |
rootTarget | rootTarget: string | The dotted name of the requested (root) target. |
status | status: RunStatus | The run's lifecycle status. |
actor | actor: string | The run's last writer (resolved from --actor, ZUKE_ACTOR, or CI env). Every resume overwrites it with whoever picked the run up, so it answers "who touched this most recently", not "whose run is this" — see RunRecord.initiator for that. |
initiator? | initiator?: RunInitiator | Who asked for the run, stamped once at creation and immutable thereafter. Absent on a record written before this field existed; such a record's RunRecord.actor is the closest answer available, and is exactly the right one on a run that was never resumed. |
overrides? | overrides?: Record<string, TargetOverride> | Operator-forced target outcomes, keyed by dotted target name (see TargetOverride). Absent until something is forced. Read when the executor reaches the target, so an override lands for any target the run has not started yet — in practice on the next resume, since that is the process that loads the record after the force was written. |
createdAt | createdAt: string | ISO-8601 timestamp when the run was created. |
updatedAt | updatedAt: string | ISO-8601 timestamp of the last write. |
graph | graph: RunGraphNode[] | The graph shape the run planned, in declaration order. |
params | params: Record<string, string> | Resolved parameter values, keyed by name. Secrets are always omitted. The values the run was launched with, and they are not rewritten. A resume may supply different ones, in which case the run executes under two sets and this field cannot hold both — so it keeps the launch's, which is what the targets that ran before the suspension used, and what a cancellation resolves each compensation body from. A resume that changed anything records it in RunRecord.events instead. |
targets | targets: Record<string, TargetRunState> | Per-target progress, keyed by dotted target name. |
signals | signals: Record<string, SignalRecord> | External signals received so far, keyed by name (see .waitsFor(...)). |
events | events: RunEvent[] | Append-only audit trail of MCP tool calls against this run (see RunEvent). |
degraded? | degraded?: boolean | True when at least one state write for this run was permanently lost — a conflicting write from another process could not be re-applied within the writer's retry budget. Writes are best-effort, so the run itself carried on; the flag is how a later reader learns that a transition which really happened may be missing from the record. In particular a target that succeeded can still be recorded running or pending, so a resume would re-run it — which is why a resume refuses a degraded record unless --resume-degraded overrides it (see "../resume.ts".ResumeOptions.resumeDegraded) — and why a cancellation compensates every target whose success the record cannot rule out, rather than only those recorded succeeded (see "../cancel.ts".runCompensations). It is set by the writer when it loses a write and persisted by the next write that lands — the failing one, by definition, could not carry it. A drop that leaves the mutation in memory for a later write to re-persist does *not* set it. Absent (or false) means no write is known to be missing. |
deadlineAt? | deadlineAt?: string | ISO-8601 wall-clock deadline for the whole run, stamped once at creation from Build.deadline(). Absent when the build sets none. A budget for *running*, not for existing. A run parked at an approval gate is not spending it — its budget there is the wait's own timeout — so only a sweep over running runs consults this. |
intendedTerminal? | intendedTerminal?: RunStatus | The terminal status the process that moved this run to cancelling means to leave it in. Absent means cancelled, which is what an ordinary zuke cancel intends and what every record written before this field existed meant. Recorded rather than inferred, because the settlement can be finished by a *different* process than the one that began it: a canceller that crashes leaves the run cancelling, and whoever recovers it has no other way to know whether an operator was cancelling the run or a sweep was failing an abandoned one. |
function runSettings(settings: S, configure?: Configure<S>): Promise<CommandOutput> Construct-configure-run: the shared shape of every task function.
export const MyTasks = {
build: (configure?: Configure<MyBuildSettings>) =>
runSettings(new MyBuildSettings(), configure),
};type RunStatus = running | suspended | cancelling | succeeded | failed | cancelled The lifecycle status of a whole run. cancelling is the transient state a cancellation moves through — the run has been asked to stop and its compensations are running — before it settles as cancelled.
interface RunSummary A compact run listing row, returned by "./store.ts".StateStore.listRuns.
| Member | Signature | Doc |
|---|---|---|
id | id: string | The run ID. |
build | build: string | The build class name. |
rootTarget | rootTarget: string | The dotted name of the requested (root) target. |
status | status: RunStatus | The run's lifecycle status. |
actor | actor: string | The run's last writer (see RunRecord.actor). |
initiator? | initiator?: RunInitiator | Who asked for the run (see RunRecord.initiator). Absent on a record written before the field existed, and from a store that does not project it — RunSummary.actor is the fallback in both cases. |
createdAt | createdAt: string | ISO-8601 creation timestamp. |
updatedAt | updatedAt: string | ISO-8601 timestamp of the last write. |
function satisfiesRole(held: unknown, required: string): boolean Whether a caller holding held satisfies a requirement for required.
A built-in role is satisfied by any built-in at or above it; anything else is satisfied only by holding that exact name. The two rules cannot be collapsed: ordering a name the model does not know would mean guessing where an identity provider's group sits in a hierarchy it never agreed to.
interface ScheduleEntry A scheduled trigger: a 5-field cron expression in an optional IANA timezone.
| Member | Signature | Doc |
|---|---|---|
cron | cron: string | A standard 5-field cron expression (minute hour day-of-month month day-of-week). |
tz? | tz?: string | An IANA timezone (e.g. Europe/Sofia) the cron is expressed in. Omitted (or UTC) means the cron is already UTC and is emitted verbatim. |
class SecretError extends Error Raised when a SecretSource cannot produce a value.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The error name. |
interface SecretSource A provider that resolves a secret's value on demand. Built by execSecret or fileSecret and attached to a parameter with .from(source); the framework calls SecretSource.resolve during parameter resolution.
| Member | Signature | Doc |
|---|---|---|
resolve | resolve(): Promise<string> | Produce the secret value, or throw SecretError on failure. |
function service(): ServiceBuilder Create a service target — a long-lived process kept running while its dependents execute. Configure it with ServiceBuilder.start / ServiceBuilder.readyWhen and depend on it from a target.
class ServiceBuilder extends TargetBuilder A long-lived target. Configure how it starts (ServiceBuilder.start), how to tell it is ready (ServiceBuilder.readyWhen), and — when the started handle is not self-stopping — how it stops (ServiceBuilder.stop). It inherits the ordering methods (dependsOn, before, after, description) from TargetBuilder; a service has no .executes body.
| Member | Signature | Doc |
|---|---|---|
effect | effect(name: string, fn: EffectFn): this | Refuse a crash-durable effect on a service. A service target's whole job is launching a process; it runs no body, so there is no point at which its effects would be driven and they would be dropped without a word. Inherited from TargetBuilder only because this is a subclass of it, so the refusal is stated here rather than left to be discovered. |
start | start(fn: unknown): this | How to start the process. Return a ServiceHandle (e.g. $\…\.spawn()) so the service can be stopped on teardown; provide a custom ServiceBuilder.stop if the handle is not self-stopping. |
readyWhen | readyWhen(fn: unknown): this | A readiness probe, polled until it returns true (or the timeout is hit). Without one, the service is considered ready the moment it starts. See tcpReachable for the common "is the port accepting connections?". |
readyTimeout | readyTimeout(ms: number): this | Override how long to wait for ServiceBuilder.readyWhen (default 30s). |
stop | stop(fn: unknown): this | Custom teardown, given the handle ServiceBuilder.start returned. |
launch_ | launch_(name: string): Promise<RunningService> | INTERNAL: start the service and wait until it is ready, returning a handle the executor stops on teardown. Throws ServiceError if no start was configured, or if the service does not become ready in time (the just-started process is stopped first so it is not leaked). |
class ServiceError extends Error Raised when a service cannot start or does not become ready in time.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The error name. |
interface ServiceHandle A running service — whatever ServiceBuilder.start returns. Its ServiceHandle.stop tears it down; a {@link https://jsr.io/@zuke/core SpawnedProcess} is one, so .start(() => $\…\.spawn()) needs no explicit stop.
| Member | Signature | Doc |
|---|---|---|
stop | stop(): void | Promise<void> | Terminate the service. Called on teardown unless .stop() overrides it. |
class ServiceRegistry Holds the services started during a run and stops them in reverse order on teardown. Stopping never throws — a failure to stop one service is reported and the rest are still stopped.
| Member | Signature | Doc |
|---|---|---|
register | register(running: RunningService): void | Record a started service to stop later. |
size | size(): number | The number of services currently held. |
stopAll | stopAll(report: unknown): Promise<void> | Stop every registered service, newest first, reporting each outcome. |
const SGR: unknown ANSI select-graphic-rendition codes, keyed by style name.
function sgrCodes(names: unknown): string Concatenate the escape codes for names (an unknown name contributes none).
class ShellArgsError extends Error Raised when splitShellArgs reaches the end of the input with a quote still open. Names the offending quote character and the offset at which it was opened so the bad spot in a long command line is findable.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The error name. |
function shimFallbackArgv(argv: ReadonlyArray<string>, os: Deno.build.os): string[] | null On Windows, wrap an argv in a cmd /c invocation so .cmd/.bat shims (such as npm's) become spawnable; returns null on other platforms.
interface SignalRecord A payload received for an external signal (see RunRecord.signals).
| Member | Signature | Doc |
|---|---|---|
data | data: JsonValue | The signal's JSON payload ({} when none was sent). |
receivedAt | receivedAt: string | ISO-8601 timestamp when the signal was recorded. |
class SlackAnnouncementSettings extends AnnouncementSettings Fluent settings for AnnounceTasksApi.slack. Bot mode (.bot().token(t).channel(c)) posts through the Web API (chat.postMessage).
class SlackApiError extends Error Raised when the Slack Web API accepts the request but reports a logical failure ({ ok: false }), carrying Slack's machine-readable error code (e.g. channel_not_found, not_in_channel, invalid_auth).
| Member | Signature | Doc |
|---|---|---|
name | name: string | The error name. |
class SpawnedProcess A long-lived process started with Command.spawn — the handle a {@link https://jsr.io/@zuke/core service} keeps alive. Unlike awaiting a Command, spawning does not wait for the process to exit; call SpawnedProcess.stop to terminate it (which is also the default service teardown). Its stdout/stderr are inherited so the process's own output is visible.
| Member | Signature | Doc |
|---|---|---|
pid | pid(): number | The operating-system process id (-1 for a dry-run stub). |
status | status(): Promise<Deno.CommandStatus> | Resolves when the process exits (immediate success for a dry-run stub). |
stop | stop(signal?: Deno.Signal, graceMs?: number): Promise<void> | Terminate the process and wait for it to exit. Sends signal (default SIGTERM); if the process has not exited within graceMs (default 5s), it escalates to SIGKILL so a process that ignores SIGTERM cannot hang teardown. A process that has already exited is treated as stopped. A dry-run stub (no child) is a no-op. |
function splitShellArgs(input: string): string[] Split input into argv the way a POSIX shell would, honouring the quoting rules only:
- Unquoted runs of whitespace separate arguments; leading, trailing, and repeated whitespace produce no empty arguments. - Single quotes are fully literal — no escape sequences at all, so 'a\b' yields a\b. - Inside double quotes a backslash escapes only ", \, ` `, $, and a newline; before anything else it stays literal, so "\d+" yields \d+ rather than silently losing the backslash. - Outside quotes a backslash escapes the following character, so a\ b is one argument. - A backslash-newline pair is a line continuation and is removed, both unquoted and inside double quotes; a backslash at the very end of the input is a dangling continuation and is dropped. - Adjacent segments concatenate (a"b c"d → ab cd) and a quoted empty string is a real, empty argument ('' → [""]`).
Non-goals, deliberately not implemented — the input is turned into argv, never interpreted: no variable expansion ("$HOME" stays $HOME), no globbing, no tilde expansion, no command substitution, and no operator handling of any kind (|, &&, ;, > are ordinary characters). A caller that needs those must split on them itself, or run a real shell.
\r is treated as a separator alongside space, tab, and newline so a command line read from a CRLF file cannot smuggle an invisible carriage return into an argument.
interface StateHost Injected filesystem effects for "./fs_store.ts".FileSystemStateStore, so it stays unit-testable. The default implementation is defaultStateHost.
| Member | Signature | Doc |
|---|---|---|
readText | readText(path: string): Promise<string | null> | File contents, or null when the file does not exist. |
writeText | writeText(path: string, content: string): Promise<void> | Write a file's contents, creating parent directories as needed. |
rename | rename(from: string, to: string): Promise<void> | Rename a file (used to publish a temp file atomically). |
createExclusive | createExclusive(path: string): Promise<boolean> | Create path exclusively: resolve true if it was created, false if it already existed. Used as an atomic lock marker. |
remove | remove(path: string): Promise<void> | Remove a file; a missing file is not an error. |
listDir | listDir(path: string): Promise<string[]> | The entry names in a directory, or [] when the directory is absent. |
mkdirp | mkdirp(path: string): Promise<void> | Create a directory and any missing parents. |
now | now(): number | The current time in epoch milliseconds — the clock for lock expiry (injectable for tests). |
interface StateStore Pluggable persistence for run records. version is an opaque token (an ETag or content hash) used for optimistic concurrency: a write only lands if the stored version still matches the one the writer last read, so two writers racing at the same version cannot both win.
| Member | Signature | Doc |
|---|---|---|
getRun | getRun(id: string): Promise<unknown | null> | Fetch a run and its current version, or null if it does not exist. |
putRun | putRun(record: RunRecord, expectedVersion: string | null): Promise<PutResult> | Write record only if the stored version equals expectedVersion (null meaning "must not exist yet"). Returns the new version, or a conflict when the stored version has moved on — the caller re-reads and retries. |
listRuns | listRuns(query: RunQuery): Promise<RunSummary[]> | List runs matching query, newest first (by createdAt, then id). |
deleteRun | deleteRun(id: string): Promise<void> | Delete a run permanently. A missing run is not an error (delete is idempotent). Backs zuke runs prune; on the HTTP backend this maps to a DELETE /runs/:id a server may leave unimplemented (retention there is the server's job — see docs/state-api.md). |
acquireLock | acquireLock(key: string, holder: LockHolder, ttlMs: number): Promise<LockResult> | Atomically acquire the lock key for holder, expiring after ttlMs. An expired lock is taken over. Returns a token on success, or the current holder when the lock is live. |
renewLock | renewLock(key: string, token: string, ttlMs: number): Promise<boolean> | Extend the lock key held under token by another ttlMs. Returns false if the token no longer owns it (expired and taken over), so a heartbeat can detect a lost lock. |
releaseLock | releaseLock(key: string, token: string): Promise<void> | Release the lock key if still held under token; a no-op otherwise. |
listLocks? | listLocks?(): Promise<HeldLockEntry[]> | Every lock currently held, keyed and ordered by key — the read-only answer to "who holds this, and until when?". Expired records are not held locks and are left out: reporting one as held is the failure this exists to prevent. Optional, so a store implemented outside this repository does not break by not having one. A caller that needs the listing rather than an empty result should go through listStoreLocks, which fails with a message naming the backend instead of pretending the store holds nothing. |
type StateStoreFactory = unknown A () => factory the kit calls once to obtain the store under test.
function stripAnsi(text: string): string Strip ANSI escape sequences, leaving the visible text.
Style type StyleName = unknown A style name understood by sgrCodes, paint, and markup.
function stylize(color: boolean, names: unknown, text: string): string Paint text in the named styles when color is enabled.
class SubcommandSettings extends ToolSettings Base for a wrapper over a CLI organised into subcommand groups — a command path built with command plus repeatable --flag [value] options built with flag. The agent and cloud wrappers (gh, gcloud, claude, gemini, codex) share this shape; each sets its binary via ToolSettings.defaultTool and, when needed, a fixed prefix via leadingTokens or between-command global flags via middleTokens.
The argv is assembled as [...leadingTokens(), ...command, ...middleTokens(), ...flags], keeping every token a discrete argv entry so command construction stays injection-free.
| Member | Signature | Doc |
|---|---|---|
command | command(...parts: Array<string | number>): this | Append command-path tokens — the group, verb, and operands — in order. |
flag | flag(name: string, value?: string | number): this | Add an arbitrary flag. With a value it renders --name value; without one the bare --name. Repeatable. |
interface SummaryEntry One rendered key: value note on a target's summary row.
| Member | Signature | Doc |
|---|---|---|
key | key: string | The note's label, as reported (whitespace collapsed to one line). |
value | value: string | The note's value, rendered as text (whitespace collapsed to one line). |
type SummaryPairs = Readonly<Record<string, SummaryValue>> The notes a target reports, keyed by their label — { Passed: 837, Failed: 0 }. Keys render in the order they are first reported.
type SummaryValue = string | number A value a summary note may carry; a number is rendered as written.
interface SymlinkOptions Options for FileTasksApi.symlink.
| Member | Signature | Doc |
|---|---|---|
force? | force?: boolean | Replace an existing entry at the link path, the way ln -sfn does (default false, which throws an AlreadyExists as Deno.symlink does). The replacement is atomic: the new link is created under a sibling temp name and renamed over the path, so a concurrent reader sees either the old entry or the new link and never a missing path. A directory at the path is never replaced — the rename refuses it, empty or not — so forcing a link cannot cost a caller a directory. |
type? | type?: file | dir | What the link points at: "file" or "dir". Windows needs the distinction and ignores nothing else; POSIX ignores the option entirely. Pass "dir" when linking a directory, or the link is unusable on Windows. |
async function syncCiFiles(files: unknown, options?: CiSyncOptions): Promise<CiSyncResult[]> Bring each declared CiFile on disk in line with its definition. By default a changed file is rewritten; in check mode it is reported stale instead (so CI can fail when the committed config has drifted).
function table(style: Style, columns: unknown, rows: unknown, options?: TableOptions): string[] An aligned text table: a styled header row, an optional dividing rule, then one line per row. Column widths fit the widest visible cell; cells may already carry ANSI colour. Rows shorter than the columns are padded with empty cells.
interface TableColumn One column of a table.
| Member | Signature | Doc |
|---|---|---|
header | header: string | The column header. |
align? | align?: left | right | Cell alignment. Defaults to left. |
interface TableOptions Options for table.
| Member | Signature | Doc |
|---|---|---|
separator? | separator?: string | Column separator. Defaults to two spaces. |
divider? | divider?: boolean | Draw a dividing rule under the header. Defaults to true. |
headerStyle? | headerStyle?: unknown | Styles for the header row. Defaults to ["bold"]. |
dividerStyle? | dividerStyle?: unknown | Styles for the divider rule. Defaults to ["dim"]. |
function tar(entries: TarEntry[]): Uint8Array Create a ustar archive from the given entries (in order).
interface TarEntry A single entry within a tar archive — a regular file or a symbolic link.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The entry's path inside the archive (≤ 100 bytes). |
data | data: Uint8Array | The file contents (empty for a symlink entry). |
linkname? | linkname?: string | For a symbolic-link entry, its target (≤ 100 bytes); absent for a regular file. Node's release tarballs, for one, ship bin/npm/bin/npx as symlinks into lib/node_modules, so extracting a runtime tree must preserve them. |
function target(): TargetBuilder Create a new, empty target builder.
type Target = TargetBuilder A configured target. Alias of TargetBuilder — the same object both builds and represents the target. Exposed as Target for use in signatures.
class TargetBuilder The fluent builder returned by target. All configuration methods are chainable and return this. A body (via TargetBuilder.executes) is required before a target can be executed.
| Member | Signature | Doc |
|---|---|---|
description | description(text: string): this | Set the human-readable description shown in zuke --list. |
dependsOn | dependsOn(...targets: Array<TargetBuilder | Group>): this | Declare hard prerequisites. References sibling targets via this.x, or a group (which expands to every member that has joined it). |
partOf | partOf(group: Group): this | Join a parallel group. Members of the same group run concurrently with one another (each still awaiting its own dependencies) even when the build is otherwise sequential. Declare the group before the targets that join it. |
inputs | inputs(...paths: PathLike[]): this | Declare input files or directories (directories are hashed recursively). A target with inputs is *incremental*: it is skipped (reported cached) when its inputs are unchanged since the last successful run and all its outputs still exist. Repeatable. |
outputs | outputs(...paths: PathLike[]): this | Declare output files or directories. A cache hit also requires every output to still exist, so deleting an output forces a rebuild. Repeatable. |
onlyWhen | onlyWhen(condition: Condition): this | Run only when condition holds; otherwise the target is skipped (and its dependents still run). The predicate may be async and can read resolved parameters or the environment. Repeatable — all conditions must hold. ``ts deploy = target() .onlyWhen(() => this.environment.value === "production") .executes(...); `` |
effect | effect(name: string, fn: EffectFn): this | Declare a crash-durable effect: fn runs only after the intent to run it has been written to the run record, so a process killed anywhere inside it leaves evidence that it was owed. That evidence is what a resume uses to drive it again. Note the precondition as it stands today: a resume only picks up a run recorded suspended, and a process killed outright leaves its run running, so an effect owed by a killed process is re-driven once something moves that run back to suspended — a reaping sweep, or an operator. An effect owed by a run that suspended for any other reason is re-driven by the ordinary resume. The guarantee is at-least-once, not exactly-once: a process that dies after the side effect but before recording it will repeat the effect. Write bodies that tolerate that, either because repeating is harmless or because the far side converges (an upsert rather than an append). ``ts gate = target().dependsOn(this.checks).always() .effect("post-gate", async (ctx) => { await postCheckRun(ctx.outcomeOf("checks")?.status === "succeeded"); }); ` Pin the inputs. A re-drive happens later, sometimes much later, so a body that looks up "the current value" of anything acts on a world that has moved on. Read what the effect acts on from durable state instead — ctx.state or ctx.stateOf(...)`, written by an earlier target — which is replayed from the record and cannot be overridden from outside. A parameter is *nearly* as good and not quite: the record seeds a resume, so a parameter nobody re-supplies keeps the value the run started with, but a resume that passes one explicitly overrides it (the only way to re-supply a secret, since secrets are kept out of the record). For a value that must not drift across a re-drive, prefer state. Effects run after the body, in declaration order, and are repeatable. A target may declare effects and no body at all. Requires a state store, which is enabled automatically — an intent that cannot be recorded is a target that fails before its effect runs, by design. |
executes | executes(fn: TargetFn): this | Set the target body. May be async. |
before | before(...targets: TargetBuilder[]): this | Run before the listed targets if both are in the plan (soft ordering). |
after | after(...targets: TargetBuilder[]): this | Run after the listed targets if both are in the plan (soft ordering). |
triggers | triggers(...targets: TargetBuilder[]): this | Pull the listed targets into the plan and run them *after* this one. The inverse of dependsOn: running this target triggers the others. |
dependentFor | dependentFor(...targets: TargetBuilder[]): this | Declare this target as a prerequisite of the listed targets — the reverse of dependsOn: each listed target gains this one as a dependency, so this runs before them. Declare the listed targets above this one. |
requires | requires(...params: AnyParameter[]): this | Require that the given parameters resolve to a value before this target runs; otherwise the target fails with a message naming the missing one. Use it when a target needs a parameter that is optional build-wide. |
proceedAfterFailure | proceedAfterFailure(): this | Keep running the rest of the build even if this target fails. The build still reports failure, and this target's own dependents are skipped. |
unlisted | unlisted(): this | Hide this target from --list and --help (it can still be run by name). |
readOnly | readOnly(): this | Mark this target query-only for MCP: its run: tool advertises MCP's readOnlyHint instead of the default destructiveHint, and it is exempt from --confirm-destructive. A hint about intent only — the target still runs its real body — so declare it on targets that inspect rather than mutate (a status check, a report). |
requiresRole | requiresRole(role: string): this | The role an MCP caller must hold to run this target — the per-target half of [authorization](../../docs/mcp.md#authorization). The built-in roles are ordered read < run < operator, so an operator satisfies a requirement for run; any other name is matched exactly, so an identity provider's own group (sre, release-manager) works here without being ranked into a hierarchy it never agreed to. Only meaningful when the server authenticates its callers — a build with no mcpAuth()/mcpIdentity() is gated by --allow-run and --protect as before, and this is inert. It raises the bar for one target; it cannot lower it, so a target requiring read still needs run to be executed. ``ts promote = target().requiresRole("operator").executes(() => deployProd()); `` |
always | always(): this | Run this target even after the build has failed — for cleanup, teardown, or an aggregate that has to report on the failure. It still waits for its own dependencies, but waits for them to settle rather than to succeed: a dependency that failed releases it, the same as one that passed or was skipped. Anything else would make the modifier unusable for the case it exists for, since a target that depends on the work it is cleaning up would be held back by exactly the failure that should trigger it. Use TargetContext.outcomeOf to see what actually happened. A dependency parked at a .waitsFor(...) gate is the exception: it has not settled, so the target waits for the resume rather than reporting on a run that is still in progress. The build's overall result is unchanged — an always target that passes does not rescue a failed build. Repeatable conditions/inputs apply. |
dryRunnable | dryRunnable(): this | Run this target's body under --dry-run instead of skipping it, with the $ shell in echo mode: each command (awaited or .spawn()ed) prints its resolved argv and returns an empty success without starting a process. Opt-in, because Zuke can only intercept $/"./shell.ts".Command — any *other* side effect a body performs (writing a file, calling an API directly) still happens under a dry run. Use it for bodies that are shell-command orchestration, to preview the exact commands a real run would execute. Without it, a dry run skips the body entirely (the default). Because an echoed command returns empty stdout and exit code 0, a body whose *control flow or command arguments* depend on a command's output (await $\git rev-parse HEAD\.text(), a .code() loop) should branch on the "./executor.ts".TargetContext dryRun flag rather than trust the echoed result. |
cacheKey | cacheKey(fn: unknown): this | Contribute an extra value to this target's cache fingerprint, beyond its input files — e.g. a parameter value, tool version, or git commit. The target is up-to-date only when its inputs *and* every cache key are unchanged. The function may be async. Repeatable. ``ts compile = target() .inputs("src") .cacheKey(() => this.configuration.value) .executes(...); `` |
produces | produces(...paths: PathLike[]): this | Declare artifact files/directories this target produces (metadata). |
consumes | consumes(...targets: Array<TargetBuilder | Group>): this | Depend on the listed targets and consume their artifacts: equivalent to dependsOn for ordering, expressing that this target uses what they produces. |
whenSkipped | whenSkipped(behavior: run-dependencies | skip-dependencies): this | When this target is skipped by an onlyWhen condition, also skip its dependencies that no other planned target needs. Because the dependencies would otherwise run first, the condition is evaluated up front, so it must not depend on state produced by other targets during the run. |
timeout | timeout(ms: number): this | Fail the target if its body runs longer than ms milliseconds (per attempt). |
retry | retry(times: number, delayMs?: number): this | Retry the target body up to times more attempts on failure, optionally pausing delayMs between attempts. Combined with timeout, each attempt is bounded by the timeout. |
validateBefore | validateBefore(...validations: Validation[]): this | Run one or more Validations *before* the target body. Each runs in declaration order; the first to throw fails the target and the body never runs. Repeatable. A cached/skipped target runs no validations. ``ts deploy = target() .validateBefore(this.securityReview) // gate before deploying .executes(...); `` |
validateAfter | validateAfter(...validations: Validation[]): this | Run one or more Validations *after* the target body completes successfully. Each runs in declaration order; the first to throw fails the target. Repeatable. |
recoverWith | recoverWith(...remediations: Remediation[]): this | Attach one or more Remediations that run only if the body fails. Each is given the failure; if any returns { retry: true }, the executor re-runs the body and, when it now passes, the target succeeds. This is the hook the AI fixer in @zuke/ai uses for self-healing builds. Repeatable. ``ts test = target() .executes(() => DenoTasks.test((s) => s.allowAll())) .recoverWith(aiFixer((f) => f.provider("claude").apiKey(this.key))); `` |
recoverAttempts | recoverAttempts(times: number): this | The maximum number of fix-then-rerun cycles attempted when the body fails and recoverWith remediations are configured (default 1). Each cycle runs every remediation, then re-runs the body once; the count bounds how many times that repeats before the failure is final. Clamped to at least 1. |
lock | lock(configure: Configure<LockSettings>): this | Hold a cross-run lock while this target runs: only one run may hold key at a time, so a second run that tries to acquire it fails with a "./state/lock.ts".LockConflictError naming the current holder. The lock is released when the target settles — success, failure, or cancellation — and expires after options.ttl as a backstop should the holder be killed (a live holder renews it as it runs). key may be a thunk, evaluated after parameters resolve, so it can depend on this.<param>.value; compose composite keys with "./state/lock.ts".lockKey. Requires a state store (a build that uses .lock() gets a .zuke/runs filesystem store by default). ``ts promote = target() .lock((s) => s.lockKey("deploy", this.repo.value) .withTtl("4h") .onConflict((h) => ${this.repo.value} is being deployed by ${h.actor} (run ${h.runId}).)) .executes(...); `` |
waitsFor | waitsFor(configure: Configure<WaitSettings>): this | Suspend the run at this target until an external event occurs, then let the run be resumed later (in a different process) — a settings lambda in the same style as lock. The target is a gate (no body): when its trigger is already satisfied it passes and dependents run; otherwise the run's state is saved, the run is marked suspended, its independent branches finish, and the process exits 0. Requires a state store. ``ts awaitApproval = target() .dependsOn(this.deploy) .waitsFor((s) => s.on(externalSignal("testing-approved")) .timeout("72h") .onTimeout(() => this.rollback)); `` |
onCancel | onCancel(compensation: OnCancel): this | Register a compensation target that undoes this target's effect when the run is later cancelled (via zuke cancel <run-id>, an MCP cancel_run, or a timed-out wait). The compensation runs iff this target succeeded — a target that never ran, was skipped, or failed has nothing to undo. On cancellation, compensations run in reverse order of the targets that succeeded, so later work is unwound before the work it built on. compensation is a sibling target, or a thunk returning one (use the thunk form to reference a target declared *below* this one — class fields initialise top-to-bottom). The compensation body receives a normal TargetContext whose state exposes this target's persisted metadata, so a deploy that recorded { slot: "sit-7" } in ctx.state can be rolled back from exactly that slot. Compensation failures are recorded but do not stop the walk (cleanup is maximal). Requires a state store. On a forEach sub-target, the compensation is per item: cancel runs it for every item that had succeeded (or was still in-flight), each with its own item-scoped context — see the fan-out section of docs/orchestration.md. ``ts deploy = target() .executes((ctx) => ctx.state.set({ slot: "sit-7" })) .onCancel(() => this.rollback); rollback = target() .executes((ctx) => tearDown(ctx.state.get().slot)); // reads deploy's meta `` |
forEach | forEach(items: unknown, factory: ForEachFactory<Item>, configure?: Configure<ForEachSettings>): this | Fan out over a runtime list: for each item, build an ordered pipeline of sub-targets and run them with per-item failure isolation and bounded concurrency. items is a thunk (evaluated when the target runs, so it can read this.<param>.value); factory returns a record of sub-targets per item, each implicitly depending on the one before it. Items run concurrently, each item's stages sequentially — the pipeline model. The sub-targets are materialised at run time (named parent[item].stage) — --list/graph show only the one fan-out node — and each is a first-class target with its own status in the summary and the run record. The fan-out target fails if any item's pipeline fails. A fan-out cannot contain a wait gate: neither the fan-out target itself nor any stage may use waitsFor — a materialised sub-target has no resume path, so the gate would be silently swallowed. Combining them fails the target with guidance. Gate a fan-out by putting the wait on a separate target that the fan-out .dependsOn(...). ``ts deployBatch = target() .forEach( () => this.repos.value, // string[] (repo) => ({ checks: target().executes(() => checkDeployable(repo)), deploy: target().executes((ctx) => applyToSit(repo, ctx)), }), (s) => s.concurrency(3).continueOnItemFailure(), ); `` |
description_? | description_?: string | Human-readable summary shown in --list. |
dependsOn_ | dependsOn_: TargetBuilder[] | Hard prerequisites: these run (transitively) before this target. |
before_ | before_: TargetBuilder[] | Soft ordering: this runs before the listed targets if both are planned. |
after_ | after_: TargetBuilder[] | Soft ordering: this runs after the listed targets if both are planned. |
fn_? | fn_?: TargetFn | The target body. |
effects_ | effects_: DeclaredEffect[] | Crash-durable effects, in declaration order (set by effect). |
name_? | name_?: string | Property name, assigned during discovery. Undefined until then. |
group_? | group_?: Group | The parallel batch this target belongs to, if any (set by partOf). |
inputs_ | inputs_: string[] | Input files/directories whose contents key the cache (set by inputs). |
outputs_ | outputs_: string[] | Output files/directories that must exist for a cache hit (set by outputs). |
onlyWhen_ | onlyWhen_: Condition[] | Conditions gating execution; all must hold or the target is skipped. |
triggers_ | triggers_: TargetBuilder[] | Targets pulled in and run after this one (set by triggers). |
requires_ | requires_: AnyParameter[] | Parameters that must be set for this target (set by requires). |
proceedAfterFailure_ | proceedAfterFailure_: boolean | Continue the build if this target fails (set by proceedAfterFailure). |
always_ | always_: boolean | Run even after the build has failed (set by always). |
unlisted_ | unlisted_: boolean | Hide this target from --list/--help (set by unlisted). |
readOnly_ | readOnly_: boolean | Advertise this target as query-only over MCP (set by readOnly). |
requiresRole_? | requiresRole_?: string | The role an MCP caller needs to run this target (set by requiresRole). |
dryRunnable_ | dryRunnable_: boolean | Run this target's body under --dry-run with $ echoed (set by dryRunnable). |
cacheKeys_ | cacheKeys_: Array<unknown> | Extra cache-key contributors beyond input files (set by cacheKey). |
produces_ | produces_: string[] | Artifact paths this target produces (set by produces). |
skipDependencies_ | skipDependencies_: boolean | When skipped by a condition, also skip dependencies (set by whenSkipped). |
timeout_? | timeout_?: number | Per-attempt timeout in milliseconds, if set by timeout. |
retries_ | retries_: number | Number of extra attempts on failure, set by retry. |
retryDelay_ | retryDelay_: number | Delay between retry attempts in milliseconds. |
validateBefore_ | validateBefore_: Validation[] | Validations run before the body (set by validateBefore). |
validateAfter_ | validateAfter_: Validation[] | Validations run after the body (set by validateAfter). |
recoverWith_ | recoverWith_: Remediation[] | Remediations run after the body fails (set by recoverWith). |
recoverAttempts_ | recoverAttempts_: number | Max fix-then-rerun cycles when the body fails (set by recoverAttempts). |
lock_? | lock_?: Configure<LockSettings> | Cross-run lock settings lambda, set by lock and run after params resolve. |
waitsFor_? | waitsFor_?: Configure<WaitSettings> | External-event wait settings lambda, set by waitsFor and run when reached. |
forEach_? | forEach_?: ForEachSpec | Fan-out spec, set by forEach: materialises per-item sub-target pipelines. |
onCancel_? | onCancel_?: unknown | Compensation thunk, set by onCancel: runs on cancel iff this target succeeded. |
interface TargetContext The context passed to every target body. Optional to receive — an existing zero-argument .executes(() => …) stays valid, since a zero-argument function is assignable to this one-parameter type — but a body that wants the run's identity, a cancellation signal, or durable per-target state reads them here.
| Member | Signature | Doc |
|---|---|---|
stateOf | stateOf(target: string): TargetStateHandle | The durable state handle of another target in this run — the seam a body reads a dependency's published metadata through (e.g. the result a .waitsFor(githubWorkflow(...)) gate recorded to its state). stateOf(this target) is equivalent to state. It reads the run's current record, so it sees writes a dependency made earlier in the run — including across a suspend/resume, since the record is durable. |
outcomeOf | outcomeOf(target: string): TargetOutcomeView | undefined | What another target in this run did, or undefined if it has no outcome yet — it has not run, or is running now. The seam for a target that must decide on the run's results rather than merely follow them: an aggregate gate reporting one verdict for a fan of checks, some of which are allowed to fail. .dependsOn(...) alone cannot express that, because a failed dependency never lets its dependents run; pair this with .always() and .proceedAfterFailure() on the checks. It reads what this run has settled so far, whichever process settled it: outcomes from a previous process come back after a resume, because they are in the durable record. A sibling running concurrently has no outcome yet — depend on what you intend to read. |
outcomes | outcomes(): ReadonlyMap<string, TargetOutcomeView> | Every outcome this run has settled so far, keyed by dotted target name — a snapshot, not a live view. Targets that have not settled are absent rather than present with a placeholder status. |
plan | plan(): RunPlan | The resolved shape of this run — which targets it plans, and how they relate. The seam for a body whose work depends on what *else* was asked for: a build that signs its artifact only when deploy is in the run. Reports the plan, not the outcome — see "./run_plan.ts".RunPlan. |
reportSummary | reportSummary(pairs: SummaryPairs): void | Report key: value notes into this target's row of the end-of-build summary — where a count or a version belongs once the body is done: ``text test Succeeded 8.1s // Tests: 837 · Passed: 837 · Failed: 0 ` Notes accumulate across calls, and reporting a key again replaces its value in place. Each key and value is rendered on one line (whitespace collapsed, control sequences removed). Library code with no context in hand — a tool wrapper reporting the counts its tool printed — reports through the ambient test` row still says how many failed. A compensation (see TargetBuilder.onCancel) has no row, so its calls are dropped. |
runId | runId: string | Unique ID of this run, stable for every target in the run. |
initiator? | initiator?: RunInitiator | Who asked for this run — stamped once when the run was created, and unchanged by any later resume, so it still names the engineer who started a deploy that a sweep has since picked up several times. Absent when the run has no durable record to have stamped one (no state store), and on a record written before the field existed. |
target | target: string | Dotted name of the executing target. |
signal | signal: AbortSignal | Aborted when the run is cancelled (see "./executor.ts".ExecuteOptions signal). Pass it to a shell command's .signal() to have that command terminated on cancellation; the executor also applies it as the shell's ambient default, so a plain $ in the body is terminated too. |
state | state: TargetStateHandle | Durable per-target metadata. Persisted to the run's state store when one is configured (see "./state/store.ts".StateStore), and an in-memory no-op otherwise. The carrier for state that must survive across a suspend/resume boundary — do not put secrets in it. |
signals | signals: ReadonlyMap<string, SignalRecord> | Payloads of the external signals received so far, keyed by name (see .waitsFor(...) and "./wait.ts".externalSignal). Empty until a signal is delivered by zuke resume <id> --signal <name>. |
dryRun | dryRun: boolean | True when the run is a dry run (bodies do not execute under a dry run). |
type TargetFn = unknown The executable body of a target. May be synchronous or asynchronous, and any returned value is ignored — so a body can return a tool-wrapper call directly (.executes(() => DenoTasks.lint()), which resolves to a CommandOutput) without wrapping it in an async block just to discard the result. A single returned promise is awaited before dependents run; a returned *array* of promises is not (it is not a thenable), so await Promise.all([...]) inside the body when you fan work out, rather than returning the array.
interface TargetOutcomeView What another target in this run did, as TargetContext.outcomeOf reports it.
The status is the record's vocabulary, not the summary's: a target whose body ran and one served from the cache both read "succeeded", because that is the distinction the durable record keeps. A body branching on this wants "did it work", which both answer the same way.
| Member | Signature | Doc |
|---|---|---|
status | status: TargetRunStatus | The target's status: succeeded, failed, skipped, waiting, … |
error? | error?: string | The failure's message, when it failed. Redacted like every stored string. |
startedAt? | startedAt?: string | When it started, ISO-8601, if it did. |
endedAt? | endedAt?: string | When it settled, ISO-8601, if it has. |
summary? | summary?: unknown | The notes it reported into its row of the build summary (see TargetContext.reportSummary), when it reported any — so an aggregating target can read a dependency's test counts, not only its verdict. Durable: present after a resume too. |
interface TargetOverride An operator's decision to settle a target without running it — recorded on the run so the executor honours it and the trail says who decided.
Two shapes of intervention, both of which a build cannot express itself: skipped takes a step off the plan that cannot succeed, and succeeded marks one that a person completed by hand. Dependents proceed either way; the difference is what a later cancellation compensates, since only the second asserts that the target's effects exist.
| Member | Signature | Doc |
|---|---|---|
outcome | outcome: ForcedOutcome | What the target settles to when the executor reaches it. |
actor | actor: string | Who forced it (a resolved actor). |
at | at: string | ISO-8601 time the override was recorded. |
reason? | reason?: string | Why, when the operator gave a reason. |
interface TargetReport One row of the end-of-build summary.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The target's name. |
status | status: TargetStatus | The target's terminal status. |
ms | ms: number | The target's wall-clock duration in milliseconds. |
summary? | summary?: SummaryEntry[] | The notes the target reported into its row (see "./target.ts".TargetContext.reportSummary) — present only when it reported at least one, so a note-less row stays { name, status, ms }. |
interface TargetRunState The recorded progress of a single target.
| Member | Signature | Doc |
|---|---|---|
status | status: TargetRunStatus | The target's current status within the run. |
meta | meta: Record<string, JsonValue> | Durable metadata written via "../target.ts".TargetStateHandle. |
startedAt? | startedAt?: string | ISO-8601 timestamp when the body started, if it has. |
endedAt? | endedAt?: string | ISO-8601 timestamp when the target settled, if it has. |
error? | error?: string | The failure message when status is failed. |
waitingFor? | waitingFor?: WaitState | The pending wait when status is waiting (set by .waitsFor(...)). |
effects? | effects?: Record<string, EffectState> | The declared effects of this target, keyed by effect name — present only once at least one has been armed. |
summary? | summary?: SummaryEntry[] | The notes the target reported into its row of the build summary (see "../target.ts".TargetContext.reportSummary), in the order they were first reported — present only when it reported at least one, and redacted like every other stored string. What lets zuke runs show and ctx.outcomeOf say a target ran 4094 tests, not only that it succeeded. |
type TargetRunStatus = pending | running | waiting | succeeded | failed | skipped The status of one target within a run record. waiting (a suspended external-event wait) is produced only from a later milestone; the executor records the others.
interface TargetStateHandle A target's durable, per-target metadata, surfaced on TargetContext as state. Writes are persisted to the run's state store (see "./state/store.ts".StateStore) and are visible to later runs — e.g. a resuming process reading what a suspended target recorded. When no store is configured, the handle is an in-memory no-op scoped to the current run.
Never store a secret here — state is persisted in plain JSON and read back by later runs and by zuke runs show.
| Member | Signature | Doc |
|---|---|---|
set | set(patch: Record<string, JsonValue>): Promise<void> | Merge a JSON patch into this target's persisted metadata (awaits the write). |
trySet | trySet(patch: Record<string, JsonValue>): Promise<boolean> | set, reporting whether the patch was recorded: true when it reached the store, false when the write was dropped. A write can be dropped — conflicted away for good, or refused by a store that errored — and set resolves the same either way, so a body that needs the value to be durable cannot tell. This is the seam for the cases that do need to know: before an irreversible step that depends on the value, or before handing a compensation something it will have to read back. Do not read false as "it may still land": a dropped write is sometimes re-persisted by a later one, but nothing guarantees it, so treat false as not recorded. A dropped write also warns, and one that is definitely unrecoverable marks the run "./state/types.ts".RunRecord.degraded so a later resume knows the record is missing something. A handle with nothing durable behind it always answers true — a build with no state store, and a compensation, whose state is in-memory by design. Nothing is persisted, but nothing is dropped either. |
get | get(): Record<string, JsonValue> | Read this target's persisted metadata (from prior attempts/runs too). |
type TargetStatus = passed | failed | skipped | cached | waiting The outcome of a single target, reported in the summary and lifecycle hooks. waiting marks a .waitsFor(...) gate whose event has not occurred — the run suspends there.
interface TargetTiming Timing for a settled target, passed to Plugin.onTargetEnd.
| Member | Signature | Doc |
|---|---|---|
runId | runId: string | The run id (see RunInfo). |
durationMs | durationMs: number | The target's wall-clock duration in milliseconds (0 for skipped/cached). |
async function tcpReachable(address: string): Promise<boolean> Whether a TCP host:port is accepting connections — the usual readiness probe for a server. Resolves true once a connection succeeds (it is closed immediately), false while the port is still refused/unreachable, so it plugs straight into ServiceBuilder.readyWhen.
.readyWhen(() => tcpReachable("localhost:5432"))class TeamsAnnouncementSettings extends AnnouncementSettings Fluent settings for AnnounceTasksApi.teams. Bot mode (.bot().token(t).team(id).channel(c)) posts through Microsoft Graph with a bearer token.
| Member | Signature | Doc |
|---|---|---|
team | team(team: string): this | Set the Teams team (group) id to post to in bot mode (Microsoft Graph). |
interface TestCounts The counts a test run produced — the one shape every test-runner wrapper maps its runner's own summary line onto, so DenoTasks.test, VitestTasks.run, JestTasks.run and the rest all put the same labels on their rows. passed and failed are always known; the rest are the optional categories a runner may or may not have, left out when it has none.
| Member | Signature | Doc |
|---|---|---|
passed | passed: number | Tests that passed. |
failed | failed: number | Tests that failed. |
skipped? | skipped?: number | Tests the run selected but did not execute: skipped, ignored, pending. |
todo? | todo?: number | Tests marked as still to be written. |
flaky? | flaky?: number | Tests that failed and then passed on a retry (Playwright's "flaky"). |
function tokenize(strings: ReadonlyArray<string>, values: ReadonlyArray<Interpolatable>): string[] Tokenise a tagged-template invocation into an argv array.
Literal whitespace separates arguments; interpolated values are appended as atomic tokens (so --flag=${x} and pre${x} work), and arrays expand to one argument per element. Interpolated values are never re-split on whitespace, which is what keeps command construction injection-free.
function toolchain(configure?: unknown): Toolchain Create a Toolchain. Configure it inline with a callback, or chain Toolchain.tool on the returned instance.
const tools = toolchain((t) =>
t.tool((s) => s.name("helm").url(helmUrl))
.tool((s) => s.name("kubectl").url(kubectlUrl))
);class Toolchain A declared set of external tools. Add tools with Toolchain.tool (a ToolInstallSettings lambda) and fetch them all with Toolchain.install. Build one with toolchain.
| Member | Signature | Doc |
|---|---|---|
tool | tool(configure: Configure<ToolInstallSettings>): this | Add a release tool, configured through a settings-lambda. Chainable. |
tree | tree(configure: Configure<ToolInstallSettings>): this | Add a multi-file runtime tree (see ToolTasksApi.installTree), configured through a settings-lambda with .strip(...)/.bins(...). In install's result its entry is the extracted tree's root — a callable AbsolutePath, so root("bin") is the directory to put on PATH. Chainable. |
npm | npm(spec: NpmToolSpec): this | Add an npm-registry package to provision as a version-pinned tool — installed under <destDir>/npm/<name>@<version> and keyed in install's result by its NpmToolSpec.name. See installNpmTool. Chainable. |
tools | tools(): unknown | The configured release tools, in declaration order. |
trees | trees(): unknown | The configured runtime trees, in declaration order. |
npmTools | npmTools(): unknown | The configured npm-package tools, in declaration order. |
install | install(options?: ToolchainInstallOptions): Promise<Map<string, AbsolutePath>> | Install every declared tool concurrently — reusing a cached copy where a release tool's or tree's pinned checksum, or an npm tool's name@version marker, matches — and return a map of tool name to installed AbsolutePath. A tree's entry is its extracted root directory. |
interface ToolchainInstallOptions Options for Toolchain.install.
| Member | Signature | Doc |
|---|---|---|
destDir? | destDir?: PathLike | Where tools without their own destDir install. Defaults to .zuke/tools. |
download? | download?: DownloadFn | The download implementation for every release tool (defaults per installRelease). |
npmRun? | npmRun?: NpmRunner | The npm-install runner for npm-package tools (defaults to the ambient npm; a test seam). |
class ToolInstallSettings Fluent settings for installing a release tool. Configure it in a settings-lambda ((s) => s.name(...).url(...)), the same shape as Zuke's tool wrappers. name and url are required; everything else is optional and mirrors InstallReleaseOptions.
| Member | Signature | Doc |
|---|---|---|
name | name(name: string): this | The tool name; also the installed binary's filename (.exe on Windows). |
url | url(resolve: unknown): this | Resolve the download URL for the target Platform. |
destDir | destDir(dir: PathLike): this | The directory to install the binary into (created if missing). |
archive | archive(format: DownloadFormat | unknown): this | Treat the download as a "tar.gz" or "zip" to unpack (default "raw", the bare binary). Pair with binaryPath for the binary inside. Pass a (platform) => format resolver when the format is per-platform, as it is for most Go and Rust releases — .tar.gz on Linux and macOS, .zip on Windows (see InstallReleaseOptions.archive). |
binaryPath | binaryPath(path: string | unknown): this | For an archive, the binary's path within it (defaults to the name). Also accepts a (platform) => path resolver, for the usual case of a .exe inside the Windows archive only. |
strip | strip(components: number): this | For a tree install (ToolTasksApi.installTree / Toolchain.tree), drop this many leading path components while unpacking — 1 unwraps a release tarball's tool-v1.2.3/ directory. Ignored by a single-binary install. |
bins | bins(...paths: string[]): this | For a tree install, the paths (relative to the stripped root) to mark executable on POSIX — a runtime's bin/node, bin/npm, … Ignored by a single-binary install. |
checksum | checksum(sha256: string | unknown): this | The expected SHA-256 (hex) of the downloaded artifact — verifies and caches the install. Pass a ({ os, arch }) => string resolver to pin it per platform (see InstallReleaseOptions.checksum). |
platform | platform(platform: InstallPlatform): this | Resolve for a specific platform instead of the host (a foreign install). |
download | download(fn: DownloadFn): this | Override the downloader (defaults to an HTTPS download; a test seam). |
options_ | options_(fallbackDestDir: PathLike): InstallReleaseOptions | Build the InstallReleaseOptions, using fallbackDestDir when no destDir was set. Throws if a required field is missing. |
treeOptions_ | treeOptions_(fallbackDestDir: PathLike): InstallTreeOptions | Build the InstallTreeOptions for a tree install, using fallbackDestDir when no destDir was set. A tree always ships packed, so the archive defaults to "tar.gz" and "raw" is rejected. Throws if a required field is missing. |
name_? | name_?: string | The tool name, and the installed filename. Set by name. |
url_? | url_?: unknown | Resolves the per-platform download URL. Set by url. |
destDir_? | destDir_?: PathLike | Install directory (overrides the toolchain's). Set by destDir. |
archive_? | archive_?: DownloadFormat | unknown | Download format (or a per-platform resolver). Set by archive. |
binaryPath_? | binaryPath_?: string | unknown | The binary's path within an archive (or a resolver). Set by binaryPath. |
strip_? | strip_?: number | Leading path components to strip on a tree install. Set by strip. |
bins_? | bins_?: string[] | Executable bins within a tree install. Set by bins. |
checksum_? | checksum_?: string | unknown | Expected SHA-256 (or a per-platform resolver). Set by checksum. |
platform_? | platform_?: InstallPlatform | The platform to resolve for. Set by platform. |
download_? | download_?: DownloadFn | The download implementation. Set by download. |
class ToolNotFoundError extends Error Raised when a tool's binary cannot be found on the system.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The error name. |
type ToolResolution = node_modules | path How ToolSettings.run locates a wrapper's binary when no explicit ToolSettings.toolPath is set:
- "path" — spawn the bare tool name and let the OS resolve it on PATH (the default, matching a native/global install); - "node_modules" — npx-style: walk up from the working directory looking for node_modules/.bin/<tool>, falling back to PATH on a miss (so a package hoisted to a monorepo root runs with no .toolPath()).
class ToolSettings Abstract fluent base for tool settings. Subclasses provide the binary (defaultTool) and the pure subcommand argv (buildArgs); the base provides the shared chainers and run.
| Member | Signature | Doc |
|---|---|---|
env | env(record: Record<string, string>): this | Merge additional environment variables for the process. |
cwd | cwd(path: PathLike): this | Set the working directory for the process. |
noThrow | noThrow(): this | Do not throw on a non-zero exit; inspect code on the output instead. |
throwsOnError | throwsOnError(): boolean | Whether a failure should throw — the default, or false after noThrow. A task that layers its own validation on top of the subprocess (e.g. a coverage-threshold gate) reads this to decide whether a gate failure throws or is merely reported. |
quiet | quiet(): this | Suppress live stdout/stderr streaming to the terminal. |
killAfter | killAfter(ms: number): this | Kill the tool if it runs longer than ms milliseconds, raising a CommandTimeoutError. Fires even under noThrow. |
maxCapturedBytes | maxCapturedBytes(bytes: number): this | Cap how much of each captured stream the run keeps in memory, in bytes (default 8 MiB). Once the cap is reached the oldest bytes are dropped, CommandOutput.truncated is set, and CommandOutput.text prefixes a notice. Raise it for a tool whose whole output must be parsed — a coverage report, a --json dump — and lower it to bound a chatty one. Live streaming to the terminal is never capped. |
toolPath | toolPath(path: PathLike): this | Override the binary to run (e.g. an absolute path to the tool). |
fromNodeModules | fromNodeModules(): this | Resolve the binary npx-style: walk up from the working directory looking for node_modules/.bin/<tool>, falling back to PATH on a miss. Overrides both the wrapper default and the ambient ZUKE_TOOL_RESOLUTION. Has no effect once toolPath is set (an explicit path always wins). |
fromPath | fromPath(): this | Resolve the binary from PATH only, ignoring any node_modules/.bin. |
args | args(...extra: Array<string | number | AbsolutePath>): this | Escape hatch: append raw arguments after all typed options. |
argv | argv(): string[] | The full argv (binary first). Pure — useful for tests and diagnostics. |
resolvedArgv | resolvedArgv(): string[] | The argv run will actually spawn — like argv, but with the node_modules/.bin resolution applied (so it performs I/O). Useful for tests and diagnostics: it reveals whether a wrapper resolved to a local shim or fell back to the bare name on PATH. |
run | run(): Promise<CommandOutput> | Run the configured tool, raising a ToolNotFoundError naming it when the binary is missing. The argv is spawned as it was resolved, on every platform. Windows batch shims used to be wrapped in cmd /c here, which silently gave up the argv-boundary guarantee every wrapper relies on: Deno.Command hands cmd.exe a single command line built with C-runtime quoting, which quotes on spaces but not on &, | or ^, so cmd.exe re-parsed an operand like A=1&whoami as a second command. Spawning the shim itself instead keeps that decision where it belongs: Deno resolves a bare name through PATHEXT and launches a .cmd/.bat through the command processor with quoting hardened for it, so the operand stays one argument. |
os_ | os_: Deno.build.os | The platform identifier used when resolving a tool from node_modules, which on Windows means looking for the .cmd shim rather than the bare name. In production this is always Deno.build.os. It is exposed as a public field — rather than read from Deno.build.os inline — so that tests can pin a specific platform without spawning a subprocess or touching the environment: ``ts const s = new MyToolSettings(); s.os_ = "windows"; // resolve as Windows would, on any host `` The trailing underscore signals an internal test seam: do not rely on this field in production code. |
type ToolTask = unknown A ready-to-run task for a defineTool tool.
const ToolTasks: ToolTasksApi Provision external CLIs from a build. ToolTasks.install((s) => …) fetches a single release binary and ToolTasks.npm(...) a single npm package; group several of either with toolchain.
interface ToolTasksApi The task surface of ToolTasks.
| Member | Signature | Doc |
|---|---|---|
install | install(configure: Configure<ToolInstallSettings>): Promise<AbsolutePath> | Install a single release tool, configured through a ToolInstallSettings lambda, and resolve to its installed path. Defaults the install directory to .zuke/tools. |
installTree | installTree(configure: Configure<ToolInstallSettings>): Promise<AbsolutePath> | Install a multi-file runtime tree (Node.js, a JDK, …) from one archive, configured through a ToolInstallSettings lambda, and resolve to the extracted tree's root AbsolutePath. Because that path is callable, root("bin", "node") is a binary and root("bin") a directory to put on PATH. Use .strip(...) and .bins(...); defaults the install directory to .zuke/tools. See installTree; group several with Toolchain.tree. |
npm | npm(spec: NpmToolSpec, options?: InstallNpmToolOptions): Promise<AbsolutePath> | Provision a single npm-registry package as a version-pinned, cached tool and resolve to its installed bin path. Defaults the install root to .zuke/tools. See installNpmTool; group several with Toolchain.npm. |
const UNAUTHORIZED: McpAuthReject The bare 401 challenge: the refusal an authenticator's own failure produces (so a throw leaks nothing about why it threw), and the one the transport answers an absent static bearer token with. A token that was presented and rejected gets INVALID_TOKEN instead: the two are different facts about the caller, and only the second one is about a credential.
function untar(archive: Uint8Array): TarEntry[] Extract the entries from a tar archive — regular files, symlinks, and directories. A path longer than the 100-byte name field is reconstructed from whichever long-name form the archive uses: the POSIX ustar prefix split, GNU tar's @LongLink pseudo-entries (typeflags 'L' name / 'K' link target, whose *data* is the following member's value — Node's Linux release tarballs use this), or pax extended headers (typeflag 'x', with path=/linkpath= records — bsdtar/macOS use this). These metadata pseudo-entries accumulate onto the next real member, matching GNU/bsdtar, so a mixed archive is read correctly; a pax record wins over a GNU long name, which wins over the header's own fields.
async function unzip(archive: Uint8Array): Promise<TarEntry[]> Read the entries of a .zip archive, decompressing stored and deflate members. The central directory is the source of truth. Directory entries (a trailing /) are skipped. Encrypted, zip64, or otherwise-compressed entries throw a friendly error naming the offending entry, and a header or data field that runs past the archive is reported as a malformed zip (not a raw out-of-bounds error). Every offset read from the archive is bounds-checked; for integrity against a tampered download, pin a .checksum(...), which is verified before the archive is ever parsed.
function validateGraph(targets: Map<string, TargetBuilder>): void Validate the whole graph: unknown references first, then cycles.
interface Validation A check plugged into a target with TargetBuilder.validateBefore or TargetBuilder.validateAfter. The target decides *when* it runs; the validation decides *what* it checks. Throw from Validation.validate to fail the target (and break the build). Implemented, for example, by the AI reviewers in @zuke/ai, but any object with a validate method qualifies.
| Member | Signature | Doc |
|---|---|---|
validate | validate(context: ValidationContext): void | Promise<void> | Run the check; throw to fail the target. May be async. |
name? | name?: string | A name for diagnostics (optional). |
interface ValidationContext Context passed to a Validation when it runs.
| Member | Signature | Doc |
|---|---|---|
redact | redact(text: string): string | Mask every resolved secret parameter in text. A validation that publishes anywhere — a pull-request comment, a review thread, a job summary, a file it writes — has to run its output through this first. What it is publishing is typically a model's assessment of a diff, or of a failure, and the prompt behind it carries the command output that a secret the build holds can appear in; the secret then comes back in the reply. Handed over rather than left for the validation to find, for the same reason RemediationContext.redact is: the validation that most needs it lives in another package and cannot reach core's internals. Zuke's own reporter and run record redact what *they* emit, but a validation that posts over the network goes through neither. Masks the same values "./params.ts".parameter marked secret, so a credential the build never declared is not covered — declare it. |
target | target: string | The name of the target the validation is attached to. |
function visibleWidth(text: string): number The printable width of text, ignoring any ANSI colour codes it carries.
interface WaitContext The durable context a WaitTrigger may use while deciding whether its event has occurred. Its WaitContext.state handle is the awaiting target's persisted metadata — it survives a suspend/resume, even across processes — so a stateful trigger (e.g. "dispatch a GitHub workflow, then poll it") can remember what it started and hand a result to the target's body. The built-in triggers ignore it.
| Member | Signature | Doc |
|---|---|---|
state | state: TargetStateHandle | The awaiting target's durable state handle (the same one its body receives as ctx.state). Reads and writes here persist with the run and are visible to a later resume in another process. |
runId | runId: string | The run id — stable across a resume, so a natural correlation key. |
target | target: string | The awaiting target's dotted name. |
type WaitDisposition = fail | cancel-run | unknown What a timed-out wait does: fail, cancel the run, or run a compensation target.
class WaitSettings Fluent configuration for TargetBuilder.waitsFor: .waitsFor((s) => s.on(externalSignal("approved")).timeout("72h")). Set the trigger, an optional WaitSettings.timeout, and an optional WaitSettings.onTimeout disposition. The lambda runs when the target is reached, so the trigger may read this.<param>.value.
| Member | Signature | Doc |
|---|---|---|
on | on(trigger: WaitTrigger): this | Set the "./wait.ts".WaitTrigger the wait is satisfied by. |
timeout | timeout(duration: string | number): this | Give the wait a deadline (a duration like "72h" or milliseconds). |
onTimeout | onTimeout(disposition: OnTimeout): this | What to do when the deadline passes: a thunk returning a sibling compensation target (a thunk, so it can reference a target declared *below* this one), or the string "fail" / "cancel-run". Defaults to "fail". |
trigger_? | trigger_?: WaitTrigger | The trigger deciding when the wait is satisfied; set by on. |
timeout_? | timeout_?: string | number | The deadline duration (string or ms); set by timeout. |
onTimeout_? | onTimeout_?: OnTimeout | The timeout disposition thunk; set by onTimeout. |
interface WaitState The pending wait recorded on a suspended target (see TargetRunState.waitingFor).
| Member | Signature | Doc |
|---|---|---|
trigger | trigger: string | A human-readable descriptor of what is awaited (e.g. signal:approved). |
deadline? | deadline?: string | ISO-8601 deadline after which onTimeout applies, if a timeout was set. |
onTimeout | onTimeout: WaitDisposition | What happens when the deadline passes. |
interface WaitTrigger Decides whether the event a target waits for has occurred. descriptor is a short, JSON-safe label recorded on the suspended target; isSatisfied is evaluated against the run's received signals (and a durable WaitContext) when the target is reached and again on each resume attempt.
| Member | Signature | Doc |
|---|---|---|
isSatisfied | isSatisfied(signals: ReadonlyMap<string, SignalRecord>, context: WaitContext): boolean | Promise<boolean> | Whether the awaited event has occurred, given the run's received signals and a durable WaitContext. The context lets a trigger persist correlation state across a suspend/resume; a trigger that only inspects signals may ignore it (fewer parameters stay assignable). |
descriptor | descriptor: string | A short label recorded on the wait (e.g. signal:approved). |
pollIntervalMs? | pollIntervalMs?: number | Poll interval hint (ms) for predicate triggers driven by zuke resume --check. |
function windowsCmdShim(argv: ReadonlyArray<string>, os: Deno.build.os): string[] On Windows, wrap a resolved .cmd/.bat shim in cmd /c. Returns argv unchanged on other platforms or when the binary is not a batch shim.
interface WrapperConformanceOptions Options for assertWrapperConformance.
| Member | Signature | Doc |
|---|---|---|
resolution | resolution: ToolResolution | The resolution strategy the wrapper must use when nothing overrides it: "node_modules" for a JS-ecosystem tool installed under node_modules, "path" for a natively installed one. Required, with no default: an npm-distributed wrapper that forgot to override defaultResolution() is exactly the bug this kit exists to catch, and a default would let that wrapper's test pass by saying nothing. |
const ZUKE_ACTION: zuke-build/zuke The name a CiPinResolver is asked for the prelude action, so a repository that pins its own actions can pin this one the same way.