Deployments
What a ZopDay deployment is, how revisions flow through the pipeline, and what survives a restart.
A deployment is the catalog row for one shippable thing; a repo + branch + config that ZopDay can roll out repeatedly. A revision is one rollout of that deployment; the specific image hash + Helm release that’s live or pending.
The split is deliberate: catalog data is stable and editable; revision data is append-only and immutable once shipped. Editing a deployment changes future rollouts; it doesn’t change the past.

A deployment environment canvas; each service with its git source, port, replica count, and health status.
Catalog vs runtime
| Layer | Service | What it owns |
|---|---|---|
| Catalog | Config service | projects, environments, deployments, deployment_spaces. Stable rows you create, edit, and delete. |
| Runtime | Deployer service | deployment_revisions (versioned rollouts + work queue), deployment_events (append-only timeline), service_connections (edges between deployments). |
The deployer is stateless with respect to the catalog: it reads catalog data from the config service and never keeps its own copy.
Deployment fields
| Field | Notes |
|---|---|
env_id | The parent environment. Defines the space, namespace, and registry. |
repo_url | Git URL of the source repo (HTTPS or SSH). |
branch | The branch to track. ZopDay only deploys this branch on auto-triggers; manual deploys can target any commit. |
current_revision_id | Pointer to the latest successfully shipped revision. Updated atomically when a revision goes active. |
status | idle / building / deploying / active / failed / dlq; derived from the latest revision. |
last_deploy_at | Timestamp of the last successful deploy. Drives the project list’s “last deploy” column. |
canvas_position | {x, y} for the architecture canvas. |
Infrastructure fields (which space, which namespace, which registry) hydrate from the parent environment via JOIN. They are not stored on the deployment itself; change the environment and every deployment inside it picks up the change.
Revisions and the work queue
Each revision doubles as a work-queue item, moving through these states:
pending → claimed → build_dispatched → pending → claimed → deploying → active | failed | dlqKeeping the version metadata and the work-queue state together avoids reconciliation issues if a worker restarts mid-deploy.
| Revision column | Purpose |
|---|---|
status | The state machine above |
delivery_id | Webhook idempotency. Replays of the same GitHub delivery are ignored and no-op. |
payload_snapshot JSON | The payload that triggered this revision; repo state, env, registry. Frozen at claim time so a mid-flight env edit doesn’t surprise the build. |
stage_marker | Which pipeline stage the worker last completed. Used for resume on pod restart. |
retry_count | How many times this revision has been retried. Caps at 3 before DLQ. |
claimed_at | When the current worker pod last claimed this row. Drives stale-lock recovery. |
config_only | True if this revision is a config-only redeploy (no rebuild). |
snapshot_at | When payload_snapshot was frozen. |
The pipeline (per revision)
Resolve → Clone → Detect → dispatch build → callback → Deploy → WatchRolloutEach stage emits an append-only event at its start and on success or failure. This is the same pipeline described in Pipelines and run in the Quickstart, viewed here per revision.
Resolve
Read the parent deployment + environment + space via configclient. Verify the space is reachable.
Clone
Git clone the repo at the configured branch. Fetches the git credentials over the same path ZopDay uses for all repo access.
Detect
Railpack walks the cloned repo to detect language, runtime version, and build approach. If a Dockerfile is present, it’s respected as-is; Railpack only takes over when there isn’t one.
Dispatch build
ZopDay dispatches the build to GitHub Actions, which handles the build entirely (not an in-cluster Kaniko build). After dispatch the revision is parked in a build-dispatched state so it isn’t re-picked before the build callback resumes it.
Callback (from builder)
The builder streams log chunks and terminal status back to ZopDay over a signed callback. On success, the deploy stage is queued.
Deploy
Render Helm chart with merged values (base + environment + branch overrides). Apply via the kubeauth-minted kubeconfig. Image hash pushed to the registry resolved from the environment (or the cloud default if no registry integration).
WatchRollout
Poll the Helm release status until the rollout reaches steady state. Health gates run here; pod readiness, probes, custom SLOs. See Pipelines for the gate detail.
On success → status='active', current_revision_id updated atomically.
On any stage failure → retry up to 3 times, then the revision moves to the DLQ.
Webhook idempotency
Each GitHub delivery ID is recorded on the revision. Replays of the same delivery are ignored: no duplicate revisions, no duplicate builds, even if GitHub re-fires the webhook hours later after a transient delivery error.
Rollback creates a revision
Rollback is not an edit to an existing revision; it’s the creation of a new revision that mirrors the target. The image hash on the new revision is the one that was already on the cluster, so no rebuild fires.
This keeps the revision history readable: every change to what’s running is its own row, with its own actor and timestamp.
See Rollbacks for the full rollback contract.
Teardown
Deleting a deployment triggers a teardown before the catalog row is soft-deleted. Teardown is:
helm uninstallof the release on the bound space’s namespace- Revision and event cleanup
- Idempotent; re-runs return
already_clean=true
If the cluster is unreachable when teardown fires, the catalog row remains in a pending_teardown state. Re-trigger from the UI once the cluster recovers; the teardown is idempotent so a partial first attempt won’t double-uninstall.
Editing a deployment
Common edits and their effects:
| Edit | Triggers new revision? |
|---|---|
repo_url change | No. Next manual or webhook trigger creates a new revision against the new repo. |
branch change | No. Same as above. |
Helm values change via PATCH /deployments/{id}/configs | Yes: config-only revision. config_only=true on the new row. No rebuild fires; the same image hash is applied with the new values. |
canvas_position change | No. Visual only. |
| Environment edits that change space/namespace/registry | No. Existing deployments flagged with divergence; re-deploy explicitly to land. |
Stateless w.r.t. credentials
Worth re-emphasising: the deploy pod holds zero standing credentials on target clusters. The kubeconfig is minted per-deploy via kubeauth, scoped to ~13–45 minutes depending on cloud, and discarded when the pipeline stage completes. There is no long-lived service account, no in-cluster agent, no kubeconfig file on disk past the deploy pod’s lifetime.
This matters when teams ask “what does ZopDay have access to on our cluster right now?”; the answer is: nothing standing. Only what was minted for the most recent active deploy, and that token has already expired.