Ship

Old Docs · Managed Resources
On this page · 9 sections

Managed Resources

Decisions

  • Remote infrastructure is declared with first-class resource items and reconciled through ship resources; ship db remains exclusively responsible for schema diff, verification, migrations, and live schema status.

  • Resource declarations compile into the same normalized typed workflow graph contract as other Ship workflows. Provider operations are versioned adapters; the executor never shells out to provider CLIs, falls back to direct commands, or passes arbitrary JSON as a semantic value.

  • The lifecycle is read → structured diff (noop, update, replace) → reconcile, plus explicit adopt and delete. Providers own remote semantics; Ship owns dependency ordering, plans, leases, state checkpoints, recovery, events, and aggregate results.

  • apply consumes the exact saved plan from .ship/resources/<env>/plan.json. It rejects changed graph/input digests, graph format, provider versions, resource schema versions, or state revisions. It never silently replans.

  • State is canonical and environment-scoped at .ship/resources/<env>/state.json. Every crash-safe atomic checkpoint advances a compare-and-swap revision while holding a stack lease.

  • State records stack ownership and per-resource generation identity. Operations checkpoint pending before remote mutation and ready or tainted after it, making retries idempotent and partial failure visible.

  • Destruction is explicit: ship resources destroy only deletes state entries whose declarations previously opted in with delete = True. Deletes run in reverse dependency order.

  • Replacement uses create-before-delete cutover. State records retired physical generations before cleanup, so interruption or cleanup failure can resume without losing either identity.

  • Sensitive values in plans, graphs, events, results, state, and output commands are typed secret references. These artifacts never contain plaintext secrets.

Commands

ship resources check --env dev .
ship resources plan --env dev .
ship resources apply --env dev .
ship resources destroy --env dev .
ship resources output AppRuntime.connection_url_pooled --env dev .

check performs provider reads and emits a typed non-mutating workflow result. plan saves the exact serializable plan. apply validates and consumes that artifact. destroy creates and immediately consumes an explicit destroy plan. output runs a typed read-state workflow node against canonical state and prints public values or an opaque <secret-ref:store/key> reference.

--offline is an explicit typed provider adapter for check and plan. It produces blocked/unknown observations and cannot mutate. Missing credentials never trigger an implicit offline fallback.

ship deploy [--env <env>] [path] is the one-step form for resources that consume build outputs: it validates credentials, builds every target named by an artifact input, hashes the output into the plan, saves it, and applies exactly that plan. ship plan deploy renders the same plan without building, so each artifact digest is an explicit <artifact:Target@unknown>.

Providers

The provider catalog in crates/ship-cli/src/domain/resources.rs lists every provider package. A package names the kinds it owns and decodes their declarations into desired resources plus offline and live adapters; the CLI builds a live adapter, and so asks for credentials, only for families that are declared or still present in state.

Providers are opted in one operation at a time. HTTP clients are generated by just provider-clients from distilled's Smithy models pinned in vendored/distilled/manifest.toml: only allowlisted operations and members are vendored or generated, each operation gets a typed error enum, and model enums become lossless Rust enums whose TAGS are the Ship tag spellings a setting accepts. A test fails when the committed models or clients drift from the manifest. When an upstream model mistypes a scalar member, a service's [service.targets] table corrects it at generation time while the vendored model stays upstream's projection; a correction must name an existing member, replace a prelude scalar with a different one, and say why in a comment. Settings a kind does not support yet are named gates with a tracking issue, so using one is a diagnostic rather than an ignored setting.

Artifact Inputs

A setting that names a server(Static) target, such as assets = Docs, is an artifact input: its value is the target's static_root plus a content digest over every file path and byte. Planning records the digest, and the provider re-hashes the output before mutating, so output that changed after the plan fails as a stale plan instead of publishing unplanned bytes. ship resources plan and apply hash existing output; output that was never built is a deploy/unbuilt-target error on every setting that publishes it. ship deploy builds first, and a failed build is a deploy/build-failed error on those same settings after the build's own diagnostics.

State And Plans

