@zuke/canary

← API reference

Canary releases for Zuke builds. `canary(...)` turns a settings lambda into

Symbols

#

canary

function
function canary(configure: Configure<CanarySettings>): Canary

Declare a canary rollout. The lambda runs when the build is constructed and decides which targets exist; a misconfigured one throws there, naming the fix.

class Deploy extends Build {
  rollout = canary((c) =>
    c.platform(myPlatform)
      .steps(10, 50)
      .bake("10m")
      .analysis(httpProbe((h) => h.url("https://api.example.com/healthz")))
      .lock((l) => l.lockKey("deploy", "api").withTtl("24h"))
  );
  ship = target().dependsOn(this.rollout.promote).executes(() => {});
}
#

Canary

interface
interface Canary

The targets canary(...) creates. Assigned to a build field — rollout = canary(…) — they are named under it: rollout.stage, rollout.step1.expose, rollout.promote, rollout.abort. Depend on this.rollout.promote to run after a successful rollout.

MemberSignatureDoc
stage stage: TargetBuilder Puts the candidate in place at 0 % and takes the rollout's lock.
approve? approve?: TargetBuilder The external-signal gate before promotion, when one is configured.
promote promote: TargetBuilder Sends all traffic to the candidate. Re-driven by a resume.
abort abort: TargetBuilder The rollback: every failure path runs it, and it can be run by hand.
soak? soak?: CanaryPhaseTargets The single phase of a canary with no steps, when it bakes or analyses.
#

CanaryAbortContext

interface
interface CanaryAbortContext

What the engine hands CanaryPlatform.abort: the context, and why.

MemberSignatureDoc
reason reason: CanaryAbortReason Why abort runs — see CanaryAbortReason.
#

CanaryAbortReason

type
type CanaryAbortReason = rollback | hand-run

Why abort runs:

- "rollback" — the engine is undoing this rollout: a stage that failed or was cancelled, a failed step, zuke cancel. With nothing in state, nothing was staged, so there is nothing to undo. - "hand-run" — rollout.abort was run on its own, outside any rollout. With nothing in state, the platform falls back to the stable version its own configuration names.

#

CanaryAnalysis

interface
interface CanaryAnalysis

A health check run against a canary: throw to fail it, which rolls the rollout back. Any core Validation is one, since a function that accepts a plain ValidationContext accepts this richer context too.

MemberSignatureDoc
validate validate(context: CanaryAnalysisContext): void | Promise<void> Run the check; throw to fail it.
name? name?: string A name for diagnostics (optional).
#

CanaryAnalysisContext

interface
interface CanaryAnalysisContext

What an analysis is handed: everything a core ValidationContext carries — the target's name, the run's redactor, and a signal — plus where the rollout stands.

MemberSignatureDoc
step step: number The 1-based step being analysed; 0 for a canary with no steps.
requested requested: number The exposure this step asked for, in percent.
exposure exposure: number The exposure the platform reported achieving, in percent.
#

CanaryContext

interface
interface CanaryContext

What the engine hands a CanaryPlatform on each call.

state is the one place a platform keeps the handles a later call needs — the revision it staged, the stable one to return to. Every call of one rollout gets the same durable state, so abort can read what stage wrote, even in another process after a resume. Never put a secret in it: it is persisted in plain JSON and shown by zuke runs show.

MemberSignatureDoc
reportSummary reportSummary(pairs: SummaryPairs): void Add key/value pairs to the calling target's row in the build summary.
state state: TargetStateHandle The rollout's durable platform state, shared by every call.
signal signal: AbortSignal Aborted when the run is cancelled. Pass it to any network call. A compensation (abort during a rollback) is handed one that never aborts: the rollback is finished, not interrupted.
#

CanaryPhaseTargets

interface
interface CanaryPhaseTargets

The targets of one phase of a rollout.

MemberSignatureDoc
expose? expose?: TargetBuilder Sets the step's exposure; absent for the soak of a canary with no steps.
bake? bake?: TargetBuilder Waits out the bake; absent when the phase does not bake.
analyze? analyze?: TargetBuilder Runs the analyses once at the end; absent when there are none.
#

