feat(orb): initiate APR repo transfer to a customer account - #7655
Conversation
…d#7638) Implements JSONbored#7590's decision that an APR (auto-provisioned) repo, created under a loopover-controlled org, can later be transferred to the customer's own account via GitHub's standard repository-transfer flow. Adds initiateAprRepoTransfer(env, installationId, repoFullName, newOwner): it mints the App installation token (same source as APR repo creation, JSONbored#7637) and calls POST /repos/{owner}/{repo}/transfer with new_owner. GitHub transfers are asynchronous and acceptance-gated -- the recipient must accept via a confirmation email -- so the function and its return type model a successful call as "transfer INITIATED, not complete", documented explicitly so nothing built on top mistakes acceptance for completion. Scope is exactly the initiation call: detecting acceptance/expiry, any customer UI, and the policy of when a transfer is offered are out of scope (separate follow-ons). It is a total function -- an API error (target account doesn't exist -> 422; caller lacks admin -> 403) comes back as a structured `{ initiated: false, status, error }` rather than throwing -- so callers branch instead of catch. Tests (mocked GitHub API, no live calls): a successful 202 initiation with the pending destination path; a 202 with an empty/omitted body (initiated, destination unknown); a 422 target-not-found and a 403 no-admin error, each handled without an unhandled exception, the 403 exercising the empty-body fallback message. The installation token is mocked to an opaque string -- no key material appears in the diff.
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #7655 +/- ##
==========================================
- Coverage 88.51% 88.50% -0.02%
==========================================
Files 724 725 +1
Lines 76020 76027 +7
Branches 22629 22630 +1
==========================================
- Hits 67291 67287 -4
- Misses 7681 7688 +7
- Partials 1048 1052 +4
Flags with carried forward coverage won't be shown. Click here to find out more.
|
|
Warning ⏸️ LoopOver review result - manual review recommendedReview updated: 2026-07-21 06:59:19 UTC
Review summary Nits — 5 non-blocking
Decision drivers
Context & advisory signals — never blocks the verdict
Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
Closes #7638
Summary
Implements #7590's decision that an APR (auto-provisioned) repo — created under a loopover-controlled GitHub org (#7637) — can later be transferred, on explicit customer request, to the customer's own account, using GitHub's standard repository-transfer flow.
Adds
initiateAprRepoTransfer(env, installationId, repoFullName, newOwner)insrc/orb/apr-repo-transfer.ts. It mints the App installation token (the same token source as APR repo creation) and calls GitHub'sPOST /repos/{owner}/{repo}/transferwithnew_owner.Correctly models an asynchronous, acceptance-gated transfer
GitHub transfers do not complete synchronously — the recipient must accept via a confirmation email within a time window. So a successful API response means the transfer was initiated, not completed. The return type encodes exactly that distinction, and the function carries a doc comment stating it explicitly, so anything built on top never mistakes acceptance-pending for done:
A total function — errors come back, they don't throw
An API error is returned as a structured
{ initiated: false, status, error }rather than thrown, so callers branch on the result instead of wrapping every call in try/catch:422403Scope
Exactly the initiation call, per the issue. Out of scope (explicitly, per #7638): detecting when a pending transfer is accepted or expires, any customer-facing UI, and the policy of when a transfer should be offered. No repo-creation or provisioning logic here.
Scope
type(scope): short summaryConventional Commit format.CONTRIBUTING.mdand does not reintroduce GitHub Pages, VitePress,site/, orCNAME.Validation
git diff --checknpm run typecheck(zero new errors vs base)npm run test:coveragelocally;codecov/patch≥99% of changed lines AND branchesnpm run ui:openapi:checknpm run docs:drift-checknpm audit --audit-level=moderatePatch coverage: 100% (7/7 executable changed lines, every branch). The GitHub API is fully mocked — no live transfer calls — per the issue's test requirement.
Tests (
test/unit/orb-apr-repo-transfer.test.ts): a successful202initiation asserting the endpoint,POSTmethod,Bearerauth, and{ new_owner }body plus the pending destination path; a202with an empty/unparseable body (initiated, destination unknown); a2xxbody omittingfull_name(null destination); a422target-not-found and a403no-admin error — each handled without an unhandled exception, the403exercising the empty-body fallback message.If any required check was skipped, explain why:
Safety
422and403error paths are the negative-path tests for this GitHub App write.ui:openapi:checkconfirms no drift).UI Evidencesection. Not applicable — backend function only.UI Evidence
Not applicable — this is a single backend function plus its unit tests. No UI, frontend, docs, or extension surface is touched.
Notes
newFullNameis surfaced from GitHub's response as the pending destination path — informational only, since the move is not yet final when the call returns.