authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-05-02 16:59:03-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-05-02 16:59:03-07:00
log350d76ee141004e202d6c365d29ffdb29bde66c6
tree6fcae55f5988fd7e0bf0e34994bb9b274e33e8ba
parent6d7fe2eeb7d2731da3f7f69988056ff7a5d3b921
signature Signed by PGP key B5690EEEBB952194

[codex] Align GitHub source control adapter errors (#2476)

Co-authored-by: Julius Marminge <julius@macmini.local>

6 files changed, 143 insertions(+), 144 deletions(-)

apps/server/src/git/GitManager.test.ts+2-1
...@@ -14,9 +14,10 @@ import type {...@@ -14,9 +14,10 @@ import type {
14 ThreadId,14 ThreadId,
15} from "@t3tools/contracts";15} from "@t3tools/contracts";
1616
17import { GitCommandError, GitHubCliError, TextGenerationError } from "@t3tools/contracts";17import { GitCommandError, TextGenerationError } from "@t3tools/contracts";
18import { type GitManagerShape } from "./GitManager.ts";18import { type GitManagerShape } from "./GitManager.ts";
19import {19import {
20 GitHubCliError,
20 type GitHubCliShape,21 type GitHubCliShape,
21 type GitHubPullRequestSummary,22 type GitHubPullRequestSummary,
22 GitHubCli,23 GitHubCli,
apps/server/src/git/githubPullRequests.ts deleted-127
...@@ -1,127 +0,0 @@
1import { Cause, DateTime, Exit, Option, Result, Schema } from "effect";
2import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts";
3import { decodeJsonResult, formatSchemaError } from "@t3tools/shared/schemaJson";
4
5export interface NormalizedGitHubPullRequestRecord {
6 readonly number: number;
7 readonly title: string;
8 readonly url: string;
9 readonly baseRefName: string;
10 readonly headRefName: string;
11 readonly state: "open" | "closed" | "merged";
12 readonly updatedAt: Option.Option<DateTime.Utc>;
13 readonly isCrossRepository?: boolean;
14 readonly headRepositoryNameWithOwner?: string | null;
15 readonly headRepositoryOwnerLogin?: string | null;
16}
17
18const GitHubPullRequestSchema = Schema.Struct({
19 number: PositiveInt,
20 title: TrimmedNonEmptyString,
21 url: TrimmedNonEmptyString,
22 baseRefName: TrimmedNonEmptyString,
23 headRefName: TrimmedNonEmptyString,
24 state: Schema.optional(Schema.NullOr(Schema.String)),
25 mergedAt: Schema.optional(Schema.NullOr(Schema.String)),
26 updatedAt: Schema.optional(Schema.OptionFromNullOr(Schema.DateTimeUtcFromString)),
27 isCrossRepository: Schema.optional(Schema.Boolean),
28 headRepository: Schema.optional(
29 Schema.NullOr(
30 Schema.Struct({
31 nameWithOwner: Schema.String,
32 }),
33 ),
34 ),
35 headRepositoryOwner: Schema.optional(
36 Schema.NullOr(
37 Schema.Struct({
38 login: Schema.String,
39 }),
40 ),
41 ),
42});
43
44function trimOptionalString(value: string | null | undefined): string | null {
45 const trimmed = value?.trim() ?? "";
46 return trimmed.length > 0 ? trimmed : null;
47}
48
49function normalizeGitHubPullRequestState(input: {
50 state?: string | null | undefined;
51 mergedAt?: string | null | undefined;
52}): "open" | "closed" | "merged" {
53 const normalizedState = input.state?.trim().toUpperCase();
54 if (
55 (typeof input.mergedAt === "string" && input.mergedAt.trim().length > 0) ||
56 normalizedState === "MERGED"
57 ) {
58 return "merged";
59 }
60 if (normalizedState === "CLOSED") {
61 return "closed";
62 }
63 return "open";
64}
65
66function normalizeGitHubPullRequestRecord(
67 raw: Schema.Schema.Type<typeof GitHubPullRequestSchema>,
68): NormalizedGitHubPullRequestRecord {
69 const headRepositoryNameWithOwner = trimOptionalString(raw.headRepository?.nameWithOwner);
70 const headRepositoryOwnerLogin =
71 trimOptionalString(raw.headRepositoryOwner?.login) ??
72 (typeof headRepositoryNameWithOwner === "string" && headRepositoryNameWithOwner.includes("/")
73 ? (headRepositoryNameWithOwner.split("/")[0] ?? null)
74 : null);
75
76 return {
77 number: raw.number,
78 title: raw.title,
79 url: raw.url,
80 baseRefName: raw.baseRefName,
81 headRefName: raw.headRefName,
82 state: normalizeGitHubPullRequestState(raw),
83 updatedAt: raw.updatedAt ?? Option.none(),
84 ...(typeof raw.isCrossRepository === "boolean"
85 ? { isCrossRepository: raw.isCrossRepository }
86 : {}),
87 ...(headRepositoryNameWithOwner ? { headRepositoryNameWithOwner } : {}),
88 ...(headRepositoryOwnerLogin ? { headRepositoryOwnerLogin } : {}),
89 };
90}
91
92const decodeGitHubPullRequestList = decodeJsonResult(Schema.Array(Schema.Unknown));
93const decodeGitHubPullRequest = decodeJsonResult(GitHubPullRequestSchema);
94const decodeGitHubPullRequestEntry = Schema.decodeUnknownExit(GitHubPullRequestSchema);
95
96export const formatGitHubJsonDecodeError = formatSchemaError;
97
98export function decodeGitHubPullRequestListJson(
99 raw: string,
100): Result.Result<
101 ReadonlyArray<NormalizedGitHubPullRequestRecord>,
102 Cause.Cause<Schema.SchemaError>
103> {
104 const result = decodeGitHubPullRequestList(raw);
105 if (Result.isSuccess(result)) {
106 const pullRequests: NormalizedGitHubPullRequestRecord[] = [];
107 for (const entry of result.success) {
108 const decodedEntry = decodeGitHubPullRequestEntry(entry);
109 if (Exit.isFailure(decodedEntry)) {
110 continue;
111 }
112 pullRequests.push(normalizeGitHubPullRequestRecord(decodedEntry.value));
113 }
114 return Result.succeed(pullRequests);
115 }
116 return Result.fail(result.failure);
117}
118
119export function decodeGitHubPullRequestJson(
120 raw: string,
121): Result.Result<NormalizedGitHubPullRequestRecord, Cause.Cause<Schema.SchemaError>> {
122 const result = decodeGitHubPullRequest(raw);
123 if (Result.isSuccess(result)) {
124 return Result.succeed(normalizeGitHubPullRequestRecord(result.success));
125 }
126 return Result.fail(result.failure);
127}
apps/server/src/sourceControl/GitHubCli.ts+12-2
...@@ -1,16 +1,26 @@...@@ -1,16 +1,26 @@
1import { Context, Effect, Layer, Result, Schema, SchemaIssue } from "effect";1import { Context, Effect, Layer, Result, Schema, SchemaIssue } from "effect";
22
3import { GitHubCliError, TrimmedNonEmptyString, type VcsError } from "@t3tools/contracts";3import { TrimmedNonEmptyString, type VcsError } from "@t3tools/contracts";
44
5import { VcsProcess, type VcsProcessOutput } from "../vcs/VcsProcess.ts";5import { VcsProcess, type VcsProcessOutput } from "../vcs/VcsProcess.ts";
6import {6import {
7 decodeGitHubPullRequestJson,7 decodeGitHubPullRequestJson,
8 decodeGitHubPullRequestListJson,8 decodeGitHubPullRequestListJson,
9 formatGitHubJsonDecodeError,9 formatGitHubJsonDecodeError,
10} from "../git/githubPullRequests.ts";10} from "./gitHubPullRequests.ts";
1111
12const DEFAULT_TIMEOUT_MS = 30_000;12const DEFAULT_TIMEOUT_MS = 30_000;
1313
14export class GitHubCliError extends Schema.TaggedErrorClass<GitHubCliError>()("GitHubCliError", {
15 operation: Schema.String,
16 detail: Schema.String,
17 cause: Schema.optional(Schema.Defect),
18}) {
19 override get message(): string {
20 return `GitHub CLI failed in ${this.operation}: ${this.detail}`;
21 }
22}
23
14export interface GitHubPullRequestSummary {24export interface GitHubPullRequestSummary {
15 readonly number: number;25 readonly number: number;
16 readonly title: string;26 readonly title: string;
apps/server/src/sourceControl/GitHubSourceControlProvider.ts+2-3
...@@ -3,11 +3,10 @@ import {...@@ -3,11 +3,10 @@ import {
3 SourceControlProviderError,3 SourceControlProviderError,
4 type ChangeRequest,4 type ChangeRequest,
5 type ChangeRequestState,5 type ChangeRequestState,
6 type GitHubCliError,
7} from "@t3tools/contracts";6} from "@t3tools/contracts";
87
9import { GitHubCli, type GitHubPullRequestSummary } from "./GitHubCli.ts";8import { GitHubCli, type GitHubCliError, type GitHubPullRequestSummary } from "./GitHubCli.ts";
10import { decodeGitHubPullRequestListJson } from "../git/githubPullRequests.ts";9import { decodeGitHubPullRequestListJson } from "./gitHubPullRequests.ts";
11import { SourceControlProvider, type SourceControlProviderShape } from "./SourceControlProvider.ts";10import { SourceControlProvider, type SourceControlProviderShape } from "./SourceControlProvider.ts";
1211
13function providerError(operation: string, cause: GitHubCliError): SourceControlProviderError {12function providerError(operation: string, cause: GitHubCliError): SourceControlProviderError {
apps/server/src/sourceControl/gitHubPullRequests.ts created+127
...@@ -0,0 +1,127 @@
1import { Cause, DateTime, Exit, Option, Result, Schema } from "effect";
2import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts";
3import { decodeJsonResult, formatSchemaError } from "@t3tools/shared/schemaJson";
4
5export interface NormalizedGitHubPullRequestRecord {
6 readonly number: number;
7 readonly title: string;
8 readonly url: string;
9 readonly baseRefName: string;
10 readonly headRefName: string;
11 readonly state: "open" | "closed" | "merged";
12 readonly updatedAt: Option.Option<DateTime.Utc>;
13 readonly isCrossRepository?: boolean;
14 readonly headRepositoryNameWithOwner?: string | null;
15 readonly headRepositoryOwnerLogin?: string | null;
16}
17
18const GitHubPullRequestSchema = Schema.Struct({
19 number: PositiveInt,
20 title: TrimmedNonEmptyString,
21 url: TrimmedNonEmptyString,
22 baseRefName: TrimmedNonEmptyString,
23 headRefName: TrimmedNonEmptyString,
24 state: Schema.optional(Schema.NullOr(Schema.String)),
25 mergedAt: Schema.optional(Schema.NullOr(Schema.String)),
26 updatedAt: Schema.optional(Schema.OptionFromNullOr(Schema.DateTimeUtcFromString)),
27 isCrossRepository: Schema.optional(Schema.Boolean),
28 headRepository: Schema.optional(
29 Schema.NullOr(
30 Schema.Struct({
31 nameWithOwner: Schema.String,
32 }),
33 ),
34 ),
35 headRepositoryOwner: Schema.optional(
36 Schema.NullOr(
37 Schema.Struct({
38 login: Schema.String,
39 }),
40 ),
41 ),
42});
43
44function trimOptionalString(value: string | null | undefined): string | null {
45 const trimmed = value?.trim() ?? "";
46 return trimmed.length > 0 ? trimmed : null;
47}
48
49function normalizeGitHubPullRequestState(input: {
50 state?: string | null | undefined;
51 mergedAt?: string | null | undefined;
52}): "open" | "closed" | "merged" {
53 const normalizedState = input.state?.trim().toUpperCase();
54 if (
55 (typeof input.mergedAt === "string" && input.mergedAt.trim().length > 0) ||
56 normalizedState === "MERGED"
57 ) {
58 return "merged";
59 }
60 if (normalizedState === "CLOSED") {
61 return "closed";
62 }
63 return "open";
64}
65
66function normalizeGitHubPullRequestRecord(
67 raw: Schema.Schema.Type<typeof GitHubPullRequestSchema>,
68): NormalizedGitHubPullRequestRecord {
69 const headRepositoryNameWithOwner = trimOptionalString(raw.headRepository?.nameWithOwner);
70 const headRepositoryOwnerLogin =
71 trimOptionalString(raw.headRepositoryOwner?.login) ??
72 (typeof headRepositoryNameWithOwner === "string" && headRepositoryNameWithOwner.includes("/")
73 ? (headRepositoryNameWithOwner.split("/")[0] ?? null)
74 : null);
75
76 return {
77 number: raw.number,
78 title: raw.title,
79 url: raw.url,
80 baseRefName: raw.baseRefName,
81 headRefName: raw.headRefName,
82 state: normalizeGitHubPullRequestState(raw),
83 updatedAt: raw.updatedAt ?? Option.none(),
84 ...(typeof raw.isCrossRepository === "boolean"
85 ? { isCrossRepository: raw.isCrossRepository }
86 : {}),
87 ...(headRepositoryNameWithOwner ? { headRepositoryNameWithOwner } : {}),
88 ...(headRepositoryOwnerLogin ? { headRepositoryOwnerLogin } : {}),
89 };
90}
91
92const decodeGitHubPullRequestList = decodeJsonResult(Schema.Array(Schema.Unknown));
93const decodeGitHubPullRequest = decodeJsonResult(GitHubPullRequestSchema);
94const decodeGitHubPullRequestEntry = Schema.decodeUnknownExit(GitHubPullRequestSchema);
95
96export const formatGitHubJsonDecodeError = formatSchemaError;
97
98export function decodeGitHubPullRequestListJson(
99 raw: string,
100): Result.Result<
101 ReadonlyArray<NormalizedGitHubPullRequestRecord>,
102 Cause.Cause<Schema.SchemaError>
103> {
104 const result = decodeGitHubPullRequestList(raw);
105 if (Result.isSuccess(result)) {
106 const pullRequests: NormalizedGitHubPullRequestRecord[] = [];
107 for (const entry of result.success) {
108 const decodedEntry = decodeGitHubPullRequestEntry(entry);
109 if (Exit.isFailure(decodedEntry)) {
110 continue;
111 }
112 pullRequests.push(normalizeGitHubPullRequestRecord(decodedEntry.value));
113 }
114 return Result.succeed(pullRequests);
115 }
116 return Result.fail(result.failure);
117}
118
119export function decodeGitHubPullRequestJson(
120 raw: string,
121): Result.Result<NormalizedGitHubPullRequestRecord, Cause.Cause<Schema.SchemaError>> {
122 const result = decodeGitHubPullRequest(raw);
123 if (Result.isSuccess(result)) {
124 return Result.succeed(normalizeGitHubPullRequestRecord(result.success));
125 }
126 return Result.fail(result.failure);
127}
packages/contracts/src/git.ts-11
...@@ -328,16 +328,6 @@ export class GitCommandError extends Schema.TaggedErrorClass<GitCommandError>()(...@@ -328,16 +328,6 @@ export class GitCommandError extends Schema.TaggedErrorClass<GitCommandError>()(
328 }328 }
329}329}
330330
331export class GitHubCliError extends Schema.TaggedErrorClass<GitHubCliError>()("GitHubCliError", {
332 operation: Schema.String,
333 detail: Schema.String,
334 cause: Schema.optional(Schema.Defect),
335}) {
336 override get message(): string {
337 return `GitHub CLI failed in ${this.operation}: ${this.detail}`;
338 }
339}
340
341export class TextGenerationError extends Schema.TaggedErrorClass<TextGenerationError>()(331export class TextGenerationError extends Schema.TaggedErrorClass<TextGenerationError>()(
342 "TextGenerationError",332 "TextGenerationError",
343 {333 {
...@@ -364,7 +354,6 @@ export class GitManagerError extends Schema.TaggedErrorClass<GitManagerError>()(...@@ -364,7 +354,6 @@ export class GitManagerError extends Schema.TaggedErrorClass<GitManagerError>()(
364export const GitManagerServiceError = Schema.Union([354export const GitManagerServiceError = Schema.Union([
365 GitManagerError,355 GitManagerError,
366 GitCommandError,356 GitCommandError,
367 GitHubCliError,
368 SourceControlProviderError,357 SourceControlProviderError,
369 TextGenerationError,358 TextGenerationError,
370]);359]);