Plan bindings include:

  • normalized resource graph digest;

  • evaluated input digest and environment;

  • workflow graph format;

  • every provider adapter version;

  • every resource schema version;

  • exact state revision.

    Resource action, dependency, marker-output, and state-output workflow types derive WorkflowSchema directly from their Rust definitions. Provider action keys include the provider identity and provider version, while each registered operation version binds the exact generated input/output descriptors. There is no separate resource schema catalog and no nominal-only fallback. Saved-plan loading therefore rejects a missing provider operation version or any structural descriptor mismatch before provider mutation.

    State writes use a same-directory temporary file, file synchronization, atomic rename, and directory synchronization. A lease file is acquired with create-if-absent semantics; abandoned local-process or expired leases can be recovered. Every write compares the expected revision under that lease.

    Resource state stores provider identity, physical identity, owner, generation, dependencies, desired digest, destroy intent, typed properties and outputs, and pending/ready/tainted status. Replacement state additionally stores retired physical generations until provider deletion succeeds.

Lifecycle Results

Resource workflows emit versioned structured events:

WorkflowStarted
NodeStarted
NodeCheckpoint
NodeFinished
WorkflowFinished

Independent resources continue after a failure; dependents become blocked. Results aggregate success, failure, blocking, typed outputs, and final state revision instead of returning only the first provider error.

Every failure is a ProviderError that renders as an ordinary Ship diagnostic: a namespaced code (cloudflare/domain-in-use, planetscale/permission, resources/dependency-failed), a primary label on the declared setting that caused it (or on the resource name), the remote request and its answer as a note, and help naming the next step. Credential failures name the token's required permissions; failures that look transient say so. The command then ends with one line: a blocked plan changed nothing, and a failed apply has recorded its finished changes, so rerunning the named command continues from there. A resource whose last attempt failed is not trusted as applied: providers re-observe what they can and mark what they cannot, such as an asset digest, unknown.

PlanetScale

PlanetScale.Database, PlanetScale.Branch, and PlanetScale.Role are typed resource adapters over ordinary database AppDb(Postgres) schemas. Database and branch create/read/delete, role read/delete, adoption, and typed diffs use the provider lifecycle. Role creation is blocked before mutation until a concrete typed secret-store materializer can capture PlanetScale's one-time credentials without exposing plaintext. Unsupported PlanetScale update endpoints likewise fail as typed operations; neither case invokes a CLI or direct-command fallback.

PlanetScale hosting is not a database dialect. ship db apply AppDb remains the schema operation after resource provisioning has produced the selected Postgres endpoint.

kind and seed_data are tags from the generated client (kind = :postgres, seed_data = :last_successful_backup); other database kinds are rejected until supported. Roles are addressed by name in Ship and resolved to PlanetScale's role id before deletion.

Cloudflare

Cloudflare.Worker publishes a static site as an assets-only Worker on a custom domain:

resource Cloudflare.Worker ShipSite {
  account = "<32-hex account id>";
  assets = Docs;
  domain = "docs.example.com";
  html_handling = :auto_trailing_slash;
}

name defaults to the kebab-cased resource name. zone is optional; without it the provider finds the zone whose name is the longest suffix of domain. html_handling and not_found_handling take the generated tags, and compatibility_date is YYYY-MM-DD. Root _headers and _redirects files become the Worker's asset configuration instead of assets.

Live commands need CLOUDFLARE_API_TOKEN with Workers Scripts:Edit on the account and Workers Routes:Edit plus Zone:Read on the zone; CLOUDFLARE_API_BASE overrides the API origin for tests. Deploys upload only files Cloudflare does not already have, three at a time, dropping to one at a time after a gateway error and restarting the upload session when Cloudflare rejects it. Changing account or name replaces the Worker; changing domain binds the new hostname and then unbinds the old one, without republishing. A hostname another Worker serves fails on domain and names that Worker. main, routes, bindings, dispatch_namespace, and workers_dev are gated (SHIP-282 through SHIP-286). crates/ship-cli/tests/golden/deploy-static runs the full deploy against a fake API.

Next ship-cli Work

Local check, test, native binaries, and local Postgres — not secret stores. Track: SHIP-236. Next design questions are how tests work: ../questions/192-test-declaration-and-expect-syntax.md and ../questions/209-test-fixtures-and-seeds.md.

Secret materialization is deferred: ../questions/300-resource-sensitive-outputs-and-secrets.md (SHIP-237). The managed-resource lifecycle deliberately settles non-disclosure only; it does not answer secret materialization.