# Releases and rollback

The append-only release ledger, what a deploy records under each source mode, and how rollback works.

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.

```bash
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](/guides/source-control) - 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](/guides/source-control#selecting-or-transferring-authority).

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.

**`--allow-do-deletion` deletes data.** When the target release declares fewer Durable Object classes than the current one, rollback refuses, because completing it permanently deletes those classes' stored data. Before using the flag, spell out exactly which classes disappear and what data that destroys, and get the app owner's explicit approval. This is a data-loss decision, not a confirmation to click through.

## 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](/guides/workspaces) 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](/guides/app-identity#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](/guides/workspaces#the-refusal-contract). Build and secrets refusals (`secrets_config_missing`, reserved-name and binding collisions) are deploy mechanics rather than lineage - see [Deployment](/concepts/deployment) and the [secrets guide](/guides/secrets).

This ledger is your app's release history. For the SDK's own version history - what changed in each `deepspace` release - see the [changelog](https://deep.space/changelog).

## Next steps

* [Deployment](/concepts/deployment) - what a deploy does, and when it is actually live.
* [Command reference](/cli-reference/commands#releases) - flags for `releases`, `rollback`, and `deploy`.
* [Collaborators](/guides/collaborators) - who can ship and roll back.
* [Changelog](https://deep.space/changelog) - what shipped in each SDK release.