CanaryPlatform

interface
interface CanaryPlatform

A place a canary runs. The engine stages the candidate, moves its exposure step by step, and then promotes it or aborts it — through these calls only.

Rules the engine relies on:

- expose is absolute (set exposure to percent, not add to it) and returns the exposure actually achieved, which may differ on a "replicas" platform. A repeated call with the same value is harmless — a resumed run repeats one that was interrupted. - promote and abort are idempotent: either can run twice. - abort must also work with empty state. A rollback reads the state stage wrote, but zuke <rollout>.abort run by hand is a fresh run that has none, so abort falls back to what its own configuration says the stable version is. It does so only when ctx.reason is "hand-run": a "rollback" with empty state means nothing was staged, and moving traffic to the configured stable version would change a service no rollout touched. So stage records what a rollback needs before it changes anything — with ctx.state.trySet(...), stopping when it returns false — or a rollback after a lost write would find nothing to undo.

MemberSignatureDoc
describe describe(): string A short description for the build summary ("Cloud Run service api").
stage stage(ctx: CanaryContext): Promise<void> Put the candidate in place at 0 % exposure; record handles in ctx.state.
expose expose(percent: number, ctx: CanaryContext): Promise<number> Set exposure to percent (0–100) and return the exposure achieved.
promote promote(ctx: CanaryContext): Promise<void> Send all traffic to the candidate and retire the old version.
abort abort(ctx: CanaryAbortContext): Promise<void> Return all traffic to the stable version and remove the candidate.
exposure exposure: ExposureKind How this platform measures exposure.
#

CanarySettings

class
class CanarySettings

How a canary rolls out, configured through canary((c) => …):

rollout = canary((c) =>
  c.platform(myPlatform)
    .steps(10, 25, 50)
    .bake("10m")
    .analysis(httpProbe((h) => h.url(HEALTH_URL)))
    .approval("canary-approved")
    .lock((l) => l.lockKey("deploy", "api").withTtl("24h"))
);

The lambda runs once, when the build is constructed, because it decides which targets exist. Anything that reads a parameter — an image tag, a URL — belongs inside the platform's or the analysis's own lambda, which runs when the target does.

MemberSignatureDoc
platform platform(platform: CanaryPlatform): this Where the canary runs: a platform adapter, or a hand-written object.
steps steps(...percents: number[]): this The exposure after each step, in percent and increasing — steps(10, 25, 50) exposes 10 %, then 25 %, then 50 %, and promotion takes it to 100 %. A "channel" platform has no percentage and declares no steps.
bake bake(duration: string | number): this How long every step bakes before its final analysis ("10m", or ms).
bakeStep bakeStep(step: number, duration: string | number): this How long one step (1-based) bakes, overriding bake for it.
analysis analysis(...analyses: CanaryAnalysis[]): this Health checks run against each step: on an interval while it bakes (inline bakes only) and once when it ends. Any core Validation works.
analysisInterval analysisInterval(duration: string | number): this How often an inline bake runs its analyses (default "1m").
approval approval(signal: string): this Wait for an external signal before promoting — zuke resume <run-id> --signal <name> delivers it.
approvalTimeout approvalTimeout(duration: string | number): this How long the approval gate waits before rolling the canary back.
lock lock(configure: Configure<LockSettings>): this Lock the rollout for the whole run, from staging to promotion or rollback — a core lock settings lambda, held with holdForRun(). Set its TTL to cover the longest the run can stay parked at a gate.
durable durable(): this Bake by suspending the run instead of sleeping in the process: each bake becomes a gate that zuke resume --check reopens once its time is up. Needs a state store. In-flight analyses do not run (no process is there to run them); each step's final analysis does.
platform_? platform_?: CanaryPlatform Where the canary runs (set by platform).
steps_ steps_: number[] The exposure after each step, in percent (set by steps).
bake_? bake_?: string | number How long each step bakes by default (set by bake).
bakeAt_ bakeAt_: Map<number, string | number> Per-step bake overrides, by 1-based step (set by bakeStep).
analyses_ analyses_: CanaryAnalysis[] The analyses each step runs (set by analysis).
analysisInterval_? analysisInterval_?: string | number How often a bake runs its analyses in flight (set by analysisInterval).
approval_? approval_?: string The signal the approval gate waits for (set by approval).
approvalTimeout_? approvalTimeout_?: string | number How long the approval gate waits (set by approvalTimeout).
lock_? lock_?: Configure<LockSettings> The rollout's lock, held for the whole run (set by lock).
durable_ durable_: boolean Whether bakes suspend the run instead of sleeping (set by durable).
#

