authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 18:41:25-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 18:41:25-07:00
log1a59277dc84105e2131c2bc828deb51c0b12d7f6
tree51f0ab8e4fadb3a3b9ce6d923db3522bc4c8b837
parent6424197a19a28d5992be555633a6e37fd0a58573
signature Signed by PGP key B5690EEEBB952194

[codex] Structure GitLab CLI failures (#3458)

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

3 files changed, 297 insertions(+), 126 deletions(-)

apps/server/src/sourceControl/GitLabCli.test.ts+29-2
...@@ -315,10 +315,11 @@ layer("GitLabCli.layer", (it) => {...@@ -315,10 +315,11 @@ layer("GitLabCli.layer", (it) => {
315 Effect.gen(function* () {315 Effect.gen(function* () {
316 const cause = new VcsProcessExitError({316 const cause = new VcsProcessExitError({
317 operation: "GitLabCli.execute",317 operation: "GitLabCli.execute",
318 command: "glab mr view 4888",318 command: "glab",
319 cwd: "/repo",319 cwd: "/repo",
320 exitCode: 1,320 exitCode: 1,
321 detail: "GET 404 merge request not found",321 detail: "GET 404 merge request not found",
322 failureKind: "not-found",
322 });323 });
323 mockedRun.mockReturnValueOnce(Effect.fail(cause));324 mockedRun.mockReturnValueOnce(Effect.fail(cause));
324325
...@@ -330,11 +331,37 @@ layer("GitLabCli.layer", (it) => {...@@ -330,11 +331,37 @@ layer("GitLabCli.layer", (it) => {
330 });331 });
331 }).pipe(Effect.flip);332 }).pipe(Effect.flip);
332333
333 assert.equal(error.message.includes("Merge request not found"), true);334 assert.equal(error.message.includes("Merge request 4888 was not found"), true);
335 assert.strictEqual(error._tag, "GitLabMergeRequestNotFoundError");
334 assert.strictEqual(error.command, "glab");336 assert.strictEqual(error.command, "glab");
335 assert.strictEqual(error.cwd, "/repo");337 assert.strictEqual(error.cwd, "/repo");
336 assert.strictEqual(error.cause, cause);338 assert.strictEqual(error.cause, cause);
337 assert.equal(error.message.includes(cause.detail), false);339 assert.equal(error.message.includes(cause.detail), false);
338 }),340 }),
339 );341 );
342
343 it.effect("keeps non-merge-request not-found failures generic", () =>
344 Effect.gen(function* () {
345 const cause = new VcsProcessExitError({
346 operation: "GitLabCli.execute",
347 command: "glab",
348 cwd: "/repo",
349 exitCode: 1,
350 detail: "GET 404 project not found",
351 failureKind: "not-found",
352 });
353 mockedRun.mockReturnValueOnce(Effect.fail(cause));
354
355 const error = yield* Effect.gen(function* () {
356 const glab = yield* GitLabCli.GitLabCli;
357 return yield* glab.getRepositoryCloneUrls({
358 cwd: "/repo",
359 repository: "missing/project",
360 });
361 }).pipe(Effect.flip);
362
363 assert.strictEqual(error._tag, "GitLabCliCommandError");
364 assert.strictEqual(error.cause, cause);
365 }),
366 );
340});367});
apps/server/src/sourceControl/GitLabCli.ts+267-122
...@@ -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 Match from "effect/Match";
4import * as Option from "effect/Option";5import * as Option from "effect/Option";
5import * as Result from "effect/Result";6import * as Result from "effect/Result";
6import * as Schema from "effect/Schema";7import * as Schema from "effect/Schema";
...@@ -21,13 +22,56 @@ import type * as SourceControlProvider from "./SourceControlProvider.ts";...@@ -21,13 +22,56 @@ import type * as SourceControlProvider from "./SourceControlProvider.ts";
2122
22const DEFAULT_TIMEOUT_MS = 30_000;23const DEFAULT_TIMEOUT_MS = 30_000;
2324
24export class GitLabCliError extends Schema.TaggedErrorClass<GitLabCliError>()("GitLabCliError", {25const gitLabCliExecutionErrorContext = {
25 operation: Schema.String,26 operation: Schema.Literal("execute"),
26 command: Schema.String,27 command: Schema.Literal("glab"),
27 cwd: Schema.String,28 cwd: Schema.String,
28 detail: Schema.String,29 cause: Schema.Defect(),
29 cause: Schema.optional(Schema.Defect()),30};
30}) {31
32const gitLabCliDecodeErrorContext = {
33 command: Schema.Literal("glab"),
34 cwd: Schema.String,
35 cause: Schema.Defect(),
36};
37
38export class GitLabCliUnavailableError extends Schema.TaggedErrorClass<GitLabCliUnavailableError>()(
39 "GitLabCliUnavailableError",
40 gitLabCliExecutionErrorContext,
41) {
42 get detail(): string {
43 return "GitLab CLI (`glab`) is required but not available on PATH.";
44 }
45
46 override get message(): string {
47 return `GitLab CLI failed in ${this.operation}: ${this.detail}`;
48 }
49}
50
51export class GitLabCliAuthenticationError extends Schema.TaggedErrorClass<GitLabCliAuthenticationError>()(
52 "GitLabCliAuthenticationError",
53 gitLabCliExecutionErrorContext,
54) {
55 get detail(): string {
56 return "GitLab CLI is not authenticated. Run `glab auth login` and retry.";
57 }
58
59 override get message(): string {
60 return `GitLab CLI failed in ${this.operation}: ${this.detail}`;
61 }
62}
63
64export class GitLabMergeRequestNotFoundError extends Schema.TaggedErrorClass<GitLabMergeRequestNotFoundError>()(
65 "GitLabMergeRequestNotFoundError",
66 {
67 ...gitLabCliExecutionErrorContext,
68 reference: Schema.String,
69 },
70) {
71 get detail(): string {
72 return `Merge request ${this.reference} was not found. Check the MR number or URL and try again.`;
73 }
74
31 override get message(): string {75 override get message(): string {
32 return `GitLab CLI failed in ${this.operation}: ${this.detail}`;76 return `GitLab CLI failed in ${this.operation}: ${this.detail}`;
33 }77 }
...@@ -37,52 +81,145 @@ export class GitLabCliError extends Schema.TaggedErrorClass<GitLabCliError>()("G...@@ -37,52 +81,145 @@ export class GitLabCliError extends Schema.TaggedErrorClass<GitLabCliError>()("G
37 readonly operation: "execute";81 readonly operation: "execute";
38 readonly command: "glab";82 readonly command: "glab";
39 readonly cwd: string;83 readonly cwd: string;
84 readonly reference: string;
40 },85 },
41 error: VcsError | unknown,86 error: VcsError,
42 ): GitLabCliError {87 ): GitLabCliError {
43 const lower = errorText(error).toLowerCase();88 if (error._tag === "VcsProcessExitError" && error.failureKind === "not-found") {
4489 return new GitLabMergeRequestNotFoundError({ ...context, cause: error });
45 if (lower.includes("command not found: glab") || isVcsProcessSpawnError(error)) {
46 return new GitLabCliError({
47 ...context,
48 detail: "GitLab CLI (`glab`) is required but not available on PATH.",
49 cause: error,
50 });
51 }90 }
5291
53 if (92 return GitLabCliCommandError.fromVcsError(
54 lower.includes("authentication failed") ||93 {
55 lower.includes("not logged in") ||94 operation: context.operation,
56 lower.includes("glab auth login") ||95 command: context.command,
57 lower.includes("token")96 cwd: context.cwd,
58 ) {97 },
59 return new GitLabCliError({98 error,
60 ...context,99 );
61 detail: "GitLab CLI is not authenticated. Run `glab auth login` and retry.",100 }
62 cause: error,101}
63 });
64 }
65102
66 if (103export class GitLabCliCommandError extends Schema.TaggedErrorClass<GitLabCliCommandError>()(
67 lower.includes("merge request not found") ||104 "GitLabCliCommandError",
68 lower.includes("not found") ||105 gitLabCliExecutionErrorContext,
69 lower.includes("404")106) {
70 ) {107 get detail(): string {
71 return new GitLabCliError({108 return "GitLab CLI command failed.";
72 ...context,109 }
73 detail: "Merge request not found. Check the MR number or URL and try again.",
74 cause: error,
75 });
76 }
77110
78 return new GitLabCliError({111 override get message(): string {
79 ...context,112 return `GitLab CLI failed in ${this.operation}: ${this.detail}`;
80 detail: "GitLab CLI command failed.",113 }
81 cause: error,114
115 static fromVcsError(
116 context: {
117 readonly operation: "execute";
118 readonly command: "glab";
119 readonly cwd: string;
120 },
121 error: VcsError,
122 ): GitLabCliError {
123 return Match.valueTags(error, {
124 VcsProcessSpawnError: (cause) => new GitLabCliUnavailableError({ ...context, cause }),
125 VcsProcessExitError: (cause) => {
126 switch (cause.failureKind) {
127 case "authentication":
128 return new GitLabCliAuthenticationError({ ...context, cause });
129 case "not-found":
130 case "command-failed":
131 case undefined:
132 return new GitLabCliCommandError({ ...context, cause });
133 }
134 },
135 VcsProcessTimeoutError: (cause) => new GitLabCliCommandError({ ...context, cause }),
136 VcsOutputDecodeError: (cause) => new GitLabCliCommandError({ ...context, cause }),
137 VcsRepositoryDetectionError: (cause) => new GitLabCliCommandError({ ...context, cause }),
138 VcsUnsupportedOperationError: (cause) => new GitLabCliCommandError({ ...context, cause }),
82 });139 });
83 }140 }
84}141}
85142
143export class GitLabMergeRequestListDecodeError extends Schema.TaggedErrorClass<GitLabMergeRequestListDecodeError>()(
144 "GitLabMergeRequestListDecodeError",
145 {
146 ...gitLabCliDecodeErrorContext,
147 operation: Schema.Literal("listMergeRequests"),
148 },
149) {
150 get detail(): string {
151 return "GitLab CLI returned invalid MR list JSON.";
152 }
153
154 override get message(): string {
155 return `GitLab CLI failed in ${this.operation}: ${this.detail}`;
156 }
157}
158
159export class GitLabMergeRequestDecodeError extends Schema.TaggedErrorClass<GitLabMergeRequestDecodeError>()(
160 "GitLabMergeRequestDecodeError",
161 {
162 ...gitLabCliDecodeErrorContext,
163 operation: Schema.Literal("getMergeRequest"),
164 reference: Schema.String,
165 },
166) {
167 get detail(): string {
168 return "GitLab CLI returned invalid merge request JSON.";
169 }
170
171 override get message(): string {
172 return `GitLab CLI failed in ${this.operation}: ${this.detail}`;
173 }
174}
175
176export class GitLabRepositoryDecodeError extends Schema.TaggedErrorClass<GitLabRepositoryDecodeError>()(
177 "GitLabRepositoryDecodeError",
178 {
179 ...gitLabCliDecodeErrorContext,
180 operation: Schema.Literals(["getRepositoryCloneUrls", "createRepository", "getDefaultBranch"]),
181 repository: Schema.optional(Schema.String),
182 },
183) {
184 get detail(): string {
185 return "GitLab CLI returned invalid repository JSON.";
186 }
187
188 override get message(): string {
189 return `GitLab CLI failed in ${this.operation}: ${this.detail}`;
190 }
191}
192
193export class GitLabNamespaceDecodeError extends Schema.TaggedErrorClass<GitLabNamespaceDecodeError>()(
194 "GitLabNamespaceDecodeError",
195 {
196 ...gitLabCliDecodeErrorContext,
197 operation: Schema.Literal("createRepository"),
198 namespacePath: Schema.String,
199 },
200) {
201 get detail(): string {
202 return "GitLab CLI returned invalid namespace JSON.";
203 }
204
205 override get message(): string {
206 return `GitLab CLI failed in ${this.operation}: ${this.detail}`;
207 }
208}
209
210export const GitLabCliError = Schema.Union([
211 GitLabCliUnavailableError,
212 GitLabCliAuthenticationError,
213 GitLabMergeRequestNotFoundError,
214 GitLabCliCommandError,
215 GitLabMergeRequestListDecodeError,
216 GitLabMergeRequestDecodeError,
217 GitLabRepositoryDecodeError,
218 GitLabNamespaceDecodeError,
219]);
220export type GitLabCliError = typeof GitLabCliError.Type;
221export const isGitLabCliError = Schema.is(GitLabCliError);
222
86export interface GitLabMergeRequestSummary {223export interface GitLabMergeRequestSummary {
87 readonly number: number;224 readonly number: number;
88 readonly title: string;225 readonly title: string;
...@@ -102,17 +239,6 @@ export interface GitLabRepositoryCloneUrls {...@@ -102,17 +239,6 @@ export interface GitLabRepositoryCloneUrls {
102 readonly sshUrl: string;239 readonly sshUrl: string;
103}240}
104241
105function errorText(error: VcsError | unknown): string {
106 if (typeof error === "object" && error !== null) {
107 const tag = "_tag" in error && typeof error._tag === "string" ? error._tag : "";
108 const detail = "detail" in error && typeof error.detail === "string" ? error.detail : "";
109 const message = "message" in error && typeof error.message === "string" ? error.message : "";
110 return [tag, detail, message].filter(Boolean).join("\n");
111 }
112
113 return String(error);
114}
115
116export class GitLabCli extends Context.Service<242export class GitLabCli extends Context.Service<
117 GitLabCli,243 GitLabCli,
118 {244 {
...@@ -168,15 +294,6 @@ export class GitLabCli extends Context.Service<...@@ -168,15 +294,6 @@ export class GitLabCli extends Context.Service<
168 }294 }
169>()("t3/sourceControl/GitLabCli") {}295>()("t3/sourceControl/GitLabCli") {}
170296
171function isVcsProcessSpawnError(error: unknown): boolean {
172 return (
173 typeof error === "object" &&
174 error !== null &&
175 "_tag" in error &&
176 error._tag === "VcsProcessSpawnError"
177 );
178}
179
180const RawGitLabRepositoryCloneUrlsSchema = Schema.Struct({297const RawGitLabRepositoryCloneUrlsSchema = Schema.Struct({
181 path_with_namespace: TrimmedNonEmptyString,298 path_with_namespace: TrimmedNonEmptyString,
182 web_url: TrimmedNonEmptyString,299 web_url: TrimmedNonEmptyString,
...@@ -192,6 +309,14 @@ const RawGitLabNamespaceSchema = Schema.Struct({...@@ -192,6 +309,14 @@ const RawGitLabNamespaceSchema = Schema.Struct({
192 id: Schema.Number,309 id: Schema.Number,
193});310});
194311
312const decodeGitLabRepositoryCloneUrls = Schema.decodeEffect(
313 Schema.fromJsonString(RawGitLabRepositoryCloneUrlsSchema),
314);
315const decodeGitLabDefaultBranch = Schema.decodeEffect(
316 Schema.fromJsonString(RawGitLabDefaultBranchSchema),
317);
318const decodeGitLabNamespace = Schema.decodeEffect(Schema.fromJsonString(RawGitLabNamespaceSchema));
319
195function normalizeRepositoryCloneUrls(320function normalizeRepositoryCloneUrls(
196 raw: Schema.Schema.Type<typeof RawGitLabRepositoryCloneUrlsSchema>,321 raw: Schema.Schema.Type<typeof RawGitLabRepositoryCloneUrlsSchema>,
197): GitLabRepositoryCloneUrls {322): GitLabRepositoryCloneUrls {
...@@ -202,27 +327,6 @@ function normalizeRepositoryCloneUrls(...@@ -202,27 +327,6 @@ function normalizeRepositoryCloneUrls(
202 };327 };
203}328}
204329
205function decodeGitLabJson<S extends Schema.Top>(
206 raw: string,
207 schema: S,
208 operation: "getRepositoryCloneUrls" | "getDefaultBranch" | "createRepository",
209 invalidDetail: string,
210 cwd: string,
211): Effect.Effect<S["Type"], GitLabCliError, S["DecodingServices"]> {
212 return Schema.decodeEffect(Schema.fromJsonString(schema))(raw).pipe(
213 Effect.mapError(
214 (error) =>
215 new GitLabCliError({
216 operation,
217 command: "glab",
218 cwd,
219 detail: invalidDetail,
220 cause: error,
221 }),
222 ),
223 );
224}
225
226function stateArgs(state: "open" | "closed" | "merged" | "all"): ReadonlyArray<string> {330function stateArgs(state: "open" | "closed" | "merged" | "all"): ReadonlyArray<string> {
227 switch (state) {331 switch (state) {
228 case "open":332 case "open":
...@@ -283,7 +387,10 @@ function parseRepositoryPath(repository: string): {...@@ -283,7 +387,10 @@ function parseRepositoryPath(repository: string): {
283export const make = Effect.gen(function* () {387export const make = Effect.gen(function* () {
284 const process = yield* VcsProcess.VcsProcess;388 const process = yield* VcsProcess.VcsProcess;
285389
286 const execute: GitLabCli["Service"]["execute"] = (input) =>390 const run = (
391 input: Parameters<GitLabCli["Service"]["execute"]>[0],
392 mapError: (error: VcsError) => GitLabCliError,
393 ) =>
287 process394 process
288 .run({395 .run({
289 operation: "GitLabCli.execute",396 operation: "GitLabCli.execute",
...@@ -292,14 +399,32 @@ export const make = Effect.gen(function* () {...@@ -292,14 +399,32 @@ export const make = Effect.gen(function* () {
292 cwd: input.cwd,399 cwd: input.cwd,
293 timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS,400 timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS,
294 })401 })
295 .pipe(402 .pipe(Effect.mapError(mapError));
296 Effect.mapError((error) =>403
297 GitLabCliError.fromVcsError(404 const execute: GitLabCli["Service"]["execute"] = (input) =>
298 { operation: "execute", command: "glab", cwd: input.cwd },405 run(input, (error) =>
299 error,406 GitLabCliCommandError.fromVcsError(
300 ),407 { operation: "execute", command: "glab", cwd: input.cwd },
301 ),408 error,
302 );409 ),
410 );
411
412 const executeMergeRequest = (input: {
413 readonly cwd: string;
414 readonly reference: string;
415 readonly args: ReadonlyArray<string>;
416 }) =>
417 run(input, (error) =>
418 GitLabMergeRequestNotFoundError.fromVcsError(
419 {
420 operation: "execute",
421 command: "glab",
422 cwd: input.cwd,
423 reference: input.reference,
424 },
425 error,
426 ),
427 );
303428
304 return GitLabCli.of({429 return GitLabCli.of({
305 execute,430 execute,
...@@ -326,11 +451,10 @@ export const make = Effect.gen(function* () {...@@ -326,11 +451,10 @@ export const make = Effect.gen(function* () {
326 Effect.flatMap((decoded) => {451 Effect.flatMap((decoded) => {
327 if (!Result.isSuccess(decoded)) {452 if (!Result.isSuccess(decoded)) {
328 return Effect.fail(453 return Effect.fail(
329 new GitLabCliError({454 new GitLabMergeRequestListDecodeError({
330 operation: "listMergeRequests",455 operation: "listMergeRequests",
331 command: "glab",456 command: "glab",
332 cwd: input.cwd,457 cwd: input.cwd,
333 detail: "GitLab CLI returned invalid MR list JSON.",
334 cause: decoded.failure,458 cause: decoded.failure,
335 }),459 }),
336 );460 );
...@@ -342,8 +466,9 @@ export const make = Effect.gen(function* () {...@@ -342,8 +466,9 @@ export const make = Effect.gen(function* () {
342 ),466 ),
343 ),467 ),
344 getMergeRequest: (input) =>468 getMergeRequest: (input) =>
345 execute({469 executeMergeRequest({
346 cwd: input.cwd,470 cwd: input.cwd,
471 reference: input.reference,
347 args: ["mr", "view", input.reference, "--output", "json"],472 args: ["mr", "view", input.reference, "--output", "json"],
348 }).pipe(473 }).pipe(
349 Effect.map((result) => result.stdout.trim()),474 Effect.map((result) => result.stdout.trim()),
...@@ -352,11 +477,11 @@ export const make = Effect.gen(function* () {...@@ -352,11 +477,11 @@ export const make = Effect.gen(function* () {
352 Effect.flatMap((decoded) => {477 Effect.flatMap((decoded) => {
353 if (!Result.isSuccess(decoded)) {478 if (!Result.isSuccess(decoded)) {
354 return Effect.fail(479 return Effect.fail(
355 new GitLabCliError({480 new GitLabMergeRequestDecodeError({
356 operation: "getMergeRequest",481 operation: "getMergeRequest",
357 command: "glab",482 command: "glab",
358 cwd: input.cwd,483 cwd: input.cwd,
359 detail: "GitLab CLI returned invalid merge request JSON.",484 reference: input.reference,
360 cause: decoded.failure,485 cause: decoded.failure,
361 }),486 }),
362 );487 );
...@@ -374,12 +499,17 @@ export const make = Effect.gen(function* () {...@@ -374,12 +499,17 @@ export const make = Effect.gen(function* () {
374 }).pipe(499 }).pipe(
375 Effect.map((result) => result.stdout.trim()),500 Effect.map((result) => result.stdout.trim()),
376 Effect.flatMap((raw) =>501 Effect.flatMap((raw) =>
377 decodeGitLabJson(502 decodeGitLabRepositoryCloneUrls(raw).pipe(
378 raw,503 Effect.mapError(
379 RawGitLabRepositoryCloneUrlsSchema,504 (cause) =>
380 "getRepositoryCloneUrls",505 new GitLabRepositoryDecodeError({
381 "GitLab CLI returned invalid repository JSON.",506 operation: "getRepositoryCloneUrls",
382 input.cwd,507 command: "glab",
508 cwd: input.cwd,
509 repository: input.repository,
510 cause,
511 }),
512 ),
383 ),513 ),
384 ),514 ),
385 Effect.map(normalizeRepositoryCloneUrls),515 Effect.map(normalizeRepositoryCloneUrls),
...@@ -393,12 +523,17 @@ export const make = Effect.gen(function* () {...@@ -393,12 +523,17 @@ export const make = Effect.gen(function* () {
393 }).pipe(523 }).pipe(
394 Effect.map((result) => result.stdout.trim()),524 Effect.map((result) => result.stdout.trim()),
395 Effect.flatMap((raw) =>525 Effect.flatMap((raw) =>
396 decodeGitLabJson(526 decodeGitLabNamespace(raw).pipe(
397 raw,527 Effect.mapError(
398 RawGitLabNamespaceSchema,528 (cause) =>
399 "createRepository",529 new GitLabNamespaceDecodeError({
400 "GitLab CLI returned invalid namespace JSON.",530 operation: "createRepository",
401 input.cwd,531 command: "glab",
532 cwd: input.cwd,
533 namespacePath,
534 cause,
535 }),
536 ),
402 ),537 ),
403 ),538 ),
404 Effect.map((namespace) => namespace.id),539 Effect.map((namespace) => namespace.id),
...@@ -428,12 +563,17 @@ export const make = Effect.gen(function* () {...@@ -428,12 +563,17 @@ export const make = Effect.gen(function* () {
428 ),563 ),
429 Effect.map((result) => result.stdout.trim()),564 Effect.map((result) => result.stdout.trim()),
430 Effect.flatMap((raw) =>565 Effect.flatMap((raw) =>
431 decodeGitLabJson(566 decodeGitLabRepositoryCloneUrls(raw).pipe(
432 raw,567 Effect.mapError(
433 RawGitLabRepositoryCloneUrlsSchema,568 (cause) =>
434 "createRepository",569 new GitLabRepositoryDecodeError({
435 "GitLab CLI returned invalid repository JSON.",570 operation: "createRepository",
436 input.cwd,571 command: "glab",
572 cwd: input.cwd,
573 repository: input.repository,
574 cause,
575 }),
576 ),
437 ),577 ),
438 ),578 ),
439 Effect.map(normalizeRepositoryCloneUrls),579 Effect.map(normalizeRepositoryCloneUrls),
...@@ -467,19 +607,24 @@ export const make = Effect.gen(function* () {...@@ -467,19 +607,24 @@ export const make = Effect.gen(function* () {
467 }).pipe(607 }).pipe(
468 Effect.map((result) => result.stdout.trim()),608 Effect.map((result) => result.stdout.trim()),
469 Effect.flatMap((raw) =>609 Effect.flatMap((raw) =>
470 decodeGitLabJson(610 decodeGitLabDefaultBranch(raw).pipe(
471 raw,611 Effect.mapError(
472 RawGitLabDefaultBranchSchema,612 (cause) =>
473 "getDefaultBranch",613 new GitLabRepositoryDecodeError({
474 "GitLab CLI returned invalid repository JSON.",614 operation: "getDefaultBranch",
475 input.cwd,615 command: "glab",
616 cwd: input.cwd,
617 cause,
618 }),
619 ),
476 ),620 ),
477 ),621 ),
478 Effect.map((value) => value.default_branch ?? null),622 Effect.map((value) => value.default_branch ?? null),
479 ),623 ),
480 checkoutMergeRequest: (input) =>624 checkoutMergeRequest: (input) =>
481 execute({625 executeMergeRequest({
482 cwd: input.cwd,626 cwd: input.cwd,
627 reference: input.reference,
483 args: ["mr", "checkout", input.reference],628 args: ["mr", "checkout", input.reference],
484 }).pipe(Effect.asVoid),629 }).pipe(Effect.asVoid),
485 });630 });
apps/server/src/sourceControl/GitLabSourceControlProvider.test.ts+1-2
...@@ -54,11 +54,10 @@ it.effect("maps GitLab MR summaries into provider-neutral change requests", () =...@@ -54,11 +54,10 @@ it.effect("maps GitLab MR summaries into provider-neutral change requests", () =
5454
55it.effect("adds repository context while retaining GitLab CLI causes", () =>55it.effect("adds repository context while retaining GitLab CLI causes", () =>
56 Effect.gen(function* () {56 Effect.gen(function* () {
57 const cause = new GitLabCli.GitLabCliError({57 const cause = new GitLabCli.GitLabCliCommandError({
58 operation: "execute",58 operation: "execute",
59 command: "glab",59 command: "glab",
60 cwd: "/repo",60 cwd: "/repo",
61 detail: "GitLab CLI command failed.",
62 cause: new Error("raw upstream detail that should remain in the cause"),61 cause: new Error("raw upstream detail that should remain in the cause"),
63 });62 });
64 const provider = yield* makeProvider({63 const provider = yield* makeProvider({