mirror of
https://github.com/stablyai/orca.git
synced 2026-09-26 16:02:43 +00:00
* 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>
66 lines
2.1 KiB
TypeScript
66 lines
2.1 KiB
TypeScript
/**
|
|
* Normalize persisted per-project GitHub account bindings.
|
|
*
|
|
* Why: host comparison and gh `--hostname` must be case-insensitive/canonical,
|
|
* while login casing is preserved for display and `gh auth token --user`.
|
|
*/
|
|
|
|
export type GhAccountBinding = {
|
|
host: string
|
|
user: string
|
|
}
|
|
|
|
const HOST_RE = /^[a-z0-9][a-z0-9.-]*(?::\d+)?$/i
|
|
// Why: GHES/LDAP logins often contain `_` or `.`; github.com's hyphen grammar is too strict here.
|
|
const USER_RE = /^\S{1,255}$/
|
|
|
|
export function normalizeGhAccountBinding(value: unknown): GhAccountBinding | null {
|
|
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
|
return null
|
|
}
|
|
const hostRaw = 'host' in value ? value.host : undefined
|
|
const userRaw = 'user' in value ? value.user : undefined
|
|
if (typeof hostRaw !== 'string' || typeof userRaw !== 'string') {
|
|
return null
|
|
}
|
|
const host = hostRaw.trim().toLowerCase()
|
|
const user = userRaw.trim()
|
|
// Why: reject embedded NUL without a control-char regex (oxlint no-control-regex).
|
|
if (!host || !user || user.includes('\u0000') || !HOST_RE.test(host) || !USER_RE.test(user)) {
|
|
return null
|
|
}
|
|
return { host, user }
|
|
}
|
|
|
|
/** Host-insensitive, login-exact comparison — matches how gh resolves `--hostname` and `--user`. */
|
|
export function ghAccountBindingsEqual(
|
|
a: GhAccountBinding | null | undefined,
|
|
b: GhAccountBinding | null | undefined
|
|
): boolean {
|
|
if (!a || !b) {
|
|
return a === b
|
|
}
|
|
return a.host === b.host && a.user === b.user
|
|
}
|
|
|
|
/**
|
|
* Choose the env var gh uses for a bound host.
|
|
*
|
|
* Why: github.com / github.localhost / *.ghe.com take `GH_TOKEN`; other hosts
|
|
* need `GH_ENTERPRISE_TOKEN`. Classifying `*.ghe.com` as cloud requires gh ≥
|
|
* 2.46 — older gh may ignore `GH_TOKEN` for those hosts and fail closed as
|
|
* `gh_bound_account_unavailable`.
|
|
*/
|
|
export function ghTokenEnvVarForHost(host: string): 'GH_TOKEN' | 'GH_ENTERPRISE_TOKEN' {
|
|
const normalized = host.trim().toLowerCase()
|
|
if (
|
|
!normalized ||
|
|
normalized === 'github.com' ||
|
|
normalized === 'github.localhost' ||
|
|
normalized.endsWith('.ghe.com')
|
|
) {
|
|
return 'GH_TOKEN'
|
|
}
|
|
return 'GH_ENTERPRISE_TOKEN'
|
|
}
|