Architecture¶
runway is a single Rust binary. It turns a declarative runway.yaml into one
Cloud Run service per stage, plus the resources that service needs, by
calling Google Cloud APIs directly with Application Default Credentials.
There is no state backend: the live project is the source of truth and every command re-reads it.
Data flow¶
runway.yaml ─► config::load ─► config::resolve(stage, CLI overrides) ─► Deployment
(strict serde) (stage merge, ${…} interpolation, (resolved and valid)
defaults, validation)
Deployment ─► provision::{api_step, pre_steps, post_steps} ─► Step list (check / apply)
─► plan::ServiceSpec::from_deployment ─► desired state
│
live Service (Cloud Run v2) ─► gcp::run::observed_flat ┴► plan::diff ─► Plan (text / JSON)
└► gcp::run::current_traffic ─► traffic::plan ─► traffic split
deploy:
APIs ─► inspect (service + source hash + image lookup, concurrently)
─► project tags ─► buckets, secrets, repository ─► service accounts ─► grants
─► secret values present? ─► build if needed (Cloud Build) ─► service tags
─► create/update (update mask, etag, template reuse) ─► wait until ready
─► IAP ─► public/private access
Modules¶
| Module | Responsibility |
|---|---|
cli, output, style |
clap definitions, dispatch, text/JSON rendering, colors, exit codes |
config |
Schema (deny_unknown_fields), stage merge, ${…} interpolation (interp), defaults, validation with precise paths |
naming |
Deterministic names: service, image, labels, source objects, build tags, IAM resource names |
plan |
Desired state (ServiceSpec), normalized flat representation, diff, plan rendering |
traffic |
Pure traffic logic: full rollouts, previews, canaries, promote, splits, tag names |
build |
Deterministic source scan and hash, parallel compression (package), base-image digests (inputs), Cloud Build (cloudbuild), release tags (release) |
gcp |
Client construction (ADC, impersonation), Cloud Run request construction and observation (run), registry digest adapter, IAM policy edits, buckets, logs, error classification |
deploy |
Reconciliation of the service: ownership, create/update with ambiguity handling, readiness, invoker IAM |
provision |
Stateless steps with a read-only check (plan) and an idempotent apply (deploy): APIs, project and service tags, buckets, secrets, repository, service accounts, grants, secret values, IAP |
retry, poll |
Per-step retries with backoff and error classification; bounded polling |
describe |
Offline ASCII/Mermaid diagram and explanation of a stack |
commands |
One module per CLI command |
Design decisions¶
- Official SDKs first. Cloud Run, Cloud Build, Cloud Storage, Logging,
Resource Manager, IAM, Secret Manager, Service Usage, Artifact Registry,
BigQuery and IAP use the official
google-cloud-*Rust crates. Small REST adapters exist only where no SDK call exists: the OCI registry v2 protocol (digest lookup, tag listing) and Service Usagev1beta1 generateServiceIdentity. - No state file. Ownership is encoded in labels (
managed-by=runway,runway-app,runway-stage) and in a marker in the description of service accounts runway creates. runway refuses to modify what it does not own. - Steps, not state. Provisioning is a list of steps; each re-reads before writing, so partial failures resume naturally. Grants, tags and IAP members are additive because removals would need a record of what was added.
- Template reuse. When only service-level fields change (traffic, ingress, IAP), the live revision template is sent back untouched, so Cloud Run creates no revision. Previews and canaries get a marker annotation so they always run in a revision of their own.
- Traffic without state. The split is derived from the live traffic, the
latest ready revision and the requested mode; entries following
latestare pinned before a preview or canary is added, so a branch can never take production traffic. - Content-addressed builds. The tar stream is deterministic (sorted entries, fixed metadata); its SHA-256, combined with the digests of the base images, names the uploaded object, the image tag and a Cloud Build tag. Unchanged inputs are not rebuilt; an in-flight build of the same inputs is attached to, not duplicated.
- Concurrent independent reads. Plans, deploys,
info,doctorand the readiness poll issue independent requests concurrently, so latency is bounded by the slowest call. - Bounded waits, ambiguity handled. Every poll has a deadline and honours Ctrl-C. Mutations with an ambiguous outcome (timeouts, transport errors, 5xx) are followed by a read before any retry.
- Retries that understand Google Cloud. IAM propagation, freshly enabled APIs and freshly bound tags are retried with backoff; configuration errors, conflicts and "a person must act" situations (an empty secret) fail fast.
- Explicit uncertainty. A plan computed before a build marks the image as pending and the plan as not exact.
- Testability. Clients are built with
from_stubor pointed at a mock HTTP server in tests, so request construction, error handling and reconciliation are checked against the real SDK request types offline.