Plan and apply¶
ShinyHub has two deployment paths:
shinyhub deployis the fast path. It prepares the current source and deploys it immediately.shinyhub plan --outfollowed byshinyhub applyis the reviewable path. It saves the exact bundle, desired state, target, expiry, and remote revision that were reviewed, then applies those bytes without rebuilding from the working tree.
Use the exact path when a deployment needs human approval, an audit trail, or a strict separation between review and execution:
shinyhub doctor .
shinyhub plan . --out sales.plan
shinyhub plan show sales.plan
shinyhub apply sales.plan
Pass the source path explicitly. Plan, like deploy, never assumes that the current directory is safe to bundle.
Read a plan¶
Human output follows the same decision order for a single app and a fleet:
- outcome;
- user, availability, ownership, or destructive impact;
- semantic changes;
- bundle or resource detail;
- create/update/adopt/delete totals;
- the safest next command.
At narrow terminal widths the layout stacks instead of discarding information.
Color is additive: action words, symbols, headings, and ordering retain the same
meaning with --no-color, NO_COLOR, or ASCII-only output.
The default view is decision-sized. Ask for implementation detail only when it is useful:
shinyhub plan . --details
shinyhub plan show --details sales.plan
shinyhub plan show --files sales.plan
The detail view includes every bundled path, launch command, readiness contract, manifest effect, permission, target URL, and saved-plan integrity field. JSON is always complete and does not apply the human view's progressive disclosure.
What plan verifies¶
shinyhub plan uses the same bundle preparation, launch resolution, manifest
validation, and target selection as deploy. It reports:
- create, update, or unchanged/redeploy intent;
- current and planned content digests and typed configuration values;
- create/manage permission, access, ownership, and lifecycle effects;
- source size, upload size, included paths, ignore rules, and protected data or cache paths;
- runtime, dependency preparation, launch command, readiness endpoint, and startup deadline;
- hooks, schedules, access declarations, tracing, and other manifest effects;
- fleet adoption and every prune candidate in an isolated destructive section;
- degraded comparisons, version skew, in-progress deployment, ownership, and availability warnings.
Planning is read-only. It performs local preparation and remote reads, but never creates an app, uploads a bundle, changes access, starts a stopped app, or probes permission with a write.
An unchanged digest means the bundle's files, executable bits, and manifest
match the newest successful deployment. Applying is still explicit because a
redeploy may replace replicas. If an older server cannot report a live digest,
plan reports the comparison as unknown; it never invents equality.
Saved plans are exact and private¶
--out writes an owner-only (0600) plan container containing application
source. Treat it like a release artifact:
- do not commit it;
- transfer and retain it only where the source itself is allowed;
- use
--expires-into shorten the default 24-hour lifetime; - use
--forceonly when deliberately replacing an existing plan file.
Before any mutation, shinyhub apply verifies the container structure,
integrity digest, embedded bundle digest, expiry, target host, desired-state
consistency, server compatibility, and the target's resource revision. A
changed working tree is irrelevant: apply uploads the reviewed embedded bundle.
If the app changed after planning, apply returns a conflict and tells you to create a new plan. It does not silently recompute or apply against the newer state. Creates use an expected-absent precondition, so another actor claiming the slug is also rejected before deployment.
Use plan show for offline inspection. It verifies the artifact but permits an
expired plan because inspection cannot mutate the server; apply never permits
an expired plan.
Fleet plan and apply¶
Fleet planning uses the same action vocabulary and count model:
Ownership transfers require --adopt. Deletion requires --prune plus an
interactive confirmation, or an explicit --yes supplied by the operator.
Suggested and recovery commands intentionally never add --yes.
Fleet apply is non-atomic and continues across resources. Its report therefore records a terminal status and a separate mutation state for every app or project:
none: ShinyHub can prove no mutation occurred;committed: the requested resource change completed;partial: a mutation committed but a later convergence step failed;unknown: the client cannot prove whether the remote mutation committed.
Every run has a cryptographically random run ID for correlation with server audit records. Current servers add a monotonic sequence, client heartbeat, and immutable terminal status. Late writes from an older overlapping run cannot replace newer per-app convergence, and a run that stops heartbeating without a terminal result is distinguishable from success. On failure, human and JSON output identify committed, partial, and unknown resources and provide one of three recovery strategies:
resume: transient failure; re-run apply;repair_then_resume: fix the deterministic or post-commit failure, then re-run apply;replan: remote state conflicted; review a fresh plan before re-applying.
Two repair_then_resume cases are deliberate refusals rather than defects, and
the recovery output names the specific next action for each instead of a
generic repair. A downtime_required failure means the deploy could not be
completed without dropping live sessions, because parallel generation handoff
does not support that app's current shape; rather than drop them unasked the
server made no change and left the working version serving. Re-run with
--allow-downtime to permit the stop-first deployment. A schedule_stale
failure means a freshness gate is unsatisfied and apply will not dispatch
producers itself; the recovery lists the shinyhub schedule run <slug>
<schedule> commands to run first, and only for schedules that are plainly
overdue rather than already refreshing.
Grouped worker handoff¶
Code updates to an app using grouped isolation can deploy without stopping
its existing workers. ShinyHub prepares the replacement bundle and health-checks
one new worker before publishing it. Existing client bindings and WebSockets
remain on the old version; new clients use the replacement. The Switch to
latest action gives that browser a new binding without moving other clients.
No additional flag is needed, and --allow-downtime still permits a fallback
rather than forcing one.
This path supports matching grouped isolation on the default native tier of
a single server. An unchanged manifest is accepted when its declared settings
still match the live app. Changed configuration, hooks, shared producer changes,
explicit placement, other providers, and an older generation still draining or
awaiting cleanup require the existing stop-first path. The server also checks
that there is memory for the additional worker before starting it. A candidate
that fails readiness leaves the old version serving.
Old workers count as draining and do not accept new clients or consume the new
generation's max_workers allowance. They retain memory until their clients
finish, including the reconnect grace window. server.drain_timeout remains
the hard limit: sessions still using the old version at that deadline are
terminated. Further workers and warm spares start on demand using the newly
published bundle. A server restart ends grouped sessions and cleans up recorded
workers before starting a fresh pool; this feature covers app deployment, not
session preservation across a server restart.
Re-running fleet apply recomputes current state. Already completed resources become unchanged, so they are not applied twice. Deploy attempts automatically retry only readiness timeouts, transport failures, and server errors; config patches retry only server-side failures. Invalid bundles, missing runtimes, build/hook failures, validation errors, conflicts, and crashes are not repeated implicitly. Once an upload has committed, its retry budget re-checks health and digest readback without uploading the bundle again. Source uploads on current servers are also fenced by the digest and fleet owner observed by plan, so a late overlapping apply conflicts before promotion instead of becoming the last writer.
Older servers without fleet preconditions remain usable in a clearly marked
degraded mode. Config reconciliation narrows the race with a fresh read, while
pruning is disabled unless the operator explicitly accepts the risk with
--allow-unsafe-degraded-prune.
Automation contract¶
Request JSON explicitly instead of parsing terminal text:
shinyhub plan . --output json
shinyhub plan . --detailed-exitcode --output json
shinyhub fleet plan -f fleets/eu.toml --output json
shinyhub fleet apply -f fleets/eu.toml --output json
The plan envelope includes a schema version, typed resources and values, impacts, warnings, counts, and next actions. Single-app output also includes the bundle, launch, manifest, remote state, and deploy command. Fleet apply adds the run ID, run status, per-resource result and mutation state, summary exit fields, and structured recovery guidance.
Plan exit codes:
| Code | Meaning |
|---|---|
0 |
Plan printed; with detailed exit codes, content is unchanged |
1 |
Local validation or CLI/server protocol compatibility failed |
2 |
Detailed mode only: content is new, changed, or cannot be compared |
3 |
Network, authentication, or authorization failed |
6 |
The host answered, but ShinyHub was not ready |
Fleet apply additionally uses 4 for partial convergence and 5 for a remote
precondition conflict. A successfully printed ordinary plan returns 0 unless
--detailed-exitcode (or its --fail-on-changes alias) was requested.
Fast path¶
For an interactive deployment where a separately approved artifact is not needed:
--open waits for health, verifies the routed app when possible, and opens its
canonical URL. The plan's final command remains copy-pasteable and uses --wait
for automation; replace it with --open for the browser handoff.