Skip to main content
Documentation

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
bash

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: null and 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 outputWhen
commit <first 10 chars>The release recorded a commit (DeepSpace source, normal path)
GitHub · owner/repoGitHub source - no commit, but the repository is known
DeepSpace source, no commit recordedDeepSpace source that recorded no commit (a --no-push deploy)
no source recordedGenuinely 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.

CodeWhat it meansThe fix
dirty_worktreeA 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_trunkYour 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_baseThe 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_bundleThe 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_unsyncedA deploy from a workspace whose exact HEAD has not been published with workspace sync.Run the refusal's workspace sync action, then redeploy.
rename_requiredThe 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_missingA 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_progressThe 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_progressAnother 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.
forbiddenThe 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#