Parameters
Parameters are typed inputs to a build. You declare them as
class fields — exactly like targets — and
read the resolved value inside a target with this.<name>.value.
The execution engine resolves every parameter before any target runs,
so a build never starts with a missing or malformed input. The
Concepts page introduces them in brief;
this is the full reference.
import { Build, parameter, run, target } from "jsr:@zuke/core";
class Deploy extends Build {
environment = parameter("Target environment")
.options("dev", "staging", "production")
.required();
workers = parameter("Parallel upload workers").number().default(4);
dryRun = parameter("Print actions without performing them").boolean();
deploy = target().executes(() => {
if (this.dryRun.value) console.log("(dry run)");
console.log(
`Deploying to ${this.environment.value} with ${this.workers.value} workers`,
);
});
}
await run(Deploy); ./zuke deploy --environment production --workers 8 --dry-run
ENVIRONMENT=staging ./zuke deploy # value from the environment
Each fluent method returns a new parameter whose
value type reflects the configuration — the builder narrows the
type as you go, so value is exactly as strong as the declaration.
Declaring a parameter
parameter(description?) starts an optional string
parameter. The optional description is shown in --help and
--list. From there, configure it fluently:
| Method | Effect | value type |
|---|---|---|
parameter("…") | optional string | string | undefined |
.number() | parse as a number | number | undefined |
.boolean() | a flag; defaults to false | boolean |
.options("a", "b") | restrict a string to choices | unchanged |
.default(v) | provide a default | non-optional (T) |
.required() | must be supplied | non-optional (T) |
.env("NAME") | override the env var name | unchanged |
.secret() | mark sensitive (masked everywhere) | unchanged |
.from(source) | resolve from a secret manager | unchanged |
.array() | a comma-separated / repeatable list | T[] |
Ordering matters: .number() and .boolean() come
first (they change the kind and reset the default), .options()
applies to strings, and .array() comes last so it
composes with the kind and choices declared before it.
There is no .choices(...) — the method that restricts a string
to a fixed set is .options(...). Three field names are also
reserved: dryRun, confirm, and
operatorToken collide with control keys Zuke's
MCP server injects, so a parameter using one is
rejected at discovery. Rename it.
Flags and environment variables
The command-line flag and environment variable are derived from the
property name: the flag is kebab-case, the env var is
SCREAMING_SNAKE_CASE. A camelCase field like targetEnv becomes
--target-env and TARGET_ENV. Override just the
environment variable with .env("NAME") — the flag is unaffected.
retries = parameter().number(); // --retries / RETRIES
targetEnv = parameter(); // --target-env / TARGET_ENV
apiKey = parameter().env("CI_API_KEY"); // --api-key / CI_API_KEY
A flag accepts both --environment production and
--environment=production. A boolean is set simply by naming the
flag (--dry-run).
Typing the value
A bare parameter() is a string. .number() and
.boolean() change the kind; .options() keeps it a
string but restricts the accepted values.
// string (the default): value is `string | undefined`
region = parameter("Deploy region");
// number: parsed with Number(); empty or non-finite input is rejected.
workers = parameter("Upload workers").number(); // number | undefined
// boolean: a flag. Present (--verbose) → true; absent → false.
verbose = parameter("Verbose logging").boolean(); // boolean
// string restricted to a fixed set of choices.
level = parameter("Log level").options("debug", "info", "warn"); .number()parses withNumber(); an empty string or a non-finite result ("eight","") is rejected with a parse error..boolean()acceptstrue/1/yesandfalse/0/no(any casing) from a flag value or env var; anything else is rejected. As a flag with no value it istrue, and it defaults tofalsewhen absent..options("a", "b")restricts the string to the listed choices; any other value is rejected naming the allowed set.
Lists with .array()
.array() turns a parameter into a list. On the command line, a
comma separates values or the flag is repeated — the two are
equivalent. Blank entries are dropped, and an unsupplied list defaults to
[] (never undefined).
tags = parameter("Image tags").array(); // string[]
// deploy = target().executes(() => console.log(this.tags.value)); ./zuke deploy --tags latest,canary # ["latest", "canary"]
./zuke deploy --tags latest --tags canary # same result
TAGS=latest,canary ./zuke deploy # from the environment
Because .array() reuses the element parser of the parameter it is
applied to, it composes with the kind and choices declared before it —
every element is validated, not just the raw string:
// number[]: each entry parsed as a number; "1,x" is rejected.
workers = parameter("Worker ids").number().array();
// each element must be one of the choices; "api,nope" is rejected.
services = parameter("Services").options("api", "web", "worker").array(); .required() and .default()
By default a parameter is optional and its value is
T | undefined. Two methods make it non-optional:
// Required: value is non-optional; the build fails before any target runs
// if neither a flag nor the env var (nor an interactive prompt) supplies it.
environment = parameter("Target environment").required(); // string
// Default: value is non-optional and falls back to the given value.
workers = parameter("Upload workers").number().default(4); // number
// A boolean is implicitly defaulted to false, so it is always non-optional.
dryRun = parameter("Dry run").boolean(); // boolean .required()demands a value. If none is supplied — and no default exists and no interactive prompt answers it — the build fails before any target runs, with a message naming the flag and env var..default(v)supplies a fallback, sovalueis always the declared type. A boolean is implicitly defaulted tofalse.
When a required parameter is missing and the build runs at an interactive terminal (a TTY, not CI), Zuke prompts for the value instead of failing. On CI or non-interactive runs it still errors, so automation stays deterministic.
.secret() and .from()
.secret() marks a value sensitive: its resolved value is
redacted from all of Zuke's output — every banner, target
status, summary, and error message — and, under GitHub Actions, Zuke emits an
::add-mask:: so the runner masks it in its own logs.
.from(source) resolves the value from a secret manager (1Password,
Vault, a mounted file, …) when neither a flag nor an env var supplied one. It is
a fallback provider, not an override, and is typically paired
with .secret() so the fetched value is redacted.
import { execSecret } from "jsr:@zuke/core";
// .secret() masks the resolved value everywhere Zuke prints.
token = parameter("Deploy token").secret().required();
// .from(source) pulls the value from a secret manager when neither a flag
// nor an env var supplied one — a fallback provider, not an override.
registryToken = parameter("Registry token")
.secret()
.from(
execSecret((s) => s.command("op").arg("read", "op://ci/registry/token")),
);
The Secrets guide covers sources
(execSecret, fileSecret), the exact redaction
guarantee and its boundary, and the SecretError surface in full.
Resolution precedence
Every parameter is resolved before the first target executes, from — in order of precedence:
- a command-line flag —
--environment productionor--environment=production - the environment variable —
ENVIRONMENT(or the.env("NAME")override) - the
.from(...)secret source, if declared - the declared default
Each level is consulted only when the ones above it produced nothing, so the source is invoked only when neither a flag nor an environment variable supplied a value. This is what keeps a build portable: in CI a value is usually injected as an environment variable (and the source is never touched); on a developer's machine the source pulls it from their secret manager — no code change either way. For a missing required value, an interactive prompt (at a TTY) is tried before the source.
Validation and errors
Resolution collects all parameter problems and reports them together, before any target runs — a missing required value, or a value that fails to parse (a non-number, a bad boolean, an off-list choice). The failure is a single block naming each offending flag:
Invalid or missing parameters:
--environment is required (or set ENVIRONMENT).
--workers: expected a number, got "eight"
--level: expected one of debug, info, warn, got "trace"
The error class is ParameterError, exported for programmatic use.
Reading .value before the build has resolved parameters (for
example at construction time) also throws a ParameterError — read
parameters inside a target body, not in a field initializer. For a
.secret() parameter, the raw value is registered for redaction
before it is parsed, so even a parse error on a malformed
secret is masked rather than echoed.
Reading a value
Read a parameter inside a target via this.<name>.value. The
value is fully typed by the declaration: a .required() or
.default() parameter is non-optional, while a plain optional
parameter is T | undefined.
deploy = target().executes(() => {
const env = this.environment.value; // string (.required())
const n = this.workers.value; // number (.default(4))
const region = this.region.value; // string | undefined (plain optional)
});
Resolution lives in the execution engine, not the CLI, so a
programmatic execute call
resolves parameters too — pass raw values via params and/or rely on
environment variables. See Concepts for
the short version and Secrets for sensitive inputs.