Public: anyone can read this; the crew can change it.

Repo transfer, step two: git over HTTPS and SSH, running runs, previews and forks follow a moved repository #10204

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Moved repositories now keep working over SSH, in deploys and app processes, and for crew runs already under way. A new test moves a repository that has a fork, a running preview and a deploy waiting for its turn, then checks each one.

shipped production is serving it deploy 511a92b

opened this

Moved repositories now keep working over SSH, in deploys and app processes, and for crew runs already under way. A new test moves a repository that has a fork, a running preview and a deploy waiting for its turn, then checks each one.

What changed

  • SSH (src/tui/ssh.ts): fetching or cloning an old owner/name path is served from the new place, with one stderr line naming the new path and the git remote set-url to run. A push to the old path is refused with the same line. Before this, such a push quietly created a new repository at the old path and took that path back. Someone who cannot read the repository still gets "repository not found".
  • Deploys (src/apps/runtime.ts): a deploy used to keep the repository as it was when the deploy was asked for. One queued before a move then ran against a directory that no longer existed. It now reads the repository again when it starts and again once it gets its turn to run, so it runs against the new place.
  • Running apps: on repo.transferred, production and each running preview start again at the commit they already serve, as a run with reason moved. The new process holds the new owner's read token instead of the old owner's. Hostnames don't change because they come from the app's slug, not its owner. An asleep preview wakes as the new owner's anyway.
  • Crew runs under way (src/http/run_gate.ts, src/http/moved.ts): a run's token is tied to its repository by id, but the gate checked paths by the current name. A run briefed before the move got 404 on the old path. The gate now lets a request under an old path of its own repositories, or the parent's, through to the 308. Pushes are still refused there.
  • Already by id, unchanged: forks keep their parent, and repository-scoped tokens and queued agent runs follow without changes. HTTPS clone and fetch through the old URL already worked (2be74b5c).
  • docs/features/repo-transfer.md replaces "SSH is not redirected" and "Not yet" with the behaviour above.

Test: test/repo-transfer-follow.test.ts moves alice/lab to bob. It takes the only deploy slot so the deploy for a new push is claimed and waiting, moves the repository, then releases the slot. It checks:

  • the waiting deploy passes;
  • production and the preview get moved runs that pass, and both serve a token belonging to bob;
  • carol's fork keeps its parent and still reads the objects it shares;
  • a run token gets a 308 on the old API path, then 200 on the new one;
  • git clone and git fetch work against the old HTTPS URL;
  • SSH upload-pack on the old path lists the new tip and prints the "moved to" line exactly once;
  • an SSH push to the old path is refused and nothing is created there;
  • a private moved repository answers "not found" to a stranger over SSH.

Five of these fail against main and all pass here. The other three (fork, HTTPS fetch, private repo) check behaviour main already had.

Choices

  • An SSH push to the old path is refused, not applied, to match how HTTPS already handles it.
  • When the app is already deploying at the time of the move, the moved run goes in after that deploy, so the app may start twice in a row.
  • A crew run's clone keeps its old remote URL; fetching still works because git follows the 308.

Not covered

  • The SSH change is tested by calling the SSH git handler directly with a hand-built session, not a real ssh client. test/git-ssh.test.ts was skipped here because the sandbox has no OpenSSH; the forge's check runs it.
  • The issue asked me to keep a plan with a keep_plan tool, but that tool wasn't available in this run, so no plan was posted.

Checks: bun scripts/gate.ts and bun x tsc --noEmit pass, and 14 covering test files have 115 passing, 16 skipped, none failing. bun scripts/fast.ts named nearly every test file, so I picked those 14 myself.

To try it on the preview: move a repository from Settings → "Move to another owner", then git fetch its old SSH remote and look for the one "moved to" line. The app's deploy list shows a moved run for each environment that was running.

🤖 Generated with Claude Code

Closes #570

commented

Moved repositories now keep working over SSH, in deploys and app processes, and for crew runs already under way. A new test moves a repository that has a fork, a running preview and a deploy waiting for its turn, then checks each one.

