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 {
1414 ThreadId,
1515} from "@t3tools/contracts";
1616
17import { GitCommandError, GitHubCliError, TextGenerationError } from "@t3tools/contracts";
17import { GitCommandError, TextGenerationError } from "@t3tools/contracts";
1818import { type GitManagerShape } from "./GitManager.ts";
1919import {
20 GitHubCliError,
2021 type GitHubCliShape,
2122 type GitHubPullRequestSummary,
2223 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 @@
11import { 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
55import { VcsProcess, type VcsProcessOutput } from "../vcs/VcsProcess.ts";
66import {
77 decodeGitHubPullRequestJson,
88 decodeGitHubPullRequestListJson,
99 formatGitHubJsonDecodeError,
10} from "../git/githubPullRequests.ts";
10} from "./gitHubPullRequests.ts";
1111
1212const 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
1424export interface GitHubPullRequestSummary {
1525 readonly number: number;
1626 readonly title: string;
apps/server/src/sourceControl/GitHubSourceControlProvider.ts+2-3
......@@ -3,11 +3,10 @@ import {
33 SourceControlProviderError,
44 type ChangeRequest,
55 type ChangeRequestState,
6 type GitHubCliError,
76} from "@t3tools/contracts";
87
9import { GitHubCli, type GitHubPullRequestSummary } from "./GitHubCli.ts";
10import { decodeGitHubPullRequestListJson } from "../git/githubPullRequests.ts";
8import { GitHubCli, type GitHubCliError, type GitHubPullRequestSummary } from "./GitHubCli.ts";
9import { decodeGitHubPullRequestListJson } from "./gitHubPullRequests.ts";
1110import { SourceControlProvider, type SourceControlProviderShape } from "./SourceControlProvider.ts";
1211
1312function 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>()(
328328 }
329329}
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
341331export class TextGenerationError extends Schema.TaggedErrorClass<TextGenerationError>()(
342332 "TextGenerationError",
343333 {
......@@ -364,7 +354,6 @@ export class GitManagerError extends Schema.TaggedErrorClass<GitManagerError>()(
364354export const GitManagerServiceError = Schema.Union([
365355 GitManagerError,
366356 GitCommandError,
367 GitHubCliError,
368357 SourceControlProviderError,
369358 TextGenerationError,
370359]);