ExposureKind

type
type ExposureKind = traffic | replicas | channel

How a platform exposes a candidate:

- "traffic" — a share of requests, set exactly (a traffic split). - "replicas" — a share of instances, quantised to whole replicas, so the achieved exposure can differ from the one requested. - "channel" — a release channel (a dist-tag, a pre-release); there is no percentage, so a canary on it declares no steps.

#

httpProbe

function
function httpProbe(configure: Configure<HttpProbeSettings>): CanaryAnalysis

An analysis that requests a URL a number of times and fails the canary when more than maxFailures of them fail — a status outside the expected ones, a timeout, or a connection error:

httpProbe((h) => h.url("https://api.example.com/healthz").samples(30))

The lambda runs on each check, so it may read resolved parameters. The failure message passes through the run's redactor.

#

HttpProbeSettings

class
class HttpProbeSettings

Settings for httpProbe: what to request, how often, how strictly.

MemberSignatureDoc
url url(url: string): this The URL to request.
samples samples(count: number): this How many requests to make (default 10).
maxFailures maxFailures(count: number): this How many failed requests are tolerated before the probe fails (default 0).
expectStatus expectStatus(...statuses: number[]): this The statuses that count as healthy; any 2xx when none are set.
timeout timeout(duration: string | number): this How long one request may take ("5s", or ms; default 5 s).
interval interval(duration: string | number): this The pause between requests ("1s", or ms; default 1 s).
header header(name: string, value: string): this Add a request header.
fetch fetch(fetcher: fetch): this The fetch to use — the seam a test answers requests through.
url_? url_?: string The URL to request (set by url).
samples_ samples_: number How many requests to make (set by samples).
maxFailures_ maxFailures_: number How many failed requests are tolerated (set by maxFailures).
expectStatus_ expectStatus_: number[] The statuses that count as healthy; any 2xx when empty (set by expectStatus).
timeout_ timeout_: number How long one request may take, in ms (set by timeout).
interval_ interval_: number The pause between requests, in ms (set by interval).
headers_ headers_: Record<string, string> Request headers (set by header).
fetch_ fetch_: fetch The fetch to use (set by fetch); the global one by default.
#

MetricReader

type
type MetricReader = unknown

Reads the metric a metricThreshold judges.

#

metricThreshold

function
function metricThreshold(configure: Configure<MetricThresholdSettings>): CanaryAnalysis

An analysis that reads a number and fails the canary when it is out of bounds:

metricThreshold((m) =>
  m.name("error ratio").read(() => readErrorRatio()).max(0.01)
)

The lambda runs on each check, so a reader may use resolved parameters.

#

MetricThresholdSettings

class
class MetricThresholdSettings

Settings for metricThreshold: a name, a reader, and the bounds the value must stay within.

MemberSignatureDoc
name name(name: string): this What to call the metric when it is out of bounds.
read read(reader: MetricReader): this How to read the value — handed where the rollout stands.
min min(value: number): this Fail when the value is below value.
max max(value: number): this Fail when the value is above value.
name_ name_: string What the metric is called in a failure (set by name).
read_? read_?: MetricReader How to read the value (set by read).
min_? min_?: number The lowest acceptable value (set by min).
max_? max_?: number The highest acceptable value (set by max).
#