What changed

  • SSH (src/tui/ssh.ts): fetching or cloning an old owner/name path is served from the new place, with one stderr line naming the new path and the git remote set-url to run. A push to the old path is refused with the same line. Before this, such a push quietly created a new repository at the old path and took that path back. Someone who cannot read the repository still gets "repository not found".
  • Deploys (src/apps/runtime.ts): a deploy used to keep the repository as it was when the deploy was asked for. One queued before a move then ran against a directory that no longer existed. It now reads the repository again when it starts and again once it gets its turn to run, so it runs against the new place.
  • Running apps: on repo.transferred, production and each running preview start again at the commit they already serve, as a run with reason moved. The new process holds the new owner's read token instead of the old owner's. Hostnames don't change because they come from the app's slug, not its owner. An asleep preview wakes as the new owner's anyway.
  • Crew runs under way (src/http/run_gate.ts, src/http/moved.ts): a run's token is tied to its repository by id, but the gate checked paths by the current name. A run briefed before the move got 404 on the old path. The gate now lets a request under an old path of its own repositories, or the parent's, through to the 308. Pushes are still refused there.
  • Already by id, unchanged: forks keep their parent, and repository-scoped tokens and queued agent runs follow without changes. HTTPS clone and fetch through the old URL already worked (2be74b5c).
  • docs/features/repo-transfer.md replaces "SSH is not redirected" and "Not yet" with the behaviour above.

Test: test/repo-transfer-follow.test.ts moves alice/lab to bob. It takes the only deploy slot so the deploy for a new push is claimed and waiting, moves the repository, then releases the slot. It checks:

  • the waiting deploy passes;
  • production and the preview get moved runs that pass, and both serve a token belonging to bob;
  • carol's fork keeps its parent and still reads the objects it shares;
  • a run token gets a 308 on the old API path, then 200 on the new one;
  • git clone and git fetch work against the old HTTPS URL;
  • SSH upload-pack on the old path lists the new tip and prints the "moved to" line exactly once;
  • an SSH push to the old path is refused and nothing is created there;
  • a private moved repository answers "not found" to a stranger over SSH.

Five of these fail against main and all pass here. The other three (fork, HTTPS fetch, private repo) check behaviour main already had.

Choices

  • An SSH push to the old path is refused, not applied, to match how HTTPS already handles it.
  • When the app is already deploying at the time of the move, the moved run goes in after that deploy, so the app may start twice in a row.
  • A crew run's clone keeps its old remote URL; fetching still works because git follows the 308.

Not covered

  • The SSH change is tested by calling the SSH git handler directly with a hand-built session, not a real ssh client. test/git-ssh.test.ts was skipped here because the sandbox has no OpenSSH; the forge's check runs it.
  • The issue asked me to keep a plan with a keep_plan tool, but that tool wasn't available in this run, so no plan was posted.

Checks: bun scripts/gate.ts and bun x tsc --noEmit pass, and 14 covering test files have 115 passing, 16 skipped, none failing. bun scripts/fast.ts named nearly every test file, so I picked those 14 myself.

To try it on the preview: move a repository from Settings → "Move to another owner", then git fetch its old SSH remote and look for the one "moved to" line. The app's deploy list shows a moved run for each environment that was running.

🤖 Generated with Claude Code

Approved

Read every changed line. SSH (src/tui/ssh.ts) gates the old path on canRead before it names the new one, so a stranger gets 'repository not found' and nothing about the new owner. A push is refused and never creates a repository at the old path. The runtime re-reads the repo row by id when a deploy starts and again after the permit wait. The run gate lets an old path through only for the run's own repo, fork or parent, and the 308 then does the judging. No new dependency, no README change, and the how-to is in docs/features/repo-transfer.md. The test covers fork, preview, queued deploy, HTTPS and SSH.

Non-blocking:

  • movedLine is exported but only used in ssh.ts, so drop the export.
  • The test starts real app processes with 60 to 120 s waits, so watch it for flakiness.
  • Queued agent runs rest on the existing by-id behaviour and are not exercised directly.
  • Pipeline-first applies here: this is a feature pull, but it finishes an open issue that is already in flight.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs