feat(cloud): record every PR a box links

Merged
SA
20d ago
+14994-1

Why

The cloud list and sidebar look up a box's PR by cloud_workspaces.branch, the branch the box was created on. Agents move to new branches with one PR each, so every box except the open one showed a stale or missing PR, and only one. Opening the box "fixed" it because only the open box goes through the host fan-out, which knows its live branch.

What

This is part 1 of 2. It records the data; part 2 adds the read and switches desktop and mobile over to it.

  • Table: cloud_workspace_pull_requests (cloud_workspace_id, repository_id, pr_number, linked_at), plus an index on repository_id so a repository delete doesn't scan it. linked_at is when the box linked the PR, not when it was reported. It's append-only, with one row per PR a box has linked. A row survives its branch being deleted or reused. State still comes from github_pull_requests by (repository, number), which webhooks keep current.
  • Box: startSandboxPullRequestReporter runs only in a sandbox.
    • The PR runtime already keeps every PR each workspace has linked (workspace_pull_requests in host.db, append-only). The runtime announces each new link (onPullRequestLinked), and the reporter posts the PRs it hasn't sent yet: everything on boot, then on each link. Nothing polls.
    • It covers every repository checked out on the box, archived workspaces included, and retries a failed send with backoff from 5s up to 5 minutes.
  • API: a new mutation, cloudWorkspace.reportPullRequests({ id, pullRequests }). The box calls it through host-service's API client with its sandbox credential (the one the egress firewall adds), the same way it calls cloudWorkspace.setDescription. It's on SANDBOX_ALLOWED_PROCEDURES.
    • Like setDescription, anyone who can see the workspace may call it, and a box only for itself.
    • It only records PRs on repositories that workspace checked out. A box can't attach another repo's PRs.
    • Repeats change nothing. It sends a cloud_workspaces nudge only when it adds a row.
    • It answers with the repositories it ignored. A repo renamed or transferred after the box linked a PR is reported under its old name and dropped; that's a known limit, noted in the sandbox notes.
  • Notes: docs/cloud-sandbox-mismatches.md gets an entry.

Latency: a PR opened with gh pr create is linked on the PR runtime's next sweep, up to 5 minutes, and reported about a second after it is linked. The "Create PR" button links straight away, because it refreshes the runtime itself.

Migration safety

  • Production sizes: checked on a Neon branch of production: cloud_workspaces is 216 kB with 161 rows, github_repositories is 54 MB with about 155k rows. Neither is in the hot list.
  • No type change; only a new table. The two foreign keys briefly take SHARE ROW EXCLUSIVE on their parents. The new table is empty, so validation is instant.
  • Lock order: nothing writes the child table yet, so there are no in-flight inserts to deadlock against.
  • Timing: rebased onto main again; the migration is now 0162. Applied to a fresh Neon branch of production (at main's 0161) with lock_timeout=5s statement_timeout=60s: it was the only pending migration and took 223ms. The PR's preview Neon branch was deleted so CI recreates it.
  • No rehearsal under load: the migration locks only small tables that no job holds open (rule 4).
  • No tRPC change in this PR: waived on purpose. It adds one new mutation, cloudWorkspace.reportPullRequests; no existing input or output changes.
  • Identifier lengths: the primary key has an explicit name (the generated one was 72 characters). The foreign key names follow the existing pattern; 14 existing constraints already exceed 63 characters.

Verification

  • cloudWorkspace.reportPullRequests tests: it records PRs only for the workspace's own repositories and returns the rest in ignored, and a box can't report for another workspace. Each fails when its check is removed. The first also fails if the procedure is missing from the sandbox allowlist.
  • Reporter test: a PR whose report failed arrives on a retry, and a PR already reported is not sent again. It fails without the retry or without the sent tracking.
  • Lint is clean. Typecheck shows only better-auth typing errors from this workspace's install, none in files this PR touches. CI's Typecheck is the authority.
  • A real box reporting. That needs a host-service release.

Summary by cubic

Records every PR a box links so the cloud list and sidebar no longer show a stale or missing PR when the box moves to a new branch.

  • cloud_workspace_pull_requests stores one append-only row per linked PR (workspace, repository, PR number, linkedAt); the box reports them via tRPC as itself, retrying failed sends with backoff up to 5 minutes.
  • The box reports all unsent PRs on boot and each new link, including from archived workspaces; the API records only PRs for the workspace's own checked-out repositories, clamps a reported linkedAt to now, refuses reports for deleted workspaces, and nudges clients only when a row is added.

The client read that switches desktop and mobile to this table lands in a follow-up.

Summary by CodeRabbit

  • New Features

    • Cloud workspaces now report linked pull requests, including pull requests created from branches outside the configured workspace branch.
    • Pull request links are matched to checked-out repositories and retained with their association to the workspace.
    • Reporting runs automatically in the background, with batching, periodic updates, and retry handling when communication fails.
  • Documentation

    • Added guidance on reporting closed pull requests, repository mismatches, update timing, and host-service requirements.