Files
orca/mobile/src/session/github-pr-rpc.ts
T
Neil 583ab1601b refactor(shared): group worktree, github, and linear modules into folders (#14437)
`src/shared` is a flat directory of ~1,150 entries. The worktree, github, and
linear domains accounted for 71 of them, so finding the module you wanted meant
scanning a wall of same-prefixed filenames.

Move each domain into its own folder and drop the now-redundant prefix:

    src/shared/github-pr-types.ts    -> src/shared/github/pull-request-types.ts
    src/shared/worktree-id.ts        -> src/shared/worktree/id.ts
    src/shared/linear-links.ts       -> src/shared/linear/links.ts

This follows the existing `network/` and `new-workspace/` convention in the
same directory, which also drop the prefix inside the folder.

Whole clusters move, including tests. Foldering only part of a domain would be
worse than flat: a reader would have to check both `github/` and the flat
directory, and `github-auth-types.ts` / `github-project-types.ts` are type
modules that belong with the rest. No files with these prefixes remain flat.

Import specifiers were rewritten by resolving each one to an absolute path and
recomputing it, not by string substitution, so the `@/../../shared/...` alias
forms are handled correctly. 501 specifiers across 298 files.

Two things `tsc` cannot catch, handled explicitly:

- `github-project-types.ts` carries its own `max-lines` bypass, so its baseline
  entry is REPOINTED to the new path rather than pruned. Pruning would drop the
  bypass and then flag the new path as a fresh violation. Ratchet stays at 345.
- `mobile/` is outside `pnpm typecheck` and cannot be typechecked here
  (`mobile/node_modules` is empty). Instead every relative specifier in the repo
  was resolved against the filesystem: 174 unresolved before this change and 174
  after — identical, so nothing broke in mobile either.

The pinned `tests/e2e/.cross-version-checkouts` fixtures are deliberately NOT
rewritten; they are a snapshot of an older release and still reference the old
paths.

Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted
first — these projects are `composite: true` and reuse stale caches).
2026-08-13 20:44:16 -07:00

263 lines
8.3 KiB
TypeScript

import type {
GitHubAssignableUser,
GitHubWorkItemDetails,
PRCheckDetail,
PRCheckRunDetails,
PRInfo
} from '../../../src/shared/types'
import type { HostedReviewInfo } from '../../../src/shared/hosted-review'
import {
normalizeGitHubPRForBranchOutcome,
type GitHubPRForBranchResponse
} from '../../../src/shared/github/pull-request-for-branch-outcome'
import type { RpcClient } from '../transport/rpc-client'
import type { RpcSuccess } from '../transport/types'
import { mobileRepoSelectorFromWorktreeId } from '../source-control/mobile-pr-create'
import {
readAssignableUsers,
readForBranch,
readPRCheckDetails,
readPRChecks,
readPRForBranch,
readWorkItemDetails
} from './github-pr-parsers'
// Re-export the defensive parsers so consumers (and tests) have a single entry
// point for the github.* PR RPC surface.
export {
readAssignableUsers,
readForBranch,
readPRCheckDetails,
readPRChecks,
readPRForBranch,
readWorkItemDetails
} from './github-pr-parsers'
// Why: a fork PR's head lives in a different owner/repo; the host's SlugRepo
// (`{ owner, repo }`) identifies it. Only a subset of github.* methods accept it.
// Why: `host` must survive the RPC boundary or GHES actions on the host fall
// back to a same-named github.com repo (src/shared/types.ts identity contract).
export type GitHubPrRepoSlug = { owner: string; repo: string; host?: string }
export function githubPrRepoSlugParam(slug: GitHubPrRepoSlug): Record<string, string> {
return { owner: slug.owner, repo: slug.repo, ...(slug.host ? { host: slug.host } : {}) }
}
export type GitHubPrReadOutcome<T> = { ok: true; result: T } | { ok: false; error: string }
// Why: `prRepo` remains method-asymmetric. Keep the RPC schema allow-list here
// so fork/GHES identity reaches every PR-scoped read or mutation that accepts it.
const METHODS_ACCEPTING_PR_REPO = new Set<string>([
'github.prChecks',
'github.prCheckDetails',
'github.rerunPRChecks',
'github.resolveReviewThread',
'github.setPRFileViewed',
'github.updatePRState',
'github.requestPRReviewers',
'github.removePRReviewers',
'github.mergePR',
'github.setPRAutoMerge',
'github.updatePRTitle',
'github.prComments',
'github.prFileContents',
'github.addPRReviewComment',
'github.addIssueComment',
'github.addPRReviewCommentReply'
])
// Why: only github.prChecks declares a `headSha` param (PullRequestCheckDetails
// does not), so headSha is forwarded just to that read. Check runs are commit-keyed.
const METHODS_ACCEPTING_HEAD_SHA = new Set<string>(['github.prChecks'])
export function buildGithubPrParams(
method: string,
worktreeId: string,
params: Record<string, unknown>,
options?: { prRepo?: GitHubPrRepoSlug | null; headSha?: string | null }
): Record<string, unknown> {
const built: Record<string, unknown> = {
repo: mobileRepoSelectorFromWorktreeId(worktreeId),
...params
}
if (options?.prRepo && METHODS_ACCEPTING_PR_REPO.has(method) && !('prRepo' in built)) {
built.prRepo = githubPrRepoSlugParam(options.prRepo)
}
if (options?.headSha && METHODS_ACCEPTING_HEAD_SHA.has(method) && !('headSha' in built)) {
built.headSha = options.headSha
}
return built
}
async function sendGithubPrRead<T>(
client: Pick<RpcClient, 'sendRequest'>,
method: string,
params: Record<string, unknown>,
parse: (value: unknown) => T
): Promise<GitHubPrReadOutcome<T>> {
try {
const response = await client.sendRequest(method, params)
if (!response.ok) {
return { ok: false, error: response.error?.message || `Request failed: ${method}` }
}
return { ok: true, result: parse((response as RpcSuccess).result) }
} catch (err) {
// Why: a transport drop or a parser throw must not escape as an unhandled
// rejection — normalize to the `{ ok:false, error }` contract callers expect.
return { ok: false, error: err instanceof Error ? err.message : `Request failed: ${method}` }
}
}
// Probes whether the worktree's repo has a GitHub remote (a non-null slug). Used
// to decide whether the dedicated PR-view icon is available — independent of
// whether the branch has an open PR.
export async function fetchGithubRepoSlug(
client: Pick<RpcClient, 'sendRequest'>,
worktreeId: string
): Promise<GitHubPrReadOutcome<GitHubPrRepoSlug | null>> {
return sendGithubPrRead(
client,
'github.repoSlug',
buildGithubPrParams('github.repoSlug', worktreeId, {}),
(value) => {
if (!value || typeof value !== 'object') {
return null
}
const record = value as Record<string, unknown>
const owner = record.owner
const repo = record.repo
const host = record.host
return typeof owner === 'string' && typeof repo === 'string'
? { owner, repo, ...(typeof host === 'string' && host ? { host } : {}) }
: null
}
)
}
export async function fetchHostedReviewForBranch(
client: Pick<RpcClient, 'sendRequest'>,
worktreeId: string,
args: { branch: string; linkedGitHubPR?: number | null }
): Promise<GitHubPrReadOutcome<HostedReviewInfo | null>> {
return sendGithubPrRead(
client,
'hostedReview.forBranch',
{
repo: mobileRepoSelectorFromWorktreeId(worktreeId),
branch: args.branch,
linkedGitHubPR: args.linkedGitHubPR ?? null,
// Why: the mobile PR sidebar is only ever open on the selected worktree,
// so it belongs in the host's fast re-check tier (#11532).
active: true
},
readForBranch
)
}
export async function fetchPRForBranch(
client: Pick<RpcClient, 'sendRequest'>,
worktreeId: string,
args: { branch: string; linkedPRNumber?: number | null }
): Promise<GitHubPrReadOutcome<PRInfo | null>> {
return sendGithubPrRead(
client,
'github.prForBranch',
buildGithubPrParams('github.prForBranch', worktreeId, {
branch: args.branch,
linkedPRNumber: args.linkedPRNumber ?? null
}),
(value) => {
const outcome = normalizeGitHubPRForBranchOutcome(value as GitHubPRForBranchResponse)
if (outcome.kind === 'upstream-error') {
throw new Error(outcome.message)
}
if (outcome.kind === 'no-pr') {
return null
}
const pr = readPRForBranch(outcome.pr)
if (!pr) {
throw new Error('GitHub returned an invalid pull request response.')
}
return pr
}
)
}
export async function fetchWorkItemDetails(
client: Pick<RpcClient, 'sendRequest'>,
worktreeId: string,
args: { prNumber: number }
): Promise<GitHubPrReadOutcome<GitHubWorkItemDetails | null>> {
return sendGithubPrRead(
client,
'github.workItemDetails',
buildGithubPrParams('github.workItemDetails', worktreeId, {
number: args.prNumber,
type: 'pr'
}),
readWorkItemDetails
)
}
export async function fetchPRChecks(
client: Pick<RpcClient, 'sendRequest'>,
worktreeId: string,
args: { prNumber: number; headSha?: string | null; prRepo?: GitHubPrRepoSlug | null }
): Promise<GitHubPrReadOutcome<PRCheckDetail[]>> {
return sendGithubPrRead(
client,
'github.prChecks',
buildGithubPrParams(
'github.prChecks',
worktreeId,
{ prNumber: args.prNumber },
{ prRepo: args.prRepo, headSha: args.headSha }
),
readPRChecks
)
}
export async function fetchPRCheckDetails(
client: Pick<RpcClient, 'sendRequest'>,
worktreeId: string,
args: {
checkRunId?: number
workflowRunId?: number
checkName?: string
url?: string | null
prRepo?: GitHubPrRepoSlug | null
}
): Promise<GitHubPrReadOutcome<PRCheckRunDetails | null>> {
const params: Record<string, unknown> = {}
if (args.checkRunId !== undefined) {
params.checkRunId = args.checkRunId
}
if (args.workflowRunId !== undefined) {
params.workflowRunId = args.workflowRunId
}
if (args.checkName !== undefined) {
params.checkName = args.checkName
}
if (args.url !== undefined) {
params.url = args.url
}
return sendGithubPrRead(
client,
'github.prCheckDetails',
buildGithubPrParams('github.prCheckDetails', worktreeId, params, { prRepo: args.prRepo }),
readPRCheckDetails
)
}
export async function fetchAssignableUsers(
client: Pick<RpcClient, 'sendRequest'>,
worktreeId: string
): Promise<GitHubPrReadOutcome<GitHubAssignableUser[]>> {
return sendGithubPrRead(
client,
'github.listAssignableUsers',
buildGithubPrParams('github.listAssignableUsers', worktreeId, {}),
readAssignableUsers
)
}