mirror of
https://github.com/stablyai/orca.git
synced 2026-11-14 01:47:24 +00:00
refactor(github): organize concrete feature owners (#41601)
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import { z } from 'zod'
|
||||
import { SlugRepo } from '../../rpc-contract/github-repo-target-params'
|
||||
import { ExpectedRuntimeSourceSchema } from '../../runtime-rpc-envelope'
|
||||
|
||||
const methods = new Set([
|
||||
'github.workItemDetails',
|
||||
'github.updateIssue',
|
||||
'github.listLabels',
|
||||
'github.listAssignableUsers'
|
||||
])
|
||||
const exactShape = z.object({
|
||||
ownerRepo: SlugRepo,
|
||||
expectedRuntimeSource: ExpectedRuntimeSourceSchema
|
||||
})
|
||||
|
||||
// Only the complete opt-in pair promises enforcement; old independently optional fields stay optional.
|
||||
export function classifyExactGitHubOperation(method: string, params: unknown, envelope: unknown) {
|
||||
if (
|
||||
!methods.has(method) ||
|
||||
!isRecord(params) ||
|
||||
!isRecord(envelope) ||
|
||||
!Object.hasOwn(params, 'ownerRepo') ||
|
||||
!Object.hasOwn(envelope, 'expectedRuntimeSource')
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const parsed = exactShape.safeParse({
|
||||
ownerRepo: params.ownerRepo,
|
||||
expectedRuntimeSource: envelope.expectedRuntimeSource
|
||||
})
|
||||
if (!parsed.success) {
|
||||
throw new Error('Unable to identify the GitHub operation. Open the item again and try again.')
|
||||
}
|
||||
return parsed.data
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { ClassifiedError } from '../../classified-error'
|
||||
import { GITHUB_SEARCH_RESULT_WINDOW_ERROR_PATTERN } from './work-items-query-bounds'
|
||||
|
||||
export function getWorkItemPageErrorTypes(errors: unknown): ClassifiedError['type'][] {
|
||||
if (errors === undefined || errors === null) {
|
||||
return []
|
||||
}
|
||||
if (typeof errors !== 'object') {
|
||||
return ['unknown']
|
||||
}
|
||||
const sources = [
|
||||
['issues', 'issues' in errors ? errors.issues : undefined],
|
||||
['prs', 'prs' in errors ? errors.prs : undefined]
|
||||
] as const
|
||||
return sources.flatMap(([source, error]): ClassifiedError['type'][] => {
|
||||
if (error === undefined || error === null) {
|
||||
return []
|
||||
}
|
||||
if (typeof error !== 'object' || !('type' in error)) {
|
||||
return ['unknown']
|
||||
}
|
||||
switch (error.type) {
|
||||
case 'validation_error':
|
||||
// Only issue search's 1000-result window proves a page limit.
|
||||
return [
|
||||
source === 'issues' &&
|
||||
'message' in error &&
|
||||
typeof error.message === 'string' &&
|
||||
GITHUB_SEARCH_RESULT_WINDOW_ERROR_PATTERN.test(error.message)
|
||||
? 'validation_error'
|
||||
: 'unknown'
|
||||
]
|
||||
case 'permission_denied':
|
||||
case 'not_found':
|
||||
case 'issues_disabled':
|
||||
case 'rate_limited':
|
||||
case 'network_error':
|
||||
return [error.type]
|
||||
default:
|
||||
return ['unknown']
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export const GITHUB_WORK_ITEMS_SSH_REMOTE_REQUIRED_MESSAGE =
|
||||
'GitHub work items require a GitHub remote for SSH repositories'
|
||||
|
||||
export function isGitHubWorkItemsSshRemoteRequiredError(error: unknown): boolean {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: typeof error === 'object' &&
|
||||
error !== null &&
|
||||
'message' in error &&
|
||||
typeof error.message === 'string'
|
||||
? error.message
|
||||
: typeof error === 'string'
|
||||
? error
|
||||
: ''
|
||||
|
||||
return message.includes(GITHUB_WORK_ITEMS_SSH_REMOTE_REQUIRED_MESSAGE)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { GitHubRepositoryIdentity as GitHubOwnerRepo } from '../repositories/repository-identity'
|
||||
import type { GitHubWorkItem, GitHubWorkItemDetails } from './work-item-types'
|
||||
import { githubRepoIdentityKey } from '../repositories/repository-identity-key'
|
||||
|
||||
export type GitHubWorkItemTarget = {
|
||||
repository: GitHubOwnerRepo
|
||||
type: 'issue' | 'pr'
|
||||
number: number
|
||||
}
|
||||
|
||||
export function matchesGitHubWorkItemTarget(
|
||||
item: Pick<GitHubWorkItem, 'url' | 'prRepo' | 'type' | 'number'> | null | undefined,
|
||||
target: GitHubWorkItemTarget
|
||||
): boolean {
|
||||
if (!item || item.type !== target.type || item.number !== target.number) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
const url = new URL(item.url)
|
||||
const parts = url.pathname.split('/').filter(Boolean)
|
||||
if (
|
||||
!['http:', 'https:'].includes(url.protocol) ||
|
||||
parts.length !== 4 ||
|
||||
parts[2] !== (target.type === 'pr' ? 'pull' : 'issues') ||
|
||||
parts[3] !== String(target.number)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
const repository = { host: url.host, owner: parts[0], repo: parts[1] }
|
||||
const key = githubRepoIdentityKey(repository)
|
||||
return (
|
||||
key === githubRepoIdentityKey(target.repository) &&
|
||||
(!item.prRepo || githubRepoIdentityKey(item.prRepo) === key)
|
||||
)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export class GitHubWorkItemTargetError extends Error {
|
||||
constructor(target: GitHubWorkItemTarget) {
|
||||
const { owner, repo } = target.repository
|
||||
super(
|
||||
`Couldn't load ${owner}/${repo}#${target.number} from this server. Try again or open the ${target.type === 'pr' ? 'pull request' : 'issue'} on GitHub.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function requireGitHubWorkItemTargetDetails(
|
||||
details: GitHubWorkItemDetails | null,
|
||||
target: GitHubWorkItemTarget
|
||||
): GitHubWorkItemDetails {
|
||||
if (!details || !matchesGitHubWorkItemTarget(details.item, target)) {
|
||||
throw new GitHubWorkItemTargetError(target)
|
||||
}
|
||||
return details
|
||||
}
|
||||
|
||||
export const GITHUB_EXACT_OPERATION_UNSUPPORTED =
|
||||
'Update Orca on the executing server to edit this item or load its repository options.'
|
||||
|
||||
export function githubWorkItemTargetKey(target: GitHubWorkItemTarget): string {
|
||||
return [githubRepoIdentityKey(target.repository), target.type, target.number].join('\0')
|
||||
}
|
||||
|
||||
export function parseGitHubItemRepository(url: string): GitHubOwnerRepo | null {
|
||||
try {
|
||||
const parsed = new URL(url)
|
||||
const parts = parsed.pathname.split('/').filter(Boolean)
|
||||
return ['https:', 'http:'].includes(parsed.protocol) && parts.length >= 2
|
||||
? { host: parsed.host, owner: parts[0], repo: parts[1] }
|
||||
: null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveGitHubWorkItemTarget(
|
||||
item: Pick<GitHubWorkItem, 'url' | 'prRepo' | 'type' | 'number'>
|
||||
): GitHubWorkItemTarget | null {
|
||||
const repository = item.prRepo ?? parseGitHubItemRepository(item.url)
|
||||
return repository ? { repository, type: item.type, number: item.number } : null
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { ClassifiedError } from '../../classified-error'
|
||||
import type { ExecutionHostId } from '../../execution-host'
|
||||
import type { PRCheckDetail } from '../checks/check-types'
|
||||
import type { GitHubIssueTimelineItem, PRComment } from '../comments/comment-types'
|
||||
import type {
|
||||
GitHubAssignableUser,
|
||||
GitHubPRFile,
|
||||
GitHubPRMergeMethodSettings,
|
||||
GitHubPRReviewSummary,
|
||||
PRMergeableState,
|
||||
PRReviewDecision,
|
||||
ProviderCheckSummary
|
||||
} from '../pull-requests/pull-request-types'
|
||||
import type {
|
||||
GitHubRepositoryIdentity as GitHubOwnerRepo,
|
||||
GitHubRepositoryIdentity
|
||||
} from '../repositories/repository-identity'
|
||||
|
||||
export type GitHubWorkItem = {
|
||||
id: string
|
||||
type: 'issue' | 'pr'
|
||||
number: number
|
||||
title: string
|
||||
state: 'open' | 'closed' | 'merged' | 'draft'
|
||||
url: string
|
||||
labels: string[]
|
||||
updatedAt: string
|
||||
author: string | null
|
||||
// Use API avatars for GHE logins absent on github.com (#8784). gh-pr-view omits them; UI falls back through login URL to initials.
|
||||
authorAvatarUrl?: string
|
||||
branchName?: string
|
||||
baseRefName?: string
|
||||
// Head SHA enables cached check-run requests instead of per-row gh pr checks.
|
||||
headSha?: string
|
||||
prRepo?: GitHubRepositoryIdentity
|
||||
additions?: number
|
||||
deletions?: number
|
||||
changedFiles?: number
|
||||
/** Issue-only; older hosts omit it. */
|
||||
commentCount?: number
|
||||
reviewDecision?: PRReviewDecision | null
|
||||
reviewRequests?: GitHubAssignableUser[]
|
||||
latestReviews?: GitHubPRReviewSummary[]
|
||||
assignees?: GitHubAssignableUser[]
|
||||
checksSummary?: ProviderCheckSummary
|
||||
mergeable?: PRMergeableState
|
||||
autoMergeEnabled?: boolean
|
||||
autoMergeAllowed?: boolean | null
|
||||
mergeQueueRequired?: boolean | null
|
||||
mergeMethodSettings?: GitHubPRMergeMethodSettings
|
||||
mergeStateStatus?: string | null
|
||||
maintainerCanModify?: boolean
|
||||
// Fork heads use refs/pull/<N>/head for creation and a separate push target in resolvePrBase.
|
||||
isCrossRepository?: boolean
|
||||
/** Renderer fetches/optimistic stubs stamp origin repo for cross-repo pills and open-in-browser routing. */
|
||||
repoId: string
|
||||
/** Repo ids can repeat across hosts; stamp the producing host so actions cannot target a same-id twin. */
|
||||
repoHostId?: ExecutionHostId
|
||||
}
|
||||
|
||||
export type GitHubWorkItemDetails = {
|
||||
// Main lacks Orca Repo.id; renderer stamps it when storing details.
|
||||
item: Omit<GitHubWorkItem, 'repoId'>
|
||||
body: string
|
||||
bodyImageUrls?: Record<string, string>
|
||||
comments: PRComment[]
|
||||
/** Issue-only provider activity such as assignment, references, project moves, and state changes. */
|
||||
timelineItems?: GitHubIssueTimelineItem[]
|
||||
/** Only set for PRs. Head/base SHAs used by the Files tab to fetch per-file content. */
|
||||
headSha?: string
|
||||
baseSha?: string
|
||||
/** GraphQL node ID required by GitHub's file-viewed mutations. Only set for PRs. */
|
||||
pullRequestId?: string
|
||||
checks?: PRCheckDetail[]
|
||||
files?: GitHubPRFile[]
|
||||
/** Only set for PRs. True when the file fetch failed (rate limit, auth,
|
||||
* unresolved remote) rather than the PR genuinely having no changed files. */
|
||||
filesUnavailable?: boolean
|
||||
participants?: GitHubAssignableUser[]
|
||||
/** Logins of current assignees. Only set for issues. */
|
||||
assignees?: string[]
|
||||
}
|
||||
|
||||
/** Piggyback issue/PR sources on Tasks refresh to avoid another IPC round trip. Per-source failures preserve successful items;
|
||||
* render items alongside error banners (e.g. private upstream 403), never discard the other side’s success. */
|
||||
export type ListWorkItemsResult<T> = {
|
||||
items: T[]
|
||||
sources: {
|
||||
/** Affirmative other-forge identity; absent on unresolved or legacy results. */
|
||||
ineligible?: true
|
||||
issues: GitHubOwnerRepo | null
|
||||
prs: GitHubOwnerRepo | null
|
||||
/** Raw origin independent of preferences; compare candidates without inferring from effective PR source.
|
||||
* Absent from older hosts' results: unavailable evidence, distinct from a present unresolved `null`. */
|
||||
originCandidate?: GitHubOwnerRepo | null
|
||||
/** Raw upstream independent of preferences; selector visibility/tooltips need it even when effective issues source is origin. */
|
||||
upstreamCandidate?: GitHubOwnerRepo | null
|
||||
}
|
||||
errors?: {
|
||||
issues?: ClassifiedError
|
||||
prs?: ClassifiedError
|
||||
}
|
||||
/** Present only when requested upstream was missing and origin was used; renderer shows a once-per-session toast.
|
||||
* Optional literal true preserves legacy consumers and forbids erroneous explicit false writes. */
|
||||
issueSourceFellBack?: true
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { isClipboardTextByteLengthOverLimit } from '../../clipboard-text'
|
||||
|
||||
export const GITHUB_WORK_ITEMS_QUERY_MAX_BYTES = 8 * 1024
|
||||
|
||||
/**
|
||||
* The Search API's free-text 422 wording is the only signal separating a
|
||||
* permanently unreachable page (past the first 1000 matches) from a transient failure.
|
||||
*/
|
||||
export const GITHUB_SEARCH_RESULT_WINDOW_ERROR_PATTERN = /first 1000 search results/i
|
||||
|
||||
export function isGitHubWorkItemsQueryTooLarge(
|
||||
query: string,
|
||||
maxBytes = GITHUB_WORK_ITEMS_QUERY_MAX_BYTES
|
||||
): boolean {
|
||||
return isClipboardTextByteLengthOverLimit(query, maxBytes)
|
||||
}
|
||||
Reference in New Issue
Block a user