Files
orca/src/main/runtime/runtime-gitlab-query-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

204 lines
5.9 KiB
TypeScript

import type { GitLabProjectRef, GitLabWorkItem, MRListState } from '../../shared/gitlab-types'
import type { Repo } from '../../shared/repo-types'
import {
diagnoseAuth,
getRateLimit,
getWorkItemByProjectRef,
listAssignableUsers,
listIssues,
listLabels,
listMergeRequests,
listTodos,
listWorkItems
} from '../gitlab/client'
import {
normalizeGitLabIssueListArgs,
normalizeGitLabMRListState,
normalizeGitLabPositiveInteger,
type GitLabIssueListState
} from '../gitlab/gitlab-preload-args'
import { getWorkItemDetails } from '../gitlab/work-item-details'
import { getRepoRoutingConnectionId } from '../../shared/execution-host'
type LocalGitArgs = [] | [{ wslDistro?: string }]
export type RuntimeGitLabQueryCommandsDeps = {
resolveRepo: (selector: string) => Promise<Repo>
getLocalGitArgs: (repo: Repo) => LocalGitArgs
recordProjectRecent: (projectRef: GitLabProjectRef) => void
}
export class RuntimeGitLabQueryCommands {
constructor(private readonly deps: RuntimeGitLabQueryCommandsDeps) {}
async listGitLabRepoWorkItems(
repoSelector: string,
state?: MRListState,
page?: number,
perPage?: number,
query?: string
): Promise<Awaited<ReturnType<typeof listWorkItems>>> {
const repo = await this.deps.resolveRepo(repoSelector)
return listWorkItems(
repo.path,
state ?? 'opened',
page ?? 1,
perPage ?? 20,
repo.issueSourcePreference,
query,
getRepoRoutingConnectionId(repo),
...this.deps.getLocalGitArgs(repo)
)
}
async listGitLabRepoMRs(
repoSelector: string,
state?: MRListState,
page?: number,
perPage?: number,
query?: string
): Promise<Awaited<ReturnType<typeof listMergeRequests>>> {
const repo = await this.deps.resolveRepo(repoSelector)
return listMergeRequests(
repo.path,
normalizeGitLabMRListState(state),
normalizeGitLabPositiveInteger(page, 1, 10_000),
normalizeGitLabPositiveInteger(perPage, 20, 100),
repo.issueSourcePreference,
query,
getRepoRoutingConnectionId(repo),
...this.deps.getLocalGitArgs(repo)
)
}
async listGitLabRepoIssues(
repoSelector: string,
state?: GitLabIssueListState,
assignee?: string,
limit?: number,
page?: number
): Promise<{
items: GitLabWorkItem[]
totalPages: number
error?: Awaited<ReturnType<typeof listIssues>>['error']
}> {
const repo = await this.deps.resolveRepo(repoSelector)
const normalized = normalizeGitLabIssueListArgs({ state, assignee, limit, page })
// Why: page is after localGitOptions; never spread optional args before it (#13538).
const result = await listIssues(
repo.path,
normalized.limit,
repo.issueSourcePreference,
normalized.state,
normalized.assignee,
getRepoRoutingConnectionId(repo),
this.deps.getLocalGitArgs(repo)[0] ?? {},
normalized.page
)
// Why: web runtime mirrors the desktop preload contract used by TaskPage.
const items: GitLabWorkItem[] = result.items.map((issue) => ({
id: `gitlab-issue-${repo.id}-${issue.number}`,
type: 'issue' as const,
number: issue.number,
title: issue.title,
state: issue.state,
url: issue.url,
labels: issue.labels,
updatedAt: issue.updatedAt ?? '',
author: issue.author ?? null,
repoId: repo.id
}))
return {
items,
totalPages: result.totalPages,
...(result.error ? { error: result.error } : {})
}
}
async listGitLabRepoTodos(repoSelector: string): Promise<Awaited<ReturnType<typeof listTodos>>> {
const repo = await this.deps.resolveRepo(repoSelector)
return listTodos(
repo.path,
getRepoRoutingConnectionId(repo),
...this.deps.getLocalGitArgs(repo)
)
}
diagnoseGitLabAuth(): Promise<Awaited<ReturnType<typeof diagnoseAuth>>> {
return diagnoseAuth()
}
getGitLabRateLimit(options?: {
force?: boolean
host?: string | null
}): Promise<Awaited<ReturnType<typeof getRateLimit>>> {
return getRateLimit(options)
}
async listGitLabRepoLabels(
repoSelector: string
): Promise<Awaited<ReturnType<typeof listLabels>>> {
const repo = await this.deps.resolveRepo(repoSelector)
return listLabels(
repo.path,
repo.issueSourcePreference,
getRepoRoutingConnectionId(repo),
...this.deps.getLocalGitArgs(repo)
)
}
async listGitLabRepoAssignableUsers(
repoSelector: string
): Promise<Awaited<ReturnType<typeof listAssignableUsers>>> {
const repo = await this.deps.resolveRepo(repoSelector)
return listAssignableUsers(
repo.path,
repo.issueSourcePreference,
getRepoRoutingConnectionId(repo),
...this.deps.getLocalGitArgs(repo)
)
}
async getGitLabRepoWorkItemDetails(
repoSelector: string,
iid: number,
type: 'issue' | 'mr',
projectRef?: GitLabProjectRef | null,
includeReviewDetails?: boolean
): Promise<Awaited<ReturnType<typeof getWorkItemDetails>>> {
const repo = await this.deps.resolveRepo(repoSelector)
return getWorkItemDetails(
repo.path,
iid,
type,
repo.issueSourcePreference,
getRepoRoutingConnectionId(repo),
projectRef,
this.deps.getLocalGitArgs(repo)[0] ?? {},
{ includeReviewDetails }
)
}
async getGitLabRepoWorkItemByPath(
repoSelector: string,
projectRef: GitLabProjectRef,
iid: number,
type: 'issue' | 'mr'
): Promise<Awaited<ReturnType<typeof getWorkItemByProjectRef>>> {
const repo = await this.deps.resolveRepo(repoSelector)
const result = await getWorkItemByProjectRef(
repo.path,
projectRef,
iid,
type,
getRepoRoutingConnectionId(repo),
...this.deps.getLocalGitArgs(repo)
)
// Why: successful remote pasted-URL lookups update the same recents as desktop IPC.
if (result) {
this.deps.recordProjectRecent(projectRef)
}
return result
}
}