feat(web): list the user's GitHub repositories, and give plans a repository

Merged
DA
11d ago
+1925-291

Second GitHub PR on the coding-agents branch, over B1's GitHubApp service (#1863). Server only: the desktop's repository chip is B4, and planning in a sandbox is B3.

What

GET /api/github/repositories (server/github-repositories-app.ts, routed through route-effect.ts and vercel.json): under the account bearer, every repository the signed-in user reaches through the Luke GitHub App, read from GitHub on the user's own token the moment it is asked for and stored nowhere. The answer (packages/hosted/src/github-repositories-wire.ts) is installed, repositories (owner, name, full name, default branch, private, updatedAt), most recently updated first, and installationUrl from B1, which is where the desktop sends an account with nothing installed.

plan.repository: a nullable text column holding owner/name as GitHub spells it (migration 0063_plan_repository, folded into plan-schema.ts; drizzle-schema.test.ts compares the two). The wire field is on every plan summary and plan answer in @sidecar/hosted/plan-wire. POST /api/plans takes an optional repository; PATCH /api/plans/{id} is now planUpdateRequestSchema (name, repository, or both; null clears), and a rename is one such update. The 13 existing plans stay null, which is the ordinary "no repository yet" state and no error.

Verify before save. A repository is kept only once GitHubApp.userRepository confirms the account reaches it: only the installation on the owner can reach it, so only that one is paged, the name is matched the way GitHub folds case, and what is stored is GitHub's spelling. A repository is reachable only through an installation; a public repository the token could read without one is deliberately not, since what the repository is for needs the installation's permissions.

Refusals, two new slugs in the hosted vocabulary with goldens:

  • github-sign-in-required (403): the account never signed in with GitHub, signed in before the App, or GitHub retired or revoked its token (a 401 on the user token, B1's TOKEN_REVOKED).
  • repository-not-reachable (403): the App reaches no such repository for the account. Nothing is written, the name beside it included.
  • A GitHub or store the service could not read stays unavailable.

On B1's service

userInstallations keeps B1's short-page paging and the revoked-token reason; the paging is generalised into readPages, which userRepositories and userRepository share for GET /user/installations/{id}/repositories. The GitHub fake, the fixture settings, and the account-row builder move from tests/github-app.test.ts into tests/support/github-app-fake.ts, on B1's clock-as-it-stands rows, so the route tests stand on the same fake.

Goldens

Re-recorded under LUKE_UPDATE_FIXTURES=1: the plan wire schemas gain repository, planCreateRequestSchema its optional one, planUpdateRequestSchema is new, hostedErrorSchema lists the two slugs, and the three repositories-wire schemas and two refusal bodies are new.

Note for review

repository is a required key on plan answers, and the desktop's plan client reads answers refusing unknown keys, so a desktop from before this branch cannot open or list plans against a service that carries it. The branch already breaks those desktops at the merge (B3 removes the Mac folder path), and the branch ships with a desktop release, so this is noted rather than worked around.

Tests

Hermetic, over PGlite and a scripted GitHub: pagination across installations and pages, no installation, a revoked token, a repository on another owner's installation, an installation uninstalled between the two reads, the route's refusals, and the verify-before-save refusal leaving the row untouched. ./scripts/check.sh exits 0.

šŸ¤– Generated with Claude Code


Open workspace in Conductor


Devin Review