Files
orca/src/main/project-runtime-git-options.ts
T
c34b944136 feat(github): bind projects to a specific gh account (#13664)
* feat(github): bind projects to a specific gh account

Adds per-project `Repo.ghAccount` so repo-scoped gh calls (create-worktree
issue/PR search, work items, hosted-review reads and mutations) run as the bound
account via ephemeral child-env token injection instead of the globally active
gh login. Multi-account resolution is capability-gated (gh >= 2.40) and fails
closed when the bound account or host is unavailable; Project View stays
ambient by design.

Repository settings gains a section for selecting or clearing a keyring-backed
account (shadcn `Select`), with mixed-version "not enforced" handling for older
remote runtimes. Attached `-Rhost/owner/repo` forms are covered by the host-drift
guard and its tests; es/ja/ko/zh catalogs carry the section's strings.

`getLocalProjectGhExecOptions` centralizes the binding lookup so every gh
execution path picks it up, including the Electron `hostedReview:*` handlers
that previously stayed on the ambient login. `gh auth token` (a keyring read)
is exempt from the rate-limit breaker gate so a tripped bucket cannot turn a
bound-token resolve into a false "unavailable".

The `ghAccount` update field and the two binding RPC methods live in the shared
RPC params contract; the generated catalog is regenerated.

Fixes #13612

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012B3QEP5iP4WGGEpPLtkHqA

* fix(settings): make GitHub account refresh secondary

* fix(github): satisfy strict casting quality checks

* test(rpc): use runtime fixture for repo binding

* fix(github): preserve project account for PR worktree lookups

* test(rpc): avoid incomplete runtime settings fixture

* fix(i18n): add GitHub account refresh label

* fix(i18n): refresh runtime required catalog

* fix(windows): preserve mobile patch bytes

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Neil <neil@stably.ai>
2026-09-18 20:39:52 -07:00

133 lines
4.3 KiB
TypeScript

import type { Store } from './persistence'
import type { Repo } from '../shared/repo-types'
import { isFolderRepo } from '../shared/repo-kind'
import {
resolveLocalProjectRuntimeForRepo,
type ProjectRuntimeResolutionStore
} from './local-project-runtime-resolution'
import type { ProjectExecutionRuntimeResolution } from '../shared/project-execution-runtime'
export {
resolveLocalProjectRuntimeForRepo,
resolveLocalProjectRuntimesForRepos
} from './local-project-runtime-resolution'
export type LocalProjectGitExecOptions = {
cwd: string
wslDistro?: string
}
export type LocalProjectWorktreeGitOptions = {
wslDistro?: string
}
export type LocalProjectGhExecOptions = LocalProjectWorktreeGitOptions & {
ghAccount?: Repo['ghAccount']
}
export function getLocalProjectGitExecOptions(
store: Store,
repo: Repo
): LocalProjectGitExecOptions {
// Why: local git must run in the same resolved project runtime as agents,
// terminals, and preflight; repair states must not silently fall back to host git.
return getLocalProjectGitExecOptionsForRuntime(
repo,
resolveLocalProjectRuntimeForRepo(store, repo)
)
}
function getLocalProjectGitExecOptionsForRuntime(
repo: Repo,
projectRuntime: ProjectExecutionRuntimeResolution | undefined
): LocalProjectGitExecOptions {
if (!projectRuntime) {
return { cwd: repo.path }
}
if (projectRuntime.status === 'repair-required') {
throw new Error(
`Project runtime requires repair before git execution: ${projectRuntime.repair.reason}`
)
}
if (projectRuntime.runtime.kind === 'wsl') {
return { cwd: repo.path, wslDistro: projectRuntime.runtime.distro }
}
return { cwd: repo.path }
}
export function getLocalProjectWorktreeGitOptions(
store: Store,
repo: Repo
): LocalProjectWorktreeGitOptions {
const { wslDistro } = getLocalProjectGitExecOptions(store, repo)
return wslDistro ? { wslDistro } : {}
}
/**
* Execution options for repo-scoped gh calls: the project's WSL routing plus its account binding.
*
* Why: every gh call site must resolve options through here — one that reaches for
* `getLocalProjectWorktreeGitOptions` instead silently runs as the ambient login.
*/
export function getLocalProjectGhExecOptions(store: Store, repo: Repo): LocalProjectGhExecOptions {
return {
...getLocalProjectWorktreeGitOptions(store, repo),
...(repo.ghAccount ? { ghAccount: repo.ghAccount } : {})
}
}
/**
* Git routing for the speculative worktree-create warm-up.
*
* Deliberately non-throwing where `getLocalProjectWorktreeGitOptions` throws: an
* optimistic prefetch must not report a repair-required runtime as a failure, so
* an unresolved runtime falls back to the host Git the warm-up used before
* routing existed.
*/
export function getWorktreeCreatePrefetchGitOptions(
store: Store,
repo: Repo
): LocalProjectWorktreeGitOptions {
if (isFolderRepo(repo)) {
return {}
}
const projectRuntime = resolveLocalProjectRuntimeForRepo(store, repo)
if (!projectRuntime || projectRuntime.status !== 'resolved') {
return {}
}
return getLocalProjectWorktreeGitOptionsForRuntime(repo, projectRuntime)
}
export function getLocalProjectWorktreeGitOptionsForRuntime(
repo: Repo,
projectRuntime: ProjectExecutionRuntimeResolution | undefined
): LocalProjectWorktreeGitOptions {
// Why: callers that already batch-resolved project runtimes must not rescan
// every project once per repo on a polling path.
const { wslDistro } = getLocalProjectGitExecOptionsForRuntime(repo, projectRuntime)
return wslDistro ? { wslDistro } : {}
}
/**
* Distro whose filesystem this repo's worktrees belong on, or undefined.
*
* Deliberately non-throwing where `getLocalProjectGitExecOptions` throws: a
* runtime that needs repair must not block creating a worktree, it just falls
* back to the Windows-side placement that has always been used.
*/
export function getWorktreeMirrorDistro(
store: ProjectRuntimeResolutionStore,
repo: Repo
): string | undefined {
return getWorktreeMirrorDistroForRuntime(resolveLocalProjectRuntimeForRepo(store, repo))
}
export function getWorktreeMirrorDistroForRuntime(
projectRuntime: ProjectExecutionRuntimeResolution | undefined
): string | undefined {
if (!projectRuntime || projectRuntime.status !== 'resolved') {
return undefined
}
return projectRuntime.runtime.kind === 'wsl' ? projectRuntime.runtime.distro : undefined
}