Files
orca/src/main/runtime/runtime-github-review-mutation-commands.ts
T
Neil 12bf32836b feat(ci): ratchet raw repo connectionId host checks; migrate the cheap ones (#31096)
* feat(ci): ratchet raw repo connectionId host reads; one drop-in resolver

Codex review 6 class fix (#4/#6/#7): host decisions read off a repo row's
legacy connectionId miss rows that name SSH only as executionHostId.

- getRepoRoutingConnectionId (shared/execution-host.ts): drop-in for a raw
  repo.connectionId routing check that also finds executionHostId-only SSH.
- check-repo-host-read-ratchet: per-file count of raw repo connectionId
  reads in src/main, src/shared, src/cli; counts may only shrink.
- Migrated 66 cheap reads: runtime GitHub/GitLab commands, PR/MR head ref
  fetches, preserved-branch cleanup, ref queries, base-ref IPC, worktree
  preparation, trash sweep, scan cache TTL, usage metadata.

* fix(ipc): PR base IPC routes executionHostId-only SSH rows; ratchet is a stated lower bound

Review follow-up (Opus on #31096):
- worktrees:resolvePrBase decided git/gh routing on the raw connectionId while
  its head-ref fetches already used the resolver; one resolved connectionId
  now drives all of it (test fails on main).
- getRepoRoutingConnectionId's contract is stated exactly: never local when the
  raw field is set; on a contradictory ssh row executionHostId wins.
- The ratchet regex no longer matches report.connectionId; header and docs
  say it is a lower bound.
- Regression tests for a runtime GitHub and GitLab command site.

* test(ci): pin the repo host-read ratchet step to the static phase

* fix(ci): repo host-read ratchet skips verbatim field copies into projected rows

A copy of connectionId into a projected row decides nothing; migrating it to
the resolver would change what the projection publishes. The sidebar feed
catalog's copy (added after the baseline) and two older projections no longer
count; baseline pruned to 91.
2026-10-20 14:00:32 -07:00

237 lines
6.5 KiB
TypeScript

import type { GitHubOwnerRepo } from '../../shared/github/pull-request-types'
import type { GitHubPullRequestStateUpdate } from '../../shared/issue-mutation-types'
import type { GitHubReactionContent } from '../../shared/github/comment-types'
import type { Repo } from '../../shared/repo-types'
import type { LocalProjectGhExecOptions } from '../project-runtime-git-options'
import {
mergePR,
markPRReadyForReview,
removePRReviewers,
requestPRReviewers,
rerunPRChecks,
resolveReviewThread,
setPRAutoMerge,
setPRCommentReaction,
setPRFileViewed,
updatePRDetails,
updatePRState,
updatePRTitle
} from '../github/client'
import { getRepoRoutingConnectionId } from '../../shared/execution-host'
type LocalGitArgs = [] | [LocalProjectGhExecOptions]
type RuntimeGitHubReviewMutationCommandsDeps = {
resolveRepo: (selector: string) => Promise<Repo>
getLocalGitArgs: (repo: Repo) => LocalGitArgs
}
export class RuntimeGitHubReviewMutationCommands {
constructor(private readonly deps: RuntimeGitHubReviewMutationCommandsDeps) {}
async rerunRepoPRChecks(
repoSelector: string,
prNumber: number,
options?: { headSha?: string; failedOnly?: boolean; prRepo?: GitHubOwnerRepo | null }
): Promise<Awaited<ReturnType<typeof rerunPRChecks>>> {
const repo = await this.deps.resolveRepo(repoSelector)
return rerunPRChecks(
repo.path,
prNumber,
options,
getRepoRoutingConnectionId(repo),
...this.deps.getLocalGitArgs(repo)
)
}
async setRepoPRCommentReaction(
repoSelector: string,
reactionSubjectId: string,
content: GitHubReactionContent,
reacted: boolean,
prRepo?: GitHubOwnerRepo | null
): Promise<Awaited<ReturnType<typeof setPRCommentReaction>>> {
const repo = await this.deps.resolveRepo(repoSelector)
return setPRCommentReaction(
repo.path,
reactionSubjectId,
content,
reacted,
getRepoRoutingConnectionId(repo),
prRepo ?? null,
...this.deps.getLocalGitArgs(repo)
)
}
async resolveRepoReviewThread(
repoSelector: string,
threadId: string,
resolve: boolean,
prRepo?: GitHubOwnerRepo | null
): Promise<Awaited<ReturnType<typeof resolveReviewThread>>> {
const repo = await this.deps.resolveRepo(repoSelector)
return resolveReviewThread(
repo.path,
threadId,
resolve,
getRepoRoutingConnectionId(repo),
prRepo ?? null,
...this.deps.getLocalGitArgs(repo)
)
}
async setRepoPRFileViewed(
repoSelector: string,
args: {
prRepo?: GitHubOwnerRepo | null
pullRequestId: string
path: string
viewed: boolean
}
): Promise<Awaited<ReturnType<typeof setPRFileViewed>>> {
const repo = await this.deps.resolveRepo(repoSelector)
return setPRFileViewed({
repoPath: repo.path,
connectionId: getRepoRoutingConnectionId(repo),
localGitOptions: this.deps.getLocalGitArgs(repo)[0],
...args
})
}
async updateRepoPRTitle(
repoSelector: string,
prNumber: number,
title: string,
prRepo?: GitHubOwnerRepo | null
): Promise<Awaited<ReturnType<typeof updatePRTitle>>> {
const repo = await this.deps.resolveRepo(repoSelector)
return updatePRTitle(
repo.path,
prNumber,
title,
getRepoRoutingConnectionId(repo),
prRepo ?? null,
...this.deps.getLocalGitArgs(repo)
)
}
async updateRepoPRDetails(
repoSelector: string,
prNumber: number,
updates: { title?: string; body?: string },
prRepo?: GitHubOwnerRepo | null
): Promise<Awaited<ReturnType<typeof updatePRDetails>>> {
const repo = await this.deps.resolveRepo(repoSelector)
return updatePRDetails(
repo.path,
prNumber,
updates,
getRepoRoutingConnectionId(repo),
prRepo ?? null,
...this.deps.getLocalGitArgs(repo)
)
}
async mergeRepoPR(
repoSelector: string,
prNumber: number,
method?: 'merge' | 'squash' | 'rebase',
prRepo?: GitHubOwnerRepo | null
): Promise<Awaited<ReturnType<typeof mergePR>>> {
const repo = await this.deps.resolveRepo(repoSelector)
return mergePR(
repo.path,
prNumber,
method,
getRepoRoutingConnectionId(repo),
prRepo ?? null,
...this.deps.getLocalGitArgs(repo)
)
}
async setRepoPRAutoMerge(
repoSelector: string,
prNumber: number,
enabled: boolean,
method?: 'merge' | 'squash' | 'rebase',
prRepo?: GitHubOwnerRepo | null
): Promise<Awaited<ReturnType<typeof setPRAutoMerge>>> {
const repo = await this.deps.resolveRepo(repoSelector)
return setPRAutoMerge(
repo.path,
prNumber,
enabled,
method,
getRepoRoutingConnectionId(repo),
prRepo ?? null,
...this.deps.getLocalGitArgs(repo)
)
}
async markRepoPRReadyForReview(
repoSelector: string,
prNumber: number,
prRepo?: GitHubOwnerRepo | null
): Promise<Awaited<ReturnType<typeof markPRReadyForReview>>> {
const repo = await this.deps.resolveRepo(repoSelector)
return markPRReadyForReview(
repo.path,
prNumber,
getRepoRoutingConnectionId(repo),
prRepo ?? null,
...this.deps.getLocalGitArgs(repo)
)
}
async updateRepoPRState(
repoSelector: string,
prNumber: number,
updates: GitHubPullRequestStateUpdate,
prRepo?: GitHubOwnerRepo | null
): Promise<Awaited<ReturnType<typeof updatePRState>>> {
const repo = await this.deps.resolveRepo(repoSelector)
return updatePRState(
repo.path,
prNumber,
updates,
getRepoRoutingConnectionId(repo),
prRepo ?? null,
...this.deps.getLocalGitArgs(repo)
)
}
async requestRepoPRReviewers(
repoSelector: string,
prNumber: number,
reviewers: string[],
prRepo?: GitHubOwnerRepo | null
): Promise<Awaited<ReturnType<typeof requestPRReviewers>>> {
const repo = await this.deps.resolveRepo(repoSelector)
return requestPRReviewers(
repo.path,
prNumber,
reviewers,
getRepoRoutingConnectionId(repo),
prRepo ?? null,
...this.deps.getLocalGitArgs(repo)
)
}
async removeRepoPRReviewers(
repoSelector: string,
prNumber: number,
reviewers: string[],
prRepo?: GitHubOwnerRepo | null
): Promise<Awaited<ReturnType<typeof removePRReviewers>>> {
const repo = await this.deps.resolveRepo(repoSelector)
return removePRReviewers(
repo.path,
prNumber,
reviewers,
getRepoRoutingConnectionId(repo),
prRepo ?? null,
...this.deps.getLocalGitArgs(repo)
)
}
}