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.tgz archive
  • 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

:::note 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

FlagDefaultNotes
-n, --namespacefrom contextTarget / home namespace for the release.
--create-namespacefalseCreate the namespace if it doesn't exist (referenced, never owned).
--release <name>derivedRelease identity. Defaults to the runeset/file basename.
--dry-runfalseCompute and print the plan; apply nothing. See below.
--detachfalseReturn once accepted; don't wait for rollout. Create/update-only.
--yesfalseSkip the confirm prompt for plans that prune or adopt.
--adoptfalseTake over resources that are unmanaged or owned by another release.
--timeout <dur>5mRollout wait timeout for the whole release.
-r, --recursivefalseRecurse into directories.
--forcefalseForce generation increment even with no diff.
--tag <name>Tag this deployment for later reference.
--renderfalsePrint rendered YAML and exit (works for inline casts too).
--set k=vOverride a value (repeatable).
-f, --values <file>Extra values file (repeatable; last wins).
-o, --outputjson — emit the structured plan + result.

:::note --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.

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

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 (unless --force).
  • Aggregate timeout. --timeout bounds 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, cast waits for the release to become ready. If the reconciler reports Failed it stops waiting immediately, prints a short reason + the one-sentence cause + a rune get service <name> hint, and exits non-zero. The release is left failed with 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