Releases and rollback
The append-only release ledger, what a deploy records under each source mode, and how rollback works.
On this page
Every deploy appends a release fact to an append-only ledger - even a byte-identical redeploy. npx deepspace releases reads the ledger; npx deepspace rollback re-ships an earlier entry. Nothing in the ledger is ever edited or removed, which is what makes "what is live, and what was live before it" a question with a reliable answer.
npx deepspace releases # newest first, default 20; --limit N
npx deepspace releases --json # same ledger, machine-readable
npx deepspace rollback # defaults to the previous release
npx deepspace rollback rel_... # a specific release id
npx deepspace rollback rel_... --allow-do-deletion
What a release records depends on the source#
Release semantics follow the app's source mode - the release-record delta:
- DeepSpace source (commit-first): the release records the deployed commit, so the ledger doubles as source lineage - every release maps to an exact commit - and the workspace, ancestry, and stale-base guards below apply.
- GitHub source (ships the working tree): the release records
commitOid: nulland retains the configured repository and source revision as metadata.
--no-push skips source sync for one deploy - shipping without commit lineage - and never changes source authority; see source control.
Every successful deploy --json therefore also reports what tree it shipped, as branch and dirty. Under DeepSpace source those restate a guarded invariant (a dirty worktree is refused outright, and commitOid pins the code). Under GitHub source they are the only record there is: that path ships the working tree from whatever branch you are on, uncommitted edits included, and records no commit. So a GitHub-source deploy also says it on stderr before shipping - an informational line when the tree is clean, and a warning when it is dirty, because nothing afterwards can reconstruct what went live. Both are null when the app directory is not a usable Git repository, which is a fact rather than a failure: the deploy still ships.
How a release names its source#
releases, status, and activity describe a release's source with one shared formatter, so the three can no longer disagree about the same release:
| Human output | When |
|---|---|
commit <first 10 chars> | The release recorded a commit (DeepSpace source, normal path) |
GitHub · owner/repo | GitHub source - no commit, but the repository is known |
DeepSpace source, no commit recorded | DeepSpace source that recorded no commit (a --no-push deploy) |
no source recorded | Genuinely no source information |
Previously a GitHub-source release printed "no source recorded" in human output while its --json row carried the repository - the human surface was simply wrong about a release whose source was known.
Rollback re-ships a retained bundle#
Rollback does not rebuild and does not touch Git. It re-ships the target release's stored bundle exactly as deployed, and appends another release fact - history moves forward even when the code moves back, so the ledger records the rollback itself.
Bundles are retained for rollback, but not forever: storage pressure can evict an old bundle while its ledger row remains. The releases listing marks every entry's rollback availability (rollbackAvailable in --json); a rollback against an evicted bundle refuses with no_bundle - choose another retained release.
The refusal taxonomy#
Deploy and rollback refuse with stable codes. Branch on the code, never the prose.
| Code | What it means | The fix |
|---|---|---|
dirty_worktree | A DeepSpace-source deploy (sync on) found uncommitted changes. A deploy records the commit it ships, and there is none to record. | Commit - WIP commits are fine - and re-run. |
behind_trunk | Your branch is strictly behind the cloud repo: deploying would take already-landed work off the live app. | npx deepspace pull, then redeploy. --ignore-stale ships the older tree anyway - the live app reverts until the next up-to-date deploy. |
stale_base | The deploy can't be proven to contain the live release. Three causes share the code: a newer release landed while you worked; the commit being deployed was never synced to the cloud repo (auto-push failed, or --no-push with an unpushed commit); or - GitHub source - the live release changed after source verification. | The refusal's message names the recovery: npx deepspace push when the commit never synced; pull, integrate, and redeploy when a newer release landed; re-verify and redeploy on GitHub source. --ignore-stale skips the guard - only when replacing the live release is the intent. |
no_bundle | The target release's bundle was evicted or is unreadable; only its ledger row remains. | Roll back to a different release - releases --json marks which are rollbackAvailable. |
workspace_unsynced | A deploy from a workspace whose exact HEAD has not been published with workspace sync. | Run the refusal's workspace sync action, then redeploy. |
rename_required | The name in wrangler.toml no longer matches the host the registry serves, so this deploy would move the app's URL - and there is no terminal to confirm on. | Two different intents, which is why the refusal ships no executable action: re-run with --rename to approve the move, or npx deepspace app init --new-id if you meant a separate app. The refusal also states what a rename does not carry - see renaming an app. |
owner_jwt_missing | A collaborator (or platform admin) deploying an app the owner has never deployed. There is no live version carrying an APP_OWNER_JWT, so the platform has nothing to inherit the existing secrets from. Raised before the commit, not after a partial ship. | Ask the owner to deploy once. Nothing the collaborator can do locally fixes it. |
merge_in_progress | The worktree is mid-merge, -rebase, -cherry-pick, or -revert. HEAD is the pre-operation commit, so the deploy (or push/pull, which share the guard) would ship a tree carrying none of the in-flight work. | Finish (git merge --continue and friends) or abort, then retry. Two remedies, so no single action ships. |
deploy_in_progress | Another deploy of this checkout holds the local lock at .deepspace/deploy.lock - two deploys of one directory race on dist/. The refusal names the holder's pid and start time. A lock whose process is gone is reclaimed automatically. | Wait for it to finish. Remove the lock by hand only when no deploy is running. |
release_in_progress (exit 2) | Another deploy of the same app, from another checkout, is between prepared and live. This run built and uploaded but did not release; nothing is wrong and nothing needs changing. | Run the returned action - the same deploy again - after a moment. If it keeps refusing for more than a couple of minutes, look at deepspace releases. |
forbidden | The app belongs to another account. The sentence names the app id and the account you are signed in as. | Have the owner run app collaborators add <your email>, log in as the owner, or app init --new-id to publish the code as your own app. |
The table covers the lineage and concurrency refusals, not every code: rollback's Durable-Object guard refuses with do_class_verify_failed / do_class_deletion (the codes behind --allow-do-deletion above), and guarded source operations carry their own set - see the workspace refusal contract. Build and secrets refusals (secrets_config_missing, reserved-name and binding collisions) are deploy mechanics rather than lineage - see Deployment and the secrets guide.
This ledger is your app's release history. For the SDK's own version history - what changed in each deepspace release - see the changelog.
Next steps#
- Deployment - what a deploy does, and when it is actually live.
- Command reference - flags for
releases,rollback, anddeploy. - Collaborators - who can ship and roll back.
- Changelog - what shipped in each SDK release.