mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 09:37:33 +00:00
* 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.
204 lines
5.9 KiB
TypeScript
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
|
|
}
|
|
}
|