CRUMB a card from devarno-cloud

Loco Deployment State Machine

sparki intermediate 5 min read

ELI5

A deployment row in PostgreSQL has two state knobs — status and current_phase. The API combines them into one human-friendly word (queued, validating, deploying, healthy, failed, rolled_back) so callers don’t have to learn the internal taxonomy.

Technical Deep Dive

services/deploy-loco/src/models/deployment.rs ships DeploymentRow (DB shape) and DeploymentResponse (API shape). The translation lives in DeploymentRow::api_status. The Go side (subsystems/loco/types.go) carries a parallel enum used for engine-side bookkeeping.

DB Status Values (loco subsystem types.go)

ConstantString
StatusPendingpending
StatusValidatingvalidating
StatusDeployingdeploying
StatusHealthCheckhealth_check
StatusSuccesssuccess
StatusFailedfailed
StatusRolledBackrolled_back
StatusCanceledcanceled

API Status Translation

match (status, current_phase) {
("pending", Some("queued")) => "queued",
("pending", Some("validating")) => "validating",
("deploying", _) => "deploying",
("success", _) => "healthy",
("failed", _) => "failed",
("rolled_back", _) => "rolled_back",
_ => status.clone(),
}

State Diagram

stateDiagram-v2
[*] --> pending_queued: row insert
pending_queued --> pending_validating: pre-flight starts
pending_validating --> deploying: validation passes
pending_validating --> failed: validation rejects
deploying --> health_check: platform reports rollout done
health_check --> success: probes green
health_check --> failed: probes red
success --> rolled_back: manual rollback issued
failed --> rolled_back: auto rollback
deploying --> canceled: user cancel
pending_queued --> canceled: user cancel
success --> [*]
failed --> [*]
rolled_back --> [*]
canceled --> [*]

Class Diagram

classDiagram
class DeploymentRow {
+Uuid id
+Uuid project_id
+Option~Uuid~ build_id
+String environment
+String status
+Option~String~ current_phase
+Option~String~ platform
+Option~String~ deployment_url
+Option~String~ health_check_status
+Option~Uuid~ rollback_target_id
+Option~i32~ attempts
+Option~DateTime~ next_attempt_at
+api_status() String
+to_response() DeploymentResponse
}
class DeploymentResponse {
+Uuid id
+String environment
+Option~String~ strategy
+String status
+Option~String~ current_phase
+Option~String~ deployment_url
+Option~String~ health_check_status
}
DeploymentRow --> DeploymentResponse : "api_status()"

Why Two Fields

status is coarse (workflow state) and current_phase is fine-grained sub-state. Splitting them keeps the SQL filter cheap (index on status) while preserving the detail the dashboard needs. The API merges them so external callers do not have to JOIN the meaning.

Key Terms

  • status → coarse SQL-indexable workflow state column
  • current_phase → sub-state used to disambiguate pending (queued vs validating)
  • api_status → method that flattens the two columns to one string for the response
  • rollback_target_id → FK to the deployment this one rolled back to (nullable)

Q&A

Q: Why does API show healthy instead of success? A: Operators reading a dashboard care about service health, not job-completion semantics. The translation is one place — api_status() — so renaming is a one-line change.

Q: When does rolled_back appear? A: When a deployment that previously reached success (or failed with auto-rollback) has status flipped to rolled_back and rollback_target_id populated to point at the previous good deployment.

Q: Are attempts and next_attempt_at part of the API response? A: No — they are internal scheduling fields used by the deploy-loco worker to retry. Only the columns listed in DeploymentResponse are exposed.

Examples

A web-app polling GET /deployments/:id while a Railway deploy progresses sees: queuedvalidatingdeployinghealthy (terminal). Behind the scenes the row went pending+queued → pending+validating → deploying+rolling → success+null.

neighbors on the map