authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 19:14:35-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 19:14:35-07:00
log61e6d89d69240120dd08fae72459e5fd7e7a2908
treef6275b6b7752c554f09abeff229b2373c64d4c67
parent1b2f39d018410a9e09c67e2629f6ca1562860e6b
signature Signed by PGP key B5690EEEBB952194

[codex] Structure GitHub CLI failures (#3456)

Co-authored-by: codex <codex@users.noreply.github.com>

5 files changed, 212 insertions(+), 158 deletions(-)

apps/server/src/git/GitManager.test.ts+17-39
...@@ -171,20 +171,8 @@ function runGitSyncForFakeGh(cwd: string, args: readonly string[]): void {...@@ -171,20 +171,8 @@ function runGitSyncForFakeGh(cwd: string, args: readonly string[]): void {
171 if (result.status === 0) {171 if (result.status === 0) {
172 return;172 return;
173 }173 }
174 throw new GitHubCli.GitHubCliError({174 throw new Error(
175 operation: "execute",175 `Failed to simulate gh checkout with git ${args.join(" ")}: ${result.stderr?.trim() || "unknown error"}`,
176 command: "gh",
177 cwd,
178 detail: `Failed to simulate gh checkout with git ${args.join(" ")}: ${result.stderr?.trim() || "unknown error"}`,
179 });
180}
181
182function isGitHubCliError(error: unknown): error is GitHubCli.GitHubCliError {
183 return (
184 typeof error === "object" &&
185 error !== null &&
186 "_tag" in error &&
187 (error as { _tag?: unknown })._tag === "GitHubCliError"
188 );176 );
189}177}
190178
...@@ -478,16 +466,12 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {...@@ -478,16 +466,12 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
478 return fakeGhOutput("");466 return fakeGhOutput("");
479 },467 },
480 catch: (error) =>468 catch: (error) =>
481 isGitHubCliError(error)469 GitHubCli.isGitHubCliError(error)
482 ? error470 ? error
483 : new GitHubCli.GitHubCliError({471 : new GitHubCli.GitHubCliCommandError({
484 operation: "execute",
485 command: "gh",472 command: "gh",
486 cwd: input.cwd,473 cwd: input.cwd,
487 detail:474 cause: error,
488 error instanceof Error
489 ? `Failed to simulate gh checkout: ${error.message}`
490 : "Failed to simulate gh checkout.",
491 }),475 }),
492 });476 });
493 }477 }
...@@ -498,11 +482,10 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {...@@ -498,11 +482,10 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
498 const cloneUrls = scenario.repositoryCloneUrls?.[repository];482 const cloneUrls = scenario.repositoryCloneUrls?.[repository];
499 if (!cloneUrls) {483 if (!cloneUrls) {
500 return Effect.fail(484 return Effect.fail(
501 new GitHubCli.GitHubCliError({485 new GitHubCli.GitHubCliCommandError({
502 operation: "execute",
503 command: "gh",486 command: "gh",
504 cwd: input.cwd,487 cwd: input.cwd,
505 detail: `Unexpected repository lookup: ${repository}`,488 cause: new Error(`Unexpected repository lookup: ${repository}`),
506 }),489 }),
507 );490 );
508 }491 }
...@@ -520,11 +503,10 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {...@@ -520,11 +503,10 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
520 }503 }
521504
522 return Effect.fail(505 return Effect.fail(
523 new GitHubCli.GitHubCliError({506 new GitHubCli.GitHubCliCommandError({
524 operation: "execute",
525 command: "gh",507 command: "gh",
526 cwd: input.cwd,508 cwd: input.cwd,
527 detail: `Unexpected gh command: ${args.join(" ")}`,509 cause: new Error(`Unexpected gh command: ${args.join(" ")}`),
528 }),510 }),
529 );511 );
530 };512 };
...@@ -601,11 +583,10 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {...@@ -601,11 +583,10 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
601 }).pipe(Effect.map((result) => JSON.parse(result.stdout))),583 }).pipe(Effect.map((result) => JSON.parse(result.stdout))),
602 createRepository: (input) =>584 createRepository: (input) =>
603 Effect.fail(585 Effect.fail(
604 new GitHubCli.GitHubCliError({586 new GitHubCli.GitHubCliCommandError({
605 operation: "createRepository",
606 command: "gh",587 command: "gh",
607 cwd: input.cwd,588 cwd: input.cwd,
608 detail: `Unexpected repository create: ${input.repository}`,589 cause: new Error(`Unexpected repository create: ${input.repository}`),
609 }),590 }),
610 ),591 ),
611 checkoutPullRequest: (input) =>592 checkoutPullRequest: (input) =>
...@@ -1341,11 +1322,10 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1341,11 +1322,10 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
13411322
1342 const { manager } = yield* makeManager({1323 const { manager } = yield* makeManager({
1343 ghScenario: {1324 ghScenario: {
1344 failWith: new GitHubCli.GitHubCliError({1325 failWith: new GitHubCli.GitHubCliUnavailableError({
1345 operation: "execute",
1346 command: "gh",1326 command: "gh",
1347 cwd: repoDir,1327 cwd: repoDir,
1348 detail: "GitHub CLI (`gh`) is required but not available on PATH.",1328 cause: new Error("gh is not available on PATH"),
1349 }),1329 }),
1350 },1330 },
1351 });1331 });
...@@ -2483,11 +2463,10 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -2483,11 +2463,10 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
24832463
2484 const { manager } = yield* makeManager({2464 const { manager } = yield* makeManager({
2485 ghScenario: {2465 ghScenario: {
2486 failWith: new GitHubCli.GitHubCliError({2466 failWith: new GitHubCli.GitHubCliUnavailableError({
2487 operation: "execute",
2488 command: "gh",2467 command: "gh",
2489 cwd: repoDir,2468 cwd: repoDir,
2490 detail: "GitHub CLI (`gh`) is required but not available on PATH.",2469 cause: new Error("gh is not available on PATH"),
2491 }),2470 }),
2492 },2471 },
2493 });2472 });
...@@ -2514,11 +2493,10 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -2514,11 +2493,10 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
25142493
2515 const { manager } = yield* makeManager({2494 const { manager } = yield* makeManager({
2516 ghScenario: {2495 ghScenario: {
2517 failWith: new GitHubCli.GitHubCliError({2496 failWith: new GitHubCli.GitHubCliAuthenticationError({
2518 operation: "execute",
2519 command: "gh",2497 command: "gh",
2520 cwd: repoDir,2498 cwd: repoDir,
2521 detail: "GitHub CLI is not authenticated. Run `gh auth login` and retry.",2499 cause: new Error("gh is not authenticated"),
2522 }),2500 }),
2523 },2501 },
2524 });2502 });
apps/server/src/sourceControl/GitHubCli.test.ts+25-1
...@@ -1,8 +1,9 @@...@@ -1,8 +1,9 @@
1import { assert, it, afterEach, describe, expect, vi } from "@effect/vitest";1import { assert, it, afterEach, describe, expect, vi } from "@effect/vitest";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";3import * as Layer from "effect/Layer";
4import * as PlatformError from "effect/PlatformError";
4import { ChildProcessSpawner } from "effect/unstable/process";5import { ChildProcessSpawner } from "effect/unstable/process";
5import { VcsProcessExitError } from "@t3tools/contracts";6import { VcsProcessExitError, VcsProcessSpawnError } from "@t3tools/contracts";
67
7import * as VcsProcess from "../vcs/VcsProcess.ts";8import * as VcsProcess from "../vcs/VcsProcess.ts";
8import * as GitHubCli from "./GitHubCli.ts";9import * as GitHubCli from "./GitHubCli.ts";
...@@ -30,6 +31,27 @@ afterEach(() => {...@@ -30,6 +31,27 @@ afterEach(() => {
30});31});
3132
32describe("GitHubCli.layer", () => {33describe("GitHubCli.layer", () => {
34 it("does not classify a missing cwd as an unavailable gh executable", () => {
35 const context = { command: "gh", cwd: "/repo" } as const;
36 const missingCwd = new VcsProcessSpawnError({
37 operation: "GitHubCli.execute",
38 command: "gh",
39 cwd: context.cwd,
40 cause: PlatformError.systemError({
41 _tag: "NotFound",
42 module: "FileSystem",
43 method: "access",
44 pathOrDescriptor: context.cwd,
45 }),
46 });
47
48 const commandFailure = GitHubCli.fromVcsError(context, missingCwd);
49
50 assert.equal(commandFailure._tag, "GitHubCliCommandError");
51 assert.strictEqual(commandFailure.cause, missingCwd);
52 assert.notProperty(commandFailure, "operation");
53 });
54
33 it.effect("parses pull request view output", () =>55 it.effect("parses pull request view output", () =>
34 Effect.gen(function* () {56 Effect.gen(function* () {
35 mockRun.mockReturnValueOnce(57 mockRun.mockReturnValueOnce(
...@@ -274,6 +296,7 @@ describe("GitHubCli.layer", () => {...@@ -274,6 +296,7 @@ describe("GitHubCli.layer", () => {
274 command: "gh pr view",296 command: "gh pr view",
275 cwd: "/repo",297 cwd: "/repo",
276 exitCode: 1,298 exitCode: 1,
299 failureKind: "not-found",
277 detail:300 detail:
278 "GraphQL: Could not resolve to a PullRequest with the number of 4888. (repository.pullRequest)",301 "GraphQL: Could not resolve to a PullRequest with the number of 4888. (repository.pullRequest)",
279 });302 });
...@@ -288,6 +311,7 @@ describe("GitHubCli.layer", () => {...@@ -288,6 +311,7 @@ describe("GitHubCli.layer", () => {
288 .pipe(Effect.flip);311 .pipe(Effect.flip);
289312
290 assert.equal(error.message.includes("Pull request not found"), true);313 assert.equal(error.message.includes("Pull request not found"), true);
314 assert.strictEqual(error._tag, "GitHubPullRequestNotFoundError");
291 assert.strictEqual(error.command, "gh");315 assert.strictEqual(error.command, "gh");
292 assert.strictEqual(error.cwd, "/repo");316 assert.strictEqual(error.cwd, "/repo");
293 assert.strictEqual(error.cause, cause);317 assert.strictEqual(error.cause, cause);
apps/server/src/sourceControl/GitHubCli.ts+165-105
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1import * as Context from "effect/Context";1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";3import * as Layer from "effect/Layer";
4import * as PlatformError from "effect/PlatformError";
4import * as Result from "effect/Result";5import * as Result from "effect/Result";
5import * as Schema from "effect/Schema";6import * as Schema from "effect/Schema";
67
...@@ -18,69 +19,165 @@ import {...@@ -18,69 +19,165 @@ import {
1819
19const DEFAULT_TIMEOUT_MS = 30_000;20const DEFAULT_TIMEOUT_MS = 30_000;
2021
21export class GitHubCliError extends Schema.TaggedErrorClass<GitHubCliError>()("GitHubCliError", {22const gitHubCliFailureFields = {
22 operation: Schema.String,23 command: Schema.Literal("gh"),
23 command: Schema.String,
24 cwd: Schema.String,24 cwd: Schema.String,
25 detail: Schema.String,25 cause: Schema.Defect(),
26 cause: Schema.optional(Schema.Defect()),26} as const;
27}) {27
28export class GitHubCliUnavailableError extends Schema.TaggedErrorClass<GitHubCliUnavailableError>()(
29 "GitHubCliUnavailableError",
30 gitHubCliFailureFields,
31) {
32 get detail(): string {
33 return "GitHub CLI (`gh`) is required but not available on PATH.";
34 }
35
28 override get message(): string {36 override get message(): string {
29 return `GitHub CLI failed in ${this.operation}: ${this.detail}`;37 return `GitHub CLI failed in execute: ${this.detail}`;
30 }38 }
39}
3140
32 static fromVcsError(41export class GitHubCliAuthenticationError extends Schema.TaggedErrorClass<GitHubCliAuthenticationError>()(
33 context: {42 "GitHubCliAuthenticationError",
34 readonly operation: "execute";43 gitHubCliFailureFields,
35 readonly command: "gh";44) {
36 readonly cwd: string;45 get detail(): string {
37 },46 return "GitHub CLI is not authenticated. Run `gh auth login` and retry.";
38 error: VcsError | unknown,47 }
39 ): GitHubCliError {
40 const lower = errorText(error).toLowerCase();
41
42 if (lower.includes("command not found: gh") || lower.includes("enoent")) {
43 return new GitHubCliError({
44 ...context,
45 detail: "GitHub CLI (`gh`) is required but not available on PATH.",
46 cause: error,
47 });
48 }
4948
50 if (49 override get message(): string {
51 lower.includes("authentication failed") ||50 return `GitHub CLI failed in execute: ${this.detail}`;
52 lower.includes("not logged in") ||51 }
53 lower.includes("gh auth login") ||52}
54 lower.includes("no oauth token")
55 ) {
56 return new GitHubCliError({
57 ...context,
58 detail: "GitHub CLI is not authenticated. Run `gh auth login` and retry.",
59 cause: error,
60 });
61 }
6253
63 if (54export class GitHubPullRequestNotFoundError extends Schema.TaggedErrorClass<GitHubPullRequestNotFoundError>()(
64 lower.includes("could not resolve to a pullrequest") ||55 "GitHubPullRequestNotFoundError",
65 lower.includes("repository.pullrequest") ||56 gitHubCliFailureFields,
66 lower.includes("no pull requests found for branch") ||57) {
67 lower.includes("pull request not found")58 get detail(): string {
68 ) {59 return "Pull request not found. Check the PR number or URL and try again.";
69 return new GitHubCliError({60 }
70 ...context,61
71 detail: "Pull request not found. Check the PR number or URL and try again.",62 override get message(): string {
72 cause: error,63 return `GitHub CLI failed in execute: ${this.detail}`;
73 });64 }
74 }65}
66
67export class GitHubCliCommandError extends Schema.TaggedErrorClass<GitHubCliCommandError>()(
68 "GitHubCliCommandError",
69 gitHubCliFailureFields,
70) {
71 get detail(): string {
72 return "GitHub CLI command failed.";
73 }
74
75 override get message(): string {
76 return `GitHub CLI failed in execute: ${this.detail}`;
77 }
78}
79
80const gitHubCliDecodeFields = {
81 command: Schema.Literal("gh"),
82 cwd: Schema.String,
83 cause: Schema.Defect(),
84} as const;
85
86export class GitHubPullRequestListDecodeError extends Schema.TaggedErrorClass<GitHubPullRequestListDecodeError>()(
87 "GitHubPullRequestListDecodeError",
88 gitHubCliDecodeFields,
89) {
90 get detail(): string {
91 return "GitHub CLI returned invalid PR list JSON.";
92 }
93
94 override get message(): string {
95 return `GitHub CLI failed in listOpenPullRequests: ${this.detail}`;
96 }
97}
98
99export class GitHubChangeRequestListDecodeError extends Schema.TaggedErrorClass<GitHubChangeRequestListDecodeError>()(
100 "GitHubChangeRequestListDecodeError",
101 gitHubCliDecodeFields,
102) {
103 get detail(): string {
104 return "GitHub CLI returned invalid change request JSON.";
105 }
75106
76 return new GitHubCliError({107 override get message(): string {
77 ...context,108 return `GitHub CLI failed in listChangeRequests: ${this.detail}`;
78 detail: "GitHub CLI command failed.",
79 cause: error,
80 });
81 }109 }
82}110}
83111
112export class GitHubPullRequestDecodeError extends Schema.TaggedErrorClass<GitHubPullRequestDecodeError>()(
113 "GitHubPullRequestDecodeError",
114 gitHubCliDecodeFields,
115) {
116 get detail(): string {
117 return "GitHub CLI returned invalid pull request JSON.";
118 }
119
120 override get message(): string {
121 return `GitHub CLI failed in getPullRequest: ${this.detail}`;
122 }
123}
124
125export class GitHubRepositoryDecodeError extends Schema.TaggedErrorClass<GitHubRepositoryDecodeError>()(
126 "GitHubRepositoryDecodeError",
127 gitHubCliDecodeFields,
128) {
129 get detail(): string {
130 return "GitHub CLI returned invalid repository JSON.";
131 }
132
133 override get message(): string {
134 return `GitHub CLI failed in getRepositoryCloneUrls: ${this.detail}`;
135 }
136}
137
138export const GitHubCliError = Schema.Union([
139 GitHubCliUnavailableError,
140 GitHubCliAuthenticationError,
141 GitHubPullRequestNotFoundError,
142 GitHubCliCommandError,
143 GitHubPullRequestListDecodeError,
144 GitHubChangeRequestListDecodeError,
145 GitHubPullRequestDecodeError,
146 GitHubRepositoryDecodeError,
147]);
148export type GitHubCliError = typeof GitHubCliError.Type;
149
150export const isGitHubCliError = Schema.is(GitHubCliError);
151
152export function fromVcsError(
153 context: {
154 readonly command: "gh";
155 readonly cwd: string;
156 },
157 error: VcsError,
158): GitHubCliError {
159 if (
160 error._tag === "VcsProcessSpawnError" &&
161 error.cause instanceof PlatformError.PlatformError &&
162 error.cause.reason._tag === "NotFound" &&
163 error.cause.reason.module === "ChildProcess" &&
164 error.cause.reason.method === "spawn"
165 ) {
166 return new GitHubCliUnavailableError({ ...context, cause: error });
167 }
168
169 if (error._tag === "VcsProcessExitError") {
170 if (error.failureKind === "authentication") {
171 return new GitHubCliAuthenticationError({ ...context, cause: error });
172 }
173 if (error.failureKind === "not-found") {
174 return new GitHubPullRequestNotFoundError({ ...context, cause: error });
175 }
176 }
177
178 return new GitHubCliCommandError({ ...context, cause: error });
179}
180
84export interface GitHubPullRequestSummary {181export interface GitHubPullRequestSummary {
85 readonly number: number;182 readonly number: number;
86 readonly title: string;183 readonly title: string;
...@@ -150,22 +247,14 @@ export class GitHubCli extends Context.Service<...@@ -150,22 +247,14 @@ export class GitHubCli extends Context.Service<
150 }247 }
151>()("t3/sourceControl/GitHubCli") {}248>()("t3/sourceControl/GitHubCli") {}
152249
153function errorText(error: VcsError | unknown): string {
154 if (typeof error === "object" && error !== null) {
155 const tag = "_tag" in error && typeof error._tag === "string" ? error._tag : "";
156 const detail = "detail" in error && typeof error.detail === "string" ? error.detail : "";
157 const message = "message" in error && typeof error.message === "string" ? error.message : "";
158 return [tag, detail, message].filter(Boolean).join("\n");
159 }
160
161 return String(error);
162}
163
164const RawGitHubRepositoryCloneUrlsSchema = Schema.Struct({250const RawGitHubRepositoryCloneUrlsSchema = Schema.Struct({
165 nameWithOwner: TrimmedNonEmptyString,251 nameWithOwner: TrimmedNonEmptyString,
166 url: TrimmedNonEmptyString,252 url: TrimmedNonEmptyString,
167 sshUrl: TrimmedNonEmptyString,253 sshUrl: TrimmedNonEmptyString,
168});254});
255const decodeRawGitHubRepositoryCloneUrls = Schema.decodeEffect(
256 Schema.fromJsonString(RawGitHubRepositoryCloneUrlsSchema),
257);
169258
170function normalizeRepositoryCloneUrls(259function normalizeRepositoryCloneUrls(
171 raw: Schema.Schema.Type<typeof RawGitHubRepositoryCloneUrlsSchema>,260 raw: Schema.Schema.Type<typeof RawGitHubRepositoryCloneUrlsSchema>,
...@@ -214,27 +303,6 @@ function deriveRepositoryCloneUrlsFromCreateOutput(...@@ -214,27 +303,6 @@ function deriveRepositoryCloneUrlsFromCreateOutput(
214 };303 };
215}304}
216305
217function decodeGitHubJson<S extends Schema.Top>(
218 raw: string,
219 schema: S,
220 operation: "listOpenPullRequests" | "getPullRequest" | "getRepositoryCloneUrls",
221 invalidDetail: string,
222 cwd: string,
223): Effect.Effect<S["Type"], GitHubCliError, S["DecodingServices"]> {
224 return Schema.decodeEffect(Schema.fromJsonString(schema))(raw).pipe(
225 Effect.mapError(
226 (error) =>
227 new GitHubCliError({
228 operation,
229 command: "gh",
230 cwd,
231 detail: invalidDetail,
232 cause: error,
233 }),
234 ),
235 );
236}
237
238export const make = Effect.gen(function* () {306export const make = Effect.gen(function* () {
239 const process = yield* VcsProcess.VcsProcess;307 const process = yield* VcsProcess.VcsProcess;
240308
...@@ -247,14 +315,7 @@ export const make = Effect.gen(function* () {...@@ -247,14 +315,7 @@ export const make = Effect.gen(function* () {
247 cwd: input.cwd,315 cwd: input.cwd,
248 timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS,316 timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS,
249 })317 })
250 .pipe(318 .pipe(Effect.mapError((error) => fromVcsError({ command: "gh", cwd: input.cwd }, error)));
251 Effect.mapError((error) =>
252 GitHubCliError.fromVcsError(
253 { operation: "execute", command: "gh", cwd: input.cwd },
254 error,
255 ),
256 ),
257 );
258319
259 return GitHubCli.of({320 return GitHubCli.of({
260 execute,321 execute,
...@@ -282,11 +343,9 @@ export const make = Effect.gen(function* () {...@@ -282,11 +343,9 @@ export const make = Effect.gen(function* () {
282 Effect.flatMap((decoded) => {343 Effect.flatMap((decoded) => {
283 if (!Result.isSuccess(decoded)) {344 if (!Result.isSuccess(decoded)) {
284 return Effect.fail(345 return Effect.fail(
285 new GitHubCliError({346 new GitHubPullRequestListDecodeError({
286 operation: "listOpenPullRequests",
287 command: "gh",347 command: "gh",
288 cwd: input.cwd,348 cwd: input.cwd,
289 detail: "GitHub CLI returned invalid PR list JSON.",
290 cause: decoded.failure,349 cause: decoded.failure,
291 }),350 }),
292 );351 );
...@@ -316,11 +375,9 @@ export const make = Effect.gen(function* () {...@@ -316,11 +375,9 @@ export const make = Effect.gen(function* () {
316 Effect.flatMap((decoded) => {375 Effect.flatMap((decoded) => {
317 if (!Result.isSuccess(decoded)) {376 if (!Result.isSuccess(decoded)) {
318 return Effect.fail(377 return Effect.fail(
319 new GitHubCliError({378 new GitHubPullRequestDecodeError({
320 operation: "getPullRequest",
321 command: "gh",379 command: "gh",
322 cwd: input.cwd,380 cwd: input.cwd,
323 detail: "GitHub CLI returned invalid pull request JSON.",
324 cause: decoded.failure,381 cause: decoded.failure,
325 }),382 }),
326 );383 );
...@@ -340,12 +397,15 @@ export const make = Effect.gen(function* () {...@@ -340,12 +397,15 @@ export const make = Effect.gen(function* () {
340 }).pipe(397 }).pipe(
341 Effect.map((result) => result.stdout.trim()),398 Effect.map((result) => result.stdout.trim()),
342 Effect.flatMap((raw) =>399 Effect.flatMap((raw) =>
343 decodeGitHubJson(400 decodeRawGitHubRepositoryCloneUrls(raw).pipe(
344 raw,401 Effect.mapError(
345 RawGitHubRepositoryCloneUrlsSchema,402 (cause) =>
346 "getRepositoryCloneUrls",403 new GitHubRepositoryDecodeError({
347 "GitHub CLI returned invalid repository JSON.",404 command: "gh",
348 input.cwd,405 cwd: input.cwd,
406 cause,
407 }),
408 ),
349 ),409 ),
350 ),410 ),
351 Effect.map(normalizeRepositoryCloneUrls),411 Effect.map(normalizeRepositoryCloneUrls),
apps/server/src/sourceControl/GitHubSourceControlProvider.test.ts+1-3
...@@ -70,11 +70,9 @@ it.effect("maps GitHub PR summaries into provider-neutral change requests", () =...@@ -70,11 +70,9 @@ it.effect("maps GitHub PR summaries into provider-neutral change requests", () =
7070
71it.effect("adds safe request context while retaining GitHub CLI causes", () =>71it.effect("adds safe request context while retaining GitHub CLI causes", () =>
72 Effect.gen(function* () {72 Effect.gen(function* () {
73 const cause = new GitHubCli.GitHubCliError({73 const cause = new GitHubCli.GitHubPullRequestNotFoundError({
74 operation: "execute",
75 command: "gh",74 command: "gh",
76 cwd: "/repo",75 cwd: "/repo",
77 detail: "Pull request not found. Check the PR number or URL and try again.",
78 cause: new Error("raw upstream detail that should remain in the cause"),76 cause: new Error("raw upstream detail that should remain in the cause"),
79 });77 });
80 const provider = yield* makeProvider({78 const provider = yield* makeProvider({
apps/server/src/sourceControl/GitHubSourceControlProvider.ts+4-10
...@@ -158,23 +158,17 @@ export const make = Effect.gen(function* () {...@@ -158,23 +158,17 @@ export const make = Effect.gen(function* () {
158 })),158 })),
159 )159 )
160 : Effect.fail(160 : Effect.fail(
161 new SourceControlProviderError({161 new GitHubCli.GitHubChangeRequestListDecodeError({
162 provider: "github",
163 operation: "listChangeRequests",
164 command: "gh",162 command: "gh",
165 cwd: input.cwd,163 cwd: input.cwd,
166 reference: SourceControlProvider.transportSafeSourceControlErrorValue(
167 input.headSelector,
168 ),
169 detail: "GitHub CLI returned invalid change request JSON.",
170 cause: decoded.failure,164 cause: decoded.failure,
171 }),165 }),
172 ),166 ),
173 ),167 ),
174 );168 );
175 }),169 }),
176 Effect.catchTags({170 Effect.mapError(
177 GitHubCliError: (error) =>171 (error) =>
178 new SourceControlProviderError({172 new SourceControlProviderError({
179 provider: "github",173 provider: "github",
180 operation: "listChangeRequests",174 operation: "listChangeRequests",
...@@ -186,7 +180,7 @@ export const make = Effect.gen(function* () {...@@ -186,7 +180,7 @@ export const make = Effect.gen(function* () {
186 detail: error.detail,180 detail: error.detail,
187 cause: error,181 cause: error,
188 }),182 }),
189 }),183 ),
190 );184 );
191 };185 };
192186