prometheus

function
function prometheus(configure: Configure<PrometheusSettings>): CanaryAnalysis

An analysis that runs an instant PromQL query and fails the canary when any sample is out of bounds:

prometheus((p) =>
  p.url("https://prometheus.internal")
    .query('sum(rate(http_errors_total{rev="canary"}[5m])) or vector(0)')
    .max(0.01)
)

The lambda runs on each check, so it may read resolved parameters. The query is sent by PrometheusTasks.query from @zuke/prometheus, as a GET with the expression in the URL — as this analysis always has. A URL sent a credential must be https: unless it is loopback (or ZUKE_ALLOW_INSECURE_URL is set). A credential is an Authorization, Proxy-Authorization or Cookie header, a header whose name looks like a credential's (X-API-Key, X-Auth-Token), user:password@ or a credential parameter in the URL, or anything set through connection(...) — a secretHeader(...), sigv4(), google(), azure(). Any other header, such as an X-Scope-OrgID tenant id, leaves an in-cluster http://prometheus.monitoring.svc:9090 usable. Failure messages pass through the run's redactor, so a secret parameter in the URL or the query is masked, and a header value (eight or more characters) never appears in them.

#

PrometheusSettings

class
class PrometheusSettings

Settings for prometheus: where, what to ask, and the bounds.

MemberSignatureDoc
name name(name: string): this What to call the query when it fails.
url url(url: string): this The Prometheus base URL, such as https://prometheus.internal.
query query(promql: string): this The PromQL expression. It must return a vector or a scalar; an empty vector fails, so write … or vector(0) when no data means healthy.
min min(value: number): this Fail when any sample is below value.
max max(value: number): this Fail when any sample is above value.
header header(name: string, value: string): this Add a plain request header, such as a tenant id: header("X-Scope-OrgID", tenant). Authenticate through connection instead, with connection((c) => c.bearerToken(token)) or c.basicAuth(…) / c.secretHeader(…), so @zuke/prometheus handles the credential itself: bearerToken refuses an empty token, most likely a missing secret, rather than sending a blank Bearer . As in @zuke/prometheus, a plain header leaves an in-cluster http:// URL usable, while Authorization, Proxy-Authorization, Cookie and a credential-looking name (X-API-Key, X-Auth-Token) are credentials wherever they are set and need https: (unless loopback, or ZUKE_ALLOW_INSECURE_URL is set).
timeout timeout(duration: string | number): this How long the query may take ("10s", or ms; default 30 s).
fetch fetch(fetcher: fetch): this The fetch to use — the seam a test answers queries through.
connection connection(configure: Configure<PrometheusConnectionSettings>): this Configure the rest of the connection with @zuke/prometheus's own settings — a bearer token, the managed services' credentials, an API-key header, a response cap: ``ts prometheus((p) => p.url("https://aps-workspaces.us-east-1.amazonaws.com/workspaces/ws-1") .query(ERROR_RATIO).max(0.01) .connection((c) => c.sigv4((a) => a.region("us-east-1"))) ) ` Applied after url, header, timeout and fetch`, so a request signer configured here signs the headers set above.
name_ name_: string What the query is called in a failure (set by name).
url_? url_?: string The Prometheus base URL (set by url).
query_? query_?: string The PromQL expression (set by query).
min_? min_?: number The lowest acceptable sample (set by min).
max_? max_?: number The highest acceptable sample (set by max).
headers_ headers_: Record<string, string> Request headers, such as Authorization (set by header).
timeout_ timeout_: number The request timeout in ms (set by timeout).
fetch_ fetch_: fetch The fetch to use (set by fetch); the global one by default.
connection_ connection_: Configure<PrometheusConnectionSettings> Further connection settings (set by connection).
#

ThresholdBounds

interface
interface ThresholdBounds

Inclusive bounds a metric must stay within; either side may be open.

MemberSignatureDoc
min? min?: number The lowest acceptable value, if any.
max? max?: number The highest acceptable value, if any.