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-pr-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 { return { owner: slug.owner, repo: slug.repo, ...(slug.host ? { host: slug.host } : {}) } } export type GitHubPrReadOutcome = { 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([ '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(['github.prChecks']) export function buildGithubPrParams( method: string, worktreeId: string, params: Record, options?: { prRepo?: GitHubPrRepoSlug | null; headSha?: string | null } ): Record { const built: Record = { 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( client: Pick, method: string, params: Record, parse: (value: unknown) => T ): Promise> { 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, worktreeId: string ): Promise> { return sendGithubPrRead( client, 'github.repoSlug', buildGithubPrParams('github.repoSlug', worktreeId, {}), (value) => { if (!value || typeof value !== 'object') { return null } const record = value as Record 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, worktreeId: string, args: { branch: string; linkedGitHubPR?: number | null } ): Promise> { 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, worktreeId: string, args: { branch: string; linkedPRNumber?: number | null } ): Promise> { 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, worktreeId: string, args: { prNumber: number } ): Promise> { return sendGithubPrRead( client, 'github.workItemDetails', buildGithubPrParams('github.workItemDetails', worktreeId, { number: args.prNumber, type: 'pr' }), readWorkItemDetails ) } export async function fetchPRChecks( client: Pick, worktreeId: string, args: { prNumber: number; headSha?: string | null; prRepo?: GitHubPrRepoSlug | null } ): Promise> { 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, worktreeId: string, args: { checkRunId?: number workflowRunId?: number checkName?: string url?: string | null prRepo?: GitHubPrRepoSlug | null } ): Promise> { const params: Record = {} 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, worktreeId: string ): Promise> { return sendGithubPrRead( client, 'github.listAssignableUsers', buildGithubPrParams('github.listAssignableUsers', worktreeId, {}), readAssignableUsers ) }