# Source control

Source latches permanently on first source use: how DeepSpace and GitHub source work and what each release records.

Every app has **exactly one authoritative Git repository**, and you never declare it with a setting. A new app starts unclaimed. Its first source-producing action latches one source permanently: a deploy from a checkout with a GitHub remote claims that GitHub repository; a first `deepspace push`, or a deploy without a GitHub remote, claims DeepSpace source. There is no setter, unclaim, or transfer.

To see where an app stands:

```bash
npx deepspace app source --json     # read-only: reports, never changes
npx deepspace status --json         # sourceInference: what the NEXT deploy will do
```

## The two experiences

| Source        | Experience                                                                                                                                                                                                                                     |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **DeepSpace** | The packaged default. Commit-first: deploy publishes the attached clean branch automatically, and the `space` remote, `clone`/`push`/`pull`, [workspaces](/guides/workspaces), activity, releases, and rollback all work together.             |
| **GitHub**    | Manual Git ownership. A new app's first deploy from a checkout whose selected remote points at GitHub permanently claims that repository. Deploy ships the **local working tree** and performs no Git read, write, or credential verification. |

Under GitHub source, dirty and unpushed bytes are **valid deploy inputs**. The release records the claimed repository, source revision, and a dirty flag (`releases` shows `GitHub · owner/repo, dirty worktree`), but carries no DeepSpace commit id; rollback uses the retained deployment bundle. Commit discipline is yours — the platform respects that.

## How the first source use decides

An **unclaimed** app is decided once, at the first source latch:

* A deploy whose durable release reservation is accepted with a selected GitHub remote → permanently claims the observed repository. `app source` records revision 1 immediately; the completed release records the same revision.
* A `deepspace push` whose pack POST reaches the server, or a deploy whose durable release reservation is accepted with no GitHub remote → permanently claims DeepSpace source. This includes `deploy --no-push`: skipping source sync for that release does not leave authority undecided.

A **claimed** app ignores remotes entirely — whichever provider it claimed, that is the answer forever.

**The claim is the one one-way door.** For `push`, it fires at the git pack POST — the request that actually carries commits — never at the ref advertisement, so a `git push --dry-run` or a push your client abandons never pins the app. For `deploy`, it fires after request validation when the durable release reservation is accepted, before any route or provider mutation. The claim is not undone if a later deploy phase fails; retry the same deploy. Owner and collaborator actions can claim; an admin acting on behalf of an owner never decides source. To use a different source for the same code, register a new app (`deepspace app init --new-id`).

## `app source` is read-only

```bash
npx deepspace app source            # report: claimed source, or unclaimed
npx deepspace app source --json     # { source: {...} | null, revision, ... }
```

The old setter forms (`app source github`, `app source deepspace`) refuse with `source_inferred`; the retired public `POST /source` answers a 410 tombstone. Only the server's first-use paths can write source. The revision counter appears in `--json` surfaces (`revision` on `app source`, `sourceRevision` on release rows); on new apps it changes once, at the claim, and a latching deploy's release carries that winning revision.

## Working under GitHub source

* **Push with ordinary Git.** Branches, tags, PRs — all normal.
* **Deploy ships the working tree**, dirty bytes included; the release ledger labels dirty releases so a rollback never picks between clean and dirty blind.
* **Do not run `deepspace push` before the first GitHub deploy** — that would claim DeepSpace source permanently. After the first GitHub deploy, every DeepSpace source verb refuses from the stored claim with `source_managed_by_github` and names the repository.
* **Do not create or replace `origin` unprompted.** Wiring a repo to GitHub is the developer's call — and on an *unclaimed* app, adding or removing a GitHub remote changes what the next deploy does. When asked, use ordinary Git:

  ```bash
  git remote add origin git@github.com:<org>/<repo>.git
  git push -u origin main
  ```

**Never `git push --mirror`.** It acts on *every* ref — including refs that do not belong on GitHub. Push named branches and tags instead.

### The refusal the source verbs share

`push`, `pull`, `clone`, and the workspace verbs all refuse GitHub-owned apps from the permanent registry claim with one code, `source_managed_by_github`:

> This app uses GitHub source (`owner/repo`). Use normal Git/GitHub for source operations…

`--json` carries `repository` and `appId` alongside the code. The refusal ships **no** executable action: which Git command to run is the developer's call.

### A review branch is not a second trunk

A **claimed** DeepSpace-source app may still keep a review branch on GitHub — that is fine because remotes no longer change the claim. On an *unclaimed* app a GitHub remote is not inert: the next deploy whose durable release reservation is accepted claims GitHub permanently. One repository is the truth; the other may hold a copy for a purpose, but it is not a second authoritative trunk.

### CI deploys

For CI, inspect the current flags with `npx deepspace auth login --help` and `npx deepspace deploy --help` rather than copying stale snippets. Store credentials in the CI secret manager — never in argv, repository files, or workflow logs. A CI checkout's selected remote decides an unclaimed app's first deploy: keep the intended GitHub remote to claim GitHub, or remove it only when the first release should claim DeepSpace. A depth-1 DeepSpace-source clone refuses `shallow_repo`; unshallow it before deploying.

## Legacy declared-source apps

Apps that declared a source under v0.25 and earlier keep their recorded provider; a recorded GitHub app behaves exactly like a first-release-claimed one (same refusals, same manual deploys), and `deploy` removes its stale `space` remote for it. The transfer machinery those versions documented is gone.

## See also

* [Workspaces](/guides/workspaces) — the DeepSpace-source collaboration surface
* [App identity](/guides/app-identity) — what stays put regardless of source; `app init --new-id` to fork
* [Deployment](/concepts/deployment) — what a release records under each source
* [Command reference: `app source`](/cli-reference/commands#app-source)
