Skip to content

feat(migrate): studio setup beside a live move: install url, grant status, signed setup callback - #403

Merged
ABB65 merged 4 commits into
mainfrom
feat/migrate-studio-install
Oct 3, 2026
Merged

ABB65 merged 4 commits into
mainfrom
feat/migrate-studio-install

Conversation

@ABB65

@ABB65 ABB65 commented Oct 3, 2026

Copy link
Copy Markdown
Member

Studio side of E4 (W39): the customer installs Studio's GitHub App and signs in to Studio while their Migrate move runs. Migrate side is Contentrain/migrate#355.

Routes (Migrate server-to-server, signed like account-state, jti single-use per purpose, keyed by order_id; contract in @contentrain/types 1.42.0)

  • POST /api/migrate/grants/status -> { state: claimed|bound|redeemed|revoked, installed }. installed only once redeemed. Unknown order = 404. (revoked has no stored status yet; it arrives with the revoke work.)
  • POST /api/migrate/grants/install-url -> { url, expires_at }: https://github.com/apps//installations/new?state=<HS256 JWS, 10 min>. Only when the grant is redeemed and the workspace has no installation; the state names grant, workspace and owner, no repository (none exists during the move).

Setup callback (server/api/github/setup.get.ts)

  • A state shaped like our JWS goes to handleMigrateInstallCallback; a workspace-id state takes the old path unchanged (the session middleware lets only the token shape through unauthenticated; tests prove a workspace-id state still needs a session and the old route tests pass untouched).
  • Flow: verify state, check grant (redeemed, workspace matches), take the state jti once, exchange the authorization code for the installer's identity, GitHub confirms they can reach the installation, 409 if another workspace holds it, bind. If the installer is the grant owner's GitHub account: session + redirect to the workspace. Otherwise (e.g. an org admin): bind, no session, payer is sent to sign in then the claim screen. No code (OAuth-during-install off): nothing is bound.
  • getMigrateGrantByOrderId added to the DatabaseProvider (both implementations, contract test).

Needs before staging

  • NUXT_MIGRATE_INSTALL_STATE_KEY (>=32 chars; route is off without it).
  • Studio GitHub App: 'Request user authorization during installation' on, with /api/github/setup the first callback URL.

Tests: unit, integration (477) and nuxt (274) green; typecheck and eslint clean. Under this machine's load a few unit files hit their 5 s timeouts and pass alone with a longer one (unrelated).

Deliberately not here: provision/claim core and Polar (t8).

@ABB65

ABB65 commented Oct 3, 2026

Copy link
Copy Markdown
Member Author

REVISE (t8, f94cc00): migrate-install-callback overwrites a workspace's existing, different GitHub installation (the Number(workspace.github_installation_id) !== installationId branch also fires when A is held and B arrives), which orphans projects connected through A. Re-read the workspace in the callback; if an installation is set and differs, refuse (409) and bind nothing; equal stays idempotent. Add a test (A held, B arrives). Non-blocking: redirect 403/409 failures to the claim screen instead of a raw JSON error; do not log the install URL. Rest of the review (state signing, middleware shape split, jti ordering, non-owner installer) is fine.

@ABB65

ABB65 commented Oct 3, 2026

Copy link
Copy Markdown
Member Author

ONAY (t8, e64b9da). Blocking item fixed (a held installation is never swapped; test covers A held, B arrives). Local checks at this head: eslint 0 errors (7 pre-existing warnings), nuxt typecheck exit 0, test:unit 1947 passed / 1 skipped, test:integration 486/486, test:nuxt 274/274 (--testTimeout=90000 under load). Merge note: types stay pinned to published 1.42.0, and NUXT_MIGRATE_INSTALL_STATE_KEY (>= 32 chars) must be set on staging/prod before Migrate asks for install URLs (unset = 404 migrate.unavailable).

@ABB65

ABB65 commented Oct 3, 2026

Copy link
Copy Markdown
Member Author

ONAY delta (t8, b2c2621). Compared with my approved e64b9da, only tests/contract/migrate-grants.contract.test.ts changed (6+/6-): the by-order lookup test moved after the claim test, which needs a fresh order (created=true). Reviewed the diff: pure reordering, no product code. I did not re-run the contract suite myself (needs postgres:16); I rely on t7's 13/13 against a local postgres:16 and the postgres-lineage CI. Merge condition: that CI job must be green at this head.

@ABB65

ABB65 commented Oct 3, 2026

Copy link
Copy Markdown
Member Author

ONAY (t8, e64b9da). Blocking item fixed (held installation is never swapped; test covers A held, B arrives). Local checks at this head: eslint 0 errors (7 pre-existing warnings), nuxt typecheck exit 0, test:unit 1947 passed / 1 skipped, test:integration 486/486, test:nuxt 274/274 (--testTimeout=90000 under load). Merge note: types must stay pinned to the published 1.42.0, and NUXT_MIGRATE_INSTALL_STATE_KEY (>= 32 chars) has to be set on staging/prod before Migrate asks for install URLs (unset = 404 migrate.unavailable).

@ABB65
ABB65 merged commit 8c6e5cd into main Oct 3, 2026
2 checks passed
@ABB65
ABB65 deleted the feat/migrate-studio-install branch October 3, 2026 14:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant