rune cast
Install or upgrade a release from YAML files, directories, runeset bundles, or remote URLs — plan, confirm, apply.
Aliases: apply.
cast is the universal "apply this spec" command. It accepts:
- A single YAML file
- A directory of YAML files
- A glob (
services/*.yaml) - A runeset directory or
.runeset.tgzarchive - A remote URL or git ref
rune cast my-service.yaml
rune cast my-service.yaml --namespace=production
rune cast my-service.yaml --tag=stable
rune cast my-directory/ --recursive
rune cast services/*.yaml
rune cast github.com/org/repo/path@ref --create-namespace
rune cast https://example.com/runeset.tgz --release=my-release
rune cast ./runeset.tgz --release=my-release
rune cast ./runeset --render --set=key=value
rune cast ./runeset --render --values=values.yaml
Every cast is a tracked release. There is no
fire-and-forget path. A loose service.yaml is treated as an inline runeset and
gets a release named after the file (with a warning); pin it with --release.
Operate releases afterward with rune release.
The cast pipeline
A cast is declarative — closer to terraform apply than kubectl apply. It runs:
render ─► lint ─► PLAN (3-way diff) ─► [confirm] ─► APPLY (create → update → verify) ─► PRUNE
The CLI renders and lints locally, then the server computes the plan, applies it in order, and prunes last. You see a plan block before anything changes:
Release "runesight" → revision 3 (namespace: observability)
+ create 2 (service/query-api, volume/ch-data)
~ update 1 (service/ingester)
- prune 1 (configmap/old-settings) ⚠ destructive
↪ adopt 0
= reference 0
A plan that prunes or adopts asks for confirmation before applying — type yes,
or pass --yes (CI and --detach skip the prompt). Pure create/update proceeds
automatically.
Flags
| Flag | Default | Notes |
|---|---|---|
-n, --namespace | from context | Target / home namespace for the release. |
--create-namespace | false | Create the namespace if it doesn't exist (referenced, never owned). |
--release <name> | derived | Release identity. Defaults to the runeset/file basename. |
--dry-run | false | Compute and print the plan; apply nothing. See below. |
--detach | false | Return once accepted; don't wait for rollout. Create/update-only. |
--yes | false | Skip the confirm prompt for plans that prune or adopt. |
--adopt | false | Take over resources that are unmanaged or owned by another release. |
--timeout <dur> | 15m | Rollout wait timeout for the whole release. |
-r, --recursive | false | Recurse into directories. |
--atomic | false | Roll back this cast's changes if it fails. A verify timeout also triggers rollback. See Updating running services. |
--force | false | Accepted but has no effect today. See Updating running services. |
--tag <name> | — | Tag this deployment for later reference. |
--render | false | Print rendered YAML and exit (works for inline casts too). |
--set k=v | — | Override a value (repeatable). |
-f, --values <file> | — | Extra values file (repeatable; last wins). |
-o, --output | — | json — emit the structured plan + result. |
--release, --render, --set, and --values are universal — they apply to
loose castfiles (inline runesets), not just packaged runesets.
Plan and dry-run
--dry-run renders, lints, and prints the 3-way plan — but applies nothing.
It now contacts the server to compare against the release's current state, so the
plan includes prunes and adoption conflicts:
rune cast ./my-app --release=my-app-prod --values=prod.yaml --dry-run
This is the same plan rune release diff shows for an
already-installed release; use --dry-run when you're casting from changed source
or values. For purely static, offline validation (schema, templates, cycles) use
rune lint.
Permissions
A cast needs the same verb on each resource that the direct API call would, checked in that resource's namespace (which can differ from the release's): create for a new resource, update for a changed or adopted one, delete for a pruned one. A cast that fails the check is denied before anything is written, with no pending revision. Every cast, not only --dry-run, also needs get on each resource it creates, updates, adopts or prunes, because the CLI plans before it applies (resources the release only references are skipped).
The built-in cast role (the recommended CI scope) can create and update services, configmaps, secrets and volumes, but cannot delete anything. A cast with that token that would prune a resource, including one dropped from the castfile, is denied before anything is applied:
permission denied: <subject> is not allowed to "delete" <kind> in namespace "<ns>"
<kind> is services, secrets, configmaps or volumes; together with the namespace it tells you which pruned resource tripped it. Either grant the token delete on that kind, or keep it as it is and remove the resource once with an admin token (rune delete service <name> -n prod), then re-cast. Adding --adopt doesn't get around it: adopting counts as an update, and pruning the adopted resource still needs delete. A hand-written narrow policy needs the per-kind verbs, volumes and get included.
Since v0.0.1-dev.148. Before that, a cast token only needs releases:create and can adopt then prune resources it could not delete directly.
Adoption
A cast that would overwrite a resource not owned by this release fails with an
ownership conflict — the guardrail that keeps two releases from fighting over the
same resource. Pass --adopt to take ownership (e.g. pulling pre-existing,
pre-release resources under management on first cast):
rune cast ./my-app --release=my-app-prod --adopt
Updating running services
Since v0.0.1-dev.148.
Casting a changed service updates it in place: it keeps its identity, its generation counters and any virtual IP it has been given. Only a change to something an instance is built from (the image, env, ports, mounts, probes, security context and so on) replaces instances, and they are replaced under the service's updateStrategy, not all at once. Edits to scale, networking, ingress and similar settings apply without replacing anything. The full split is in what replaces instances.
- Notes after the apply. Some edits move the service's generation but change nothing that is already running: labels,
imagePullandimagePullAnonymous(copied onto an instance when it is created), and aclaimTemplatechange (existing volumes are kept as they are, so a new size or class reaches only new replicas).rune castprints a note for each after the release summary, and-o jsonincludes them per change. They appear after the apply only, not on--dry-runor at the confirm prompt, and a failed or denied cast prints none. - A service being deleted is refused. Casting onto a service that is still tearing down fails with
service/<namespace>/<name> is being deleted; cast again once teardown finishes, rather than writing a spec that would be deleted with it. --atomicrolls back more than it changed. The rollback puts the previous spec back and replaces instances that are older than it. Instances the failed cast never touched count as older, so a failure after replacing one of six instances rolls all six. The rollback is budgeted like any update, so availability holds, but expect more churn than the failure caused (rune#263).--forcedoes nothing yet. The flag is parsed but never sent (rune#260). To roll a service whose image tag moved but whose spec did not, runrune restarton it (imagePulldefaults toalways, so the replacements pull the new image).
Detach
--detach returns as soon as the cast is accepted instead of waiting for rollout.
It is create/update-only: a plan that prunes refuses --detach so the
destructive half is never orphaned. Drop --detach to apply a destructive plan,
or inspect it first with rune release diff.
Behavior
- Idempotent. Re-casting the same spec into the same release is a no-op plan.
- Aggregate timeout.
--timeoutbounds the whole release rollout, not each service. - Deterministic apply. Resources apply in a stable order, then prune runs last.
- Multi-document YAML is supported — separate documents with
---. - Mixed resource files (services + secrets + configmaps in one file) are fine.
- Failures surface inline. Without
--detach,castwaits for the release to become ready. If the reconciler reportsFailedit stops waiting immediately, prints a short reason + the one-sentence cause + arune get service <name>hint, and exits non-zero. The release is leftfailedwith the prior revision live and nothing pruned.
✗ ImageUnreachable
pull access denied for ghcr.io/acme/api, repository does not
exist or may require 'docker login'
image: ghcr.io/acme/api:1.4.2
service: prod/api
rune get service api -n prod
Common patterns
# Validate first (offline, static)
rune lint manifests/ --recursive
# Preview the full plan against live state
rune cast manifests/ --release=app --dry-run
# Install / upgrade a release
rune cast ./my-app --release=my-app-prod --values=prod.yaml
# Non-interactive deploy in CI (structured output, skip the prompt)
rune cast ./my-app --release=my-app-prod -f prod.yaml --yes --output=json
# Diff a runeset's rendered YAML before installing
rune cast ./my-app --render --values=prod.yaml | diff - last-rendered.yaml
See also
- Releases — the stateful model behind every cast.
rune release— list, status, history, diff, rollback, uninstall.- Service spec
- Runesets
rune lint