From 487e80f46959fe467d2c78ead96b6f54ef7222b2 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 17:56:34 -0700 Subject: [PATCH 1/2] Structure GitHub CLI failures Co-authored-by: codex --- apps/server/src/git/GitManager.test.ts | 51 ++-- .../src/sourceControl/GitHubCli.test.ts | 2 + apps/server/src/sourceControl/GitHubCli.ts | 270 +++++++++++------- .../GitHubSourceControlProvider.test.ts | 3 +- .../GitHubSourceControlProvider.ts | 13 +- 5 files changed, 197 insertions(+), 142 deletions(-) diff --git a/apps/server/src/git/GitManager.test.ts b/apps/server/src/git/GitManager.test.ts index c06915c51b9..28e657e8789 100644 --- a/apps/server/src/git/GitManager.test.ts +++ b/apps/server/src/git/GitManager.test.ts @@ -171,20 +171,8 @@ function runGitSyncForFakeGh(cwd: string, args: readonly string[]): void { if (result.status === 0) { return; } - throw new GitHubCli.GitHubCliError({ - operation: "execute", - command: "gh", - cwd, - detail: `Failed to simulate gh checkout with git ${args.join(" ")}: ${result.stderr?.trim() || "unknown error"}`, - }); -} - -function isGitHubCliError(error: unknown): error is GitHubCli.GitHubCliError { - return ( - typeof error === "object" && - error !== null && - "_tag" in error && - (error as { _tag?: unknown })._tag === "GitHubCliError" + throw new Error( + `Failed to simulate gh checkout with git ${args.join(" ")}: ${result.stderr?.trim() || "unknown error"}`, ); } @@ -478,16 +466,13 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { return fakeGhOutput(""); }, catch: (error) => - isGitHubCliError(error) + GitHubCli.isGitHubCliError(error) ? error - : new GitHubCli.GitHubCliError({ + : new GitHubCli.GitHubCliCommandError({ operation: "execute", command: "gh", cwd: input.cwd, - detail: - error instanceof Error - ? `Failed to simulate gh checkout: ${error.message}` - : "Failed to simulate gh checkout.", + cause: error, }), }); } @@ -498,11 +483,11 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { const cloneUrls = scenario.repositoryCloneUrls?.[repository]; if (!cloneUrls) { return Effect.fail( - new GitHubCli.GitHubCliError({ + new GitHubCli.GitHubCliCommandError({ operation: "execute", command: "gh", cwd: input.cwd, - detail: `Unexpected repository lookup: ${repository}`, + cause: new Error(`Unexpected repository lookup: ${repository}`), }), ); } @@ -520,11 +505,11 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { } return Effect.fail( - new GitHubCli.GitHubCliError({ + new GitHubCli.GitHubCliCommandError({ operation: "execute", command: "gh", cwd: input.cwd, - detail: `Unexpected gh command: ${args.join(" ")}`, + cause: new Error(`Unexpected gh command: ${args.join(" ")}`), }), ); }; @@ -601,11 +586,11 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { }).pipe(Effect.map((result) => JSON.parse(result.stdout))), createRepository: (input) => Effect.fail( - new GitHubCli.GitHubCliError({ - operation: "createRepository", + new GitHubCli.GitHubCliCommandError({ + operation: "execute", command: "gh", cwd: input.cwd, - detail: `Unexpected repository create: ${input.repository}`, + cause: new Error(`Unexpected repository create: ${input.repository}`), }), ), checkoutPullRequest: (input) => @@ -1341,11 +1326,11 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { const { manager } = yield* makeManager({ ghScenario: { - failWith: new GitHubCli.GitHubCliError({ + failWith: new GitHubCli.GitHubCliUnavailableError({ operation: "execute", command: "gh", cwd: repoDir, - detail: "GitHub CLI (`gh`) is required but not available on PATH.", + cause: new Error("gh is not available on PATH"), }), }, }); @@ -2483,11 +2468,11 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { const { manager } = yield* makeManager({ ghScenario: { - failWith: new GitHubCli.GitHubCliError({ + failWith: new GitHubCli.GitHubCliUnavailableError({ operation: "execute", command: "gh", cwd: repoDir, - detail: "GitHub CLI (`gh`) is required but not available on PATH.", + cause: new Error("gh is not available on PATH"), }), }, }); @@ -2514,11 +2499,11 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { const { manager } = yield* makeManager({ ghScenario: { - failWith: new GitHubCli.GitHubCliError({ + failWith: new GitHubCli.GitHubCliAuthenticationError({ operation: "execute", command: "gh", cwd: repoDir, - detail: "GitHub CLI is not authenticated. Run `gh auth login` and retry.", + cause: new Error("gh is not authenticated"), }), }, }); diff --git a/apps/server/src/sourceControl/GitHubCli.test.ts b/apps/server/src/sourceControl/GitHubCli.test.ts index 7c8c9b037be..917f312deec 100644 --- a/apps/server/src/sourceControl/GitHubCli.test.ts +++ b/apps/server/src/sourceControl/GitHubCli.test.ts @@ -274,6 +274,7 @@ describe("GitHubCli.layer", () => { command: "gh pr view", cwd: "/repo", exitCode: 1, + failureKind: "not-found", detail: "GraphQL: Could not resolve to a PullRequest with the number of 4888. (repository.pullRequest)", }); @@ -288,6 +289,7 @@ describe("GitHubCli.layer", () => { .pipe(Effect.flip); assert.equal(error.message.includes("Pull request not found"), true); + assert.strictEqual(error._tag, "GitHubPullRequestNotFoundError"); assert.strictEqual(error.command, "gh"); assert.strictEqual(error.cwd, "/repo"); assert.strictEqual(error.cause, cause); diff --git a/apps/server/src/sourceControl/GitHubCli.ts b/apps/server/src/sourceControl/GitHubCli.ts index 4cdf38ec2b8..4cce8767120 100644 --- a/apps/server/src/sourceControl/GitHubCli.ts +++ b/apps/server/src/sourceControl/GitHubCli.ts @@ -18,67 +18,171 @@ import { const DEFAULT_TIMEOUT_MS = 30_000; -export class GitHubCliError extends Schema.TaggedErrorClass()("GitHubCliError", { - operation: Schema.String, - command: Schema.String, +const gitHubCliFailureFields = { + operation: Schema.Literal("execute"), + command: Schema.Literal("gh"), cwd: Schema.String, - detail: Schema.String, - cause: Schema.optional(Schema.Defect()), -}) { + cause: Schema.Defect(), +} as const; + +export class GitHubCliUnavailableError extends Schema.TaggedErrorClass()( + "GitHubCliUnavailableError", + gitHubCliFailureFields, +) { + get detail(): string { + return "GitHub CLI (`gh`) is required but not available on PATH."; + } + override get message(): string { return `GitHub CLI failed in ${this.operation}: ${this.detail}`; } +} - static fromVcsError( - context: { - readonly operation: "execute"; - readonly command: "gh"; - readonly cwd: string; - }, - error: VcsError | unknown, - ): GitHubCliError { - const lower = errorText(error).toLowerCase(); - - if (lower.includes("command not found: gh") || lower.includes("enoent")) { - return new GitHubCliError({ - ...context, - detail: "GitHub CLI (`gh`) is required but not available on PATH.", - cause: error, - }); - } +export class GitHubCliAuthenticationError extends Schema.TaggedErrorClass()( + "GitHubCliAuthenticationError", + gitHubCliFailureFields, +) { + get detail(): string { + return "GitHub CLI is not authenticated. Run `gh auth login` and retry."; + } - if ( - lower.includes("authentication failed") || - lower.includes("not logged in") || - lower.includes("gh auth login") || - lower.includes("no oauth token") - ) { - return new GitHubCliError({ - ...context, - detail: "GitHub CLI is not authenticated. Run `gh auth login` and retry.", - cause: error, - }); - } + override get message(): string { + return `GitHub CLI failed in ${this.operation}: ${this.detail}`; + } +} - if ( - lower.includes("could not resolve to a pullrequest") || - lower.includes("repository.pullrequest") || - lower.includes("no pull requests found for branch") || - lower.includes("pull request not found") - ) { - return new GitHubCliError({ - ...context, - detail: "Pull request not found. Check the PR number or URL and try again.", - cause: error, - }); - } +export class GitHubPullRequestNotFoundError extends Schema.TaggedErrorClass()( + "GitHubPullRequestNotFoundError", + gitHubCliFailureFields, +) { + get detail(): string { + return "Pull request not found. Check the PR number or URL and try again."; + } + + override get message(): string { + return `GitHub CLI failed in ${this.operation}: ${this.detail}`; + } +} + +export class GitHubCliCommandError extends Schema.TaggedErrorClass()( + "GitHubCliCommandError", + gitHubCliFailureFields, +) { + get detail(): string { + return "GitHub CLI command failed."; + } + + override get message(): string { + return `GitHub CLI failed in ${this.operation}: ${this.detail}`; + } +} + +const gitHubCliDecodeFields = { + command: Schema.Literal("gh"), + cwd: Schema.String, + cause: Schema.Defect(), +} as const; + +export class GitHubPullRequestListDecodeError extends Schema.TaggedErrorClass()( + "GitHubPullRequestListDecodeError", + { + ...gitHubCliDecodeFields, + operation: Schema.Literal("listOpenPullRequests"), + }, +) { + get detail(): string { + return "GitHub CLI returned invalid PR list JSON."; + } + + override get message(): string { + return `GitHub CLI failed in ${this.operation}: ${this.detail}`; + } +} + +export class GitHubChangeRequestListDecodeError extends Schema.TaggedErrorClass()( + "GitHubChangeRequestListDecodeError", + { + ...gitHubCliDecodeFields, + operation: Schema.Literal("listChangeRequests"), + }, +) { + get detail(): string { + return "GitHub CLI returned invalid change request JSON."; + } + + override get message(): string { + return `GitHub CLI failed in ${this.operation}: ${this.detail}`; + } +} + +export class GitHubPullRequestDecodeError extends Schema.TaggedErrorClass()( + "GitHubPullRequestDecodeError", + { + ...gitHubCliDecodeFields, + operation: Schema.Literal("getPullRequest"), + }, +) { + get detail(): string { + return "GitHub CLI returned invalid pull request JSON."; + } + + override get message(): string { + return `GitHub CLI failed in ${this.operation}: ${this.detail}`; + } +} + +export class GitHubRepositoryDecodeError extends Schema.TaggedErrorClass()( + "GitHubRepositoryDecodeError", + { + ...gitHubCliDecodeFields, + operation: Schema.Literal("getRepositoryCloneUrls"), + }, +) { + get detail(): string { + return "GitHub CLI returned invalid repository JSON."; + } + + override get message(): string { + return `GitHub CLI failed in ${this.operation}: ${this.detail}`; + } +} + +export const GitHubCliError = Schema.Union([ + GitHubCliUnavailableError, + GitHubCliAuthenticationError, + GitHubPullRequestNotFoundError, + GitHubCliCommandError, + GitHubPullRequestListDecodeError, + GitHubChangeRequestListDecodeError, + GitHubPullRequestDecodeError, + GitHubRepositoryDecodeError, +]); +export type GitHubCliError = typeof GitHubCliError.Type; + +export const isGitHubCliError = Schema.is(GitHubCliError); - return new GitHubCliError({ - ...context, - detail: "GitHub CLI command failed.", - cause: error, - }); +export function fromVcsError( + context: { + readonly operation: "execute"; + readonly command: "gh"; + readonly cwd: string; + }, + error: VcsError, +): GitHubCliError { + if (error._tag === "VcsProcessSpawnError") { + return new GitHubCliUnavailableError({ ...context, cause: error }); } + + if (error._tag === "VcsProcessExitError") { + if (error.failureKind === "authentication") { + return new GitHubCliAuthenticationError({ ...context, cause: error }); + } + if (error.failureKind === "not-found") { + return new GitHubPullRequestNotFoundError({ ...context, cause: error }); + } + } + + return new GitHubCliCommandError({ ...context, cause: error }); } export interface GitHubPullRequestSummary { @@ -150,22 +254,14 @@ export class GitHubCli extends Context.Service< } >()("t3/sourceControl/GitHubCli") {} -function errorText(error: VcsError | unknown): string { - if (typeof error === "object" && error !== null) { - const tag = "_tag" in error && typeof error._tag === "string" ? error._tag : ""; - const detail = "detail" in error && typeof error.detail === "string" ? error.detail : ""; - const message = "message" in error && typeof error.message === "string" ? error.message : ""; - return [tag, detail, message].filter(Boolean).join("\n"); - } - - return String(error); -} - const RawGitHubRepositoryCloneUrlsSchema = Schema.Struct({ nameWithOwner: TrimmedNonEmptyString, url: TrimmedNonEmptyString, sshUrl: TrimmedNonEmptyString, }); +const decodeRawGitHubRepositoryCloneUrls = Schema.decodeEffect( + Schema.fromJsonString(RawGitHubRepositoryCloneUrlsSchema), +); function normalizeRepositoryCloneUrls( raw: Schema.Schema.Type, @@ -214,27 +310,6 @@ function deriveRepositoryCloneUrlsFromCreateOutput( }; } -function decodeGitHubJson( - raw: string, - schema: S, - operation: "listOpenPullRequests" | "getPullRequest" | "getRepositoryCloneUrls", - invalidDetail: string, - cwd: string, -): Effect.Effect { - return Schema.decodeEffect(Schema.fromJsonString(schema))(raw).pipe( - Effect.mapError( - (error) => - new GitHubCliError({ - operation, - command: "gh", - cwd, - detail: invalidDetail, - cause: error, - }), - ), - ); -} - export const make = Effect.gen(function* () { const process = yield* VcsProcess.VcsProcess; @@ -249,10 +324,7 @@ export const make = Effect.gen(function* () { }) .pipe( Effect.mapError((error) => - GitHubCliError.fromVcsError( - { operation: "execute", command: "gh", cwd: input.cwd }, - error, - ), + fromVcsError({ operation: "execute", command: "gh", cwd: input.cwd }, error), ), ); @@ -282,11 +354,10 @@ export const make = Effect.gen(function* () { Effect.flatMap((decoded) => { if (!Result.isSuccess(decoded)) { return Effect.fail( - new GitHubCliError({ + new GitHubPullRequestListDecodeError({ operation: "listOpenPullRequests", command: "gh", cwd: input.cwd, - detail: "GitHub CLI returned invalid PR list JSON.", cause: decoded.failure, }), ); @@ -316,11 +387,10 @@ export const make = Effect.gen(function* () { Effect.flatMap((decoded) => { if (!Result.isSuccess(decoded)) { return Effect.fail( - new GitHubCliError({ + new GitHubPullRequestDecodeError({ operation: "getPullRequest", command: "gh", cwd: input.cwd, - detail: "GitHub CLI returned invalid pull request JSON.", cause: decoded.failure, }), ); @@ -340,12 +410,16 @@ export const make = Effect.gen(function* () { }).pipe( Effect.map((result) => result.stdout.trim()), Effect.flatMap((raw) => - decodeGitHubJson( - raw, - RawGitHubRepositoryCloneUrlsSchema, - "getRepositoryCloneUrls", - "GitHub CLI returned invalid repository JSON.", - input.cwd, + decodeRawGitHubRepositoryCloneUrls(raw).pipe( + Effect.mapError( + (cause) => + new GitHubRepositoryDecodeError({ + operation: "getRepositoryCloneUrls", + command: "gh", + cwd: input.cwd, + cause, + }), + ), ), ), Effect.map(normalizeRepositoryCloneUrls), diff --git a/apps/server/src/sourceControl/GitHubSourceControlProvider.test.ts b/apps/server/src/sourceControl/GitHubSourceControlProvider.test.ts index c1aa8680b26..790fa49aae8 100644 --- a/apps/server/src/sourceControl/GitHubSourceControlProvider.test.ts +++ b/apps/server/src/sourceControl/GitHubSourceControlProvider.test.ts @@ -70,11 +70,10 @@ it.effect("maps GitHub PR summaries into provider-neutral change requests", () = it.effect("adds safe request context while retaining GitHub CLI causes", () => Effect.gen(function* () { - const cause = new GitHubCli.GitHubCliError({ + const cause = new GitHubCli.GitHubPullRequestNotFoundError({ operation: "execute", command: "gh", cwd: "/repo", - detail: "Pull request not found. Check the PR number or URL and try again.", cause: new Error("raw upstream detail that should remain in the cause"), }); const provider = yield* makeProvider({ diff --git a/apps/server/src/sourceControl/GitHubSourceControlProvider.ts b/apps/server/src/sourceControl/GitHubSourceControlProvider.ts index 60298888e6c..bb6e9b3311a 100644 --- a/apps/server/src/sourceControl/GitHubSourceControlProvider.ts +++ b/apps/server/src/sourceControl/GitHubSourceControlProvider.ts @@ -158,23 +158,18 @@ export const make = Effect.gen(function* () { })), ) : Effect.fail( - new SourceControlProviderError({ - provider: "github", + new GitHubCli.GitHubChangeRequestListDecodeError({ operation: "listChangeRequests", command: "gh", cwd: input.cwd, - reference: SourceControlProvider.transportSafeSourceControlErrorValue( - input.headSelector, - ), - detail: "GitHub CLI returned invalid change request JSON.", cause: decoded.failure, }), ), ), ); }), - Effect.catchTags({ - GitHubCliError: (error) => + Effect.mapError( + (error) => new SourceControlProviderError({ provider: "github", operation: "listChangeRequests", @@ -186,7 +181,7 @@ export const make = Effect.gen(function* () { detail: error.detail, cause: error, }), - }), + ), ); }; From 81dd825753fb5630cfdb4af4de4b9849a1ecadb8 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 18:33:09 -0700 Subject: [PATCH 2/2] [codex] Address GitHub CLI review findings Co-authored-by: codex --- apps/server/src/git/GitManager.test.ts | 7 --- .../src/sourceControl/GitHubCli.test.ts | 24 +++++++- apps/server/src/sourceControl/GitHubCli.ts | 56 +++++++------------ .../GitHubSourceControlProvider.test.ts | 1 - .../GitHubSourceControlProvider.ts | 1 - 5 files changed, 44 insertions(+), 45 deletions(-) diff --git a/apps/server/src/git/GitManager.test.ts b/apps/server/src/git/GitManager.test.ts index 28e657e8789..e1924c03ade 100644 --- a/apps/server/src/git/GitManager.test.ts +++ b/apps/server/src/git/GitManager.test.ts @@ -469,7 +469,6 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { GitHubCli.isGitHubCliError(error) ? error : new GitHubCli.GitHubCliCommandError({ - operation: "execute", command: "gh", cwd: input.cwd, cause: error, @@ -484,7 +483,6 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { if (!cloneUrls) { return Effect.fail( new GitHubCli.GitHubCliCommandError({ - operation: "execute", command: "gh", cwd: input.cwd, cause: new Error(`Unexpected repository lookup: ${repository}`), @@ -506,7 +504,6 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { return Effect.fail( new GitHubCli.GitHubCliCommandError({ - operation: "execute", command: "gh", cwd: input.cwd, cause: new Error(`Unexpected gh command: ${args.join(" ")}`), @@ -587,7 +584,6 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { createRepository: (input) => Effect.fail( new GitHubCli.GitHubCliCommandError({ - operation: "execute", command: "gh", cwd: input.cwd, cause: new Error(`Unexpected repository create: ${input.repository}`), @@ -1327,7 +1323,6 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { const { manager } = yield* makeManager({ ghScenario: { failWith: new GitHubCli.GitHubCliUnavailableError({ - operation: "execute", command: "gh", cwd: repoDir, cause: new Error("gh is not available on PATH"), @@ -2469,7 +2464,6 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { const { manager } = yield* makeManager({ ghScenario: { failWith: new GitHubCli.GitHubCliUnavailableError({ - operation: "execute", command: "gh", cwd: repoDir, cause: new Error("gh is not available on PATH"), @@ -2500,7 +2494,6 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { const { manager } = yield* makeManager({ ghScenario: { failWith: new GitHubCli.GitHubCliAuthenticationError({ - operation: "execute", command: "gh", cwd: repoDir, cause: new Error("gh is not authenticated"), diff --git a/apps/server/src/sourceControl/GitHubCli.test.ts b/apps/server/src/sourceControl/GitHubCli.test.ts index 917f312deec..5df4862b409 100644 --- a/apps/server/src/sourceControl/GitHubCli.test.ts +++ b/apps/server/src/sourceControl/GitHubCli.test.ts @@ -1,8 +1,9 @@ import { assert, it, afterEach, describe, expect, vi } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; +import * as PlatformError from "effect/PlatformError"; import { ChildProcessSpawner } from "effect/unstable/process"; -import { VcsProcessExitError } from "@t3tools/contracts"; +import { VcsProcessExitError, VcsProcessSpawnError } from "@t3tools/contracts"; import * as VcsProcess from "../vcs/VcsProcess.ts"; import * as GitHubCli from "./GitHubCli.ts"; @@ -30,6 +31,27 @@ afterEach(() => { }); describe("GitHubCli.layer", () => { + it("does not classify a missing cwd as an unavailable gh executable", () => { + const context = { command: "gh", cwd: "/repo" } as const; + const missingCwd = new VcsProcessSpawnError({ + operation: "GitHubCli.execute", + command: "gh", + cwd: context.cwd, + cause: PlatformError.systemError({ + _tag: "NotFound", + module: "FileSystem", + method: "access", + pathOrDescriptor: context.cwd, + }), + }); + + const commandFailure = GitHubCli.fromVcsError(context, missingCwd); + + assert.equal(commandFailure._tag, "GitHubCliCommandError"); + assert.strictEqual(commandFailure.cause, missingCwd); + assert.notProperty(commandFailure, "operation"); + }); + it.effect("parses pull request view output", () => Effect.gen(function* () { mockRun.mockReturnValueOnce( diff --git a/apps/server/src/sourceControl/GitHubCli.ts b/apps/server/src/sourceControl/GitHubCli.ts index 4cce8767120..bf3f27378b5 100644 --- a/apps/server/src/sourceControl/GitHubCli.ts +++ b/apps/server/src/sourceControl/GitHubCli.ts @@ -1,6 +1,7 @@ import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; +import * as PlatformError from "effect/PlatformError"; import * as Result from "effect/Result"; import * as Schema from "effect/Schema"; @@ -19,7 +20,6 @@ import { const DEFAULT_TIMEOUT_MS = 30_000; const gitHubCliFailureFields = { - operation: Schema.Literal("execute"), command: Schema.Literal("gh"), cwd: Schema.String, cause: Schema.Defect(), @@ -34,7 +34,7 @@ export class GitHubCliUnavailableError extends Schema.TaggedErrorClass()( "GitHubPullRequestListDecodeError", - { - ...gitHubCliDecodeFields, - operation: Schema.Literal("listOpenPullRequests"), - }, + gitHubCliDecodeFields, ) { get detail(): string { return "GitHub CLI returned invalid PR list JSON."; } override get message(): string { - return `GitHub CLI failed in ${this.operation}: ${this.detail}`; + return `GitHub CLI failed in listOpenPullRequests: ${this.detail}`; } } export class GitHubChangeRequestListDecodeError extends Schema.TaggedErrorClass()( "GitHubChangeRequestListDecodeError", - { - ...gitHubCliDecodeFields, - operation: Schema.Literal("listChangeRequests"), - }, + gitHubCliDecodeFields, ) { get detail(): string { return "GitHub CLI returned invalid change request JSON."; } override get message(): string { - return `GitHub CLI failed in ${this.operation}: ${this.detail}`; + return `GitHub CLI failed in listChangeRequests: ${this.detail}`; } } export class GitHubPullRequestDecodeError extends Schema.TaggedErrorClass()( "GitHubPullRequestDecodeError", - { - ...gitHubCliDecodeFields, - operation: Schema.Literal("getPullRequest"), - }, + gitHubCliDecodeFields, ) { get detail(): string { return "GitHub CLI returned invalid pull request JSON."; } override get message(): string { - return `GitHub CLI failed in ${this.operation}: ${this.detail}`; + return `GitHub CLI failed in getPullRequest: ${this.detail}`; } } export class GitHubRepositoryDecodeError extends Schema.TaggedErrorClass()( "GitHubRepositoryDecodeError", - { - ...gitHubCliDecodeFields, - operation: Schema.Literal("getRepositoryCloneUrls"), - }, + gitHubCliDecodeFields, ) { get detail(): string { return "GitHub CLI returned invalid repository JSON."; } override get message(): string { - return `GitHub CLI failed in ${this.operation}: ${this.detail}`; + return `GitHub CLI failed in getRepositoryCloneUrls: ${this.detail}`; } } @@ -163,13 +151,18 @@ export const isGitHubCliError = Schema.is(GitHubCliError); export function fromVcsError( context: { - readonly operation: "execute"; readonly command: "gh"; readonly cwd: string; }, error: VcsError, ): GitHubCliError { - if (error._tag === "VcsProcessSpawnError") { + if ( + error._tag === "VcsProcessSpawnError" && + error.cause instanceof PlatformError.PlatformError && + error.cause.reason._tag === "NotFound" && + error.cause.reason.module === "ChildProcess" && + error.cause.reason.method === "spawn" + ) { return new GitHubCliUnavailableError({ ...context, cause: error }); } @@ -322,11 +315,7 @@ export const make = Effect.gen(function* () { cwd: input.cwd, timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS, }) - .pipe( - Effect.mapError((error) => - fromVcsError({ operation: "execute", command: "gh", cwd: input.cwd }, error), - ), - ); + .pipe(Effect.mapError((error) => fromVcsError({ command: "gh", cwd: input.cwd }, error))); return GitHubCli.of({ execute, @@ -355,7 +344,6 @@ export const make = Effect.gen(function* () { if (!Result.isSuccess(decoded)) { return Effect.fail( new GitHubPullRequestListDecodeError({ - operation: "listOpenPullRequests", command: "gh", cwd: input.cwd, cause: decoded.failure, @@ -388,7 +376,6 @@ export const make = Effect.gen(function* () { if (!Result.isSuccess(decoded)) { return Effect.fail( new GitHubPullRequestDecodeError({ - operation: "getPullRequest", command: "gh", cwd: input.cwd, cause: decoded.failure, @@ -414,7 +401,6 @@ export const make = Effect.gen(function* () { Effect.mapError( (cause) => new GitHubRepositoryDecodeError({ - operation: "getRepositoryCloneUrls", command: "gh", cwd: input.cwd, cause, diff --git a/apps/server/src/sourceControl/GitHubSourceControlProvider.test.ts b/apps/server/src/sourceControl/GitHubSourceControlProvider.test.ts index 790fa49aae8..9e8a6829566 100644 --- a/apps/server/src/sourceControl/GitHubSourceControlProvider.test.ts +++ b/apps/server/src/sourceControl/GitHubSourceControlProvider.test.ts @@ -71,7 +71,6 @@ it.effect("maps GitHub PR summaries into provider-neutral change requests", () = it.effect("adds safe request context while retaining GitHub CLI causes", () => Effect.gen(function* () { const cause = new GitHubCli.GitHubPullRequestNotFoundError({ - operation: "execute", command: "gh", cwd: "/repo", cause: new Error("raw upstream detail that should remain in the cause"), diff --git a/apps/server/src/sourceControl/GitHubSourceControlProvider.ts b/apps/server/src/sourceControl/GitHubSourceControlProvider.ts index bb6e9b3311a..b5d5d3a55f8 100644 --- a/apps/server/src/sourceControl/GitHubSourceControlProvider.ts +++ b/apps/server/src/sourceControl/GitHubSourceControlProvider.ts @@ -159,7 +159,6 @@ export const make = Effect.gen(function* () { ) : Effect.fail( new GitHubCli.GitHubChangeRequestListDecodeError({ - operation: "listChangeRequests", command: "gh", cwd: input.cwd, cause: decoded.failure,