authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 16:38:56-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 16:38:56-07:00
log5edf7c56bafeeaf4d5af1a63f010dde45e6652dd
tree50c9a39197c22720b57ab23510ecc7165545cf85
parent90dc76b11eac811e8bc89cad3f60bb5c7b56514f
signature Signed by PGP key B5690EEEBB952194

[codex] Preserve PR materialization failure chains (#3443)

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

3 files changed, 112 insertions(+), 13 deletions(-)

apps/server/src/git/GitManager.test.ts+59
...@@ -2757,6 +2757,65 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -2757,6 +2757,65 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
2757 }),2757 }),
2758 );2758 );
27592759
2760 it.effect("preserves both branch materialization failures when the fallback also fails", () =>
2761 Effect.gen(function* () {
2762 const repoDir = yield* makeTempDir("t3code-git-manager-");
2763 yield* initRepo(repoDir);
2764 const originDir = yield* createBareRemote();
2765 yield* runGit(repoDir, ["remote", "add", "origin", originDir]);
2766 yield* runGit(repoDir, ["push", "-u", "origin", "main"]);
2767
2768 const missingForkDir = NodePath.join(repoDir, "missing-fork.git");
2769 const { manager } = yield* makeManager({
2770 ghScenario: {
2771 pullRequest: {
2772 number: 93,
2773 title: "Missing fork branch",
2774 url: "https://github.com/pingdotgg/codething-mvp/pull/93",
2775 baseRefName: "main",
2776 headRefName: "feature/missing-fork-branch",
2777 state: "open",
2778 isCrossRepository: true,
2779 headRepositoryNameWithOwner: "octocat/codething-mvp",
2780 headRepositoryOwnerLogin: "octocat",
2781 },
2782 repositoryCloneUrls: {
2783 "octocat/codething-mvp": {
2784 url: missingForkDir,
2785 sshUrl: missingForkDir,
2786 },
2787 },
2788 },
2789 });
2790
2791 const error = yield* preparePullRequestThread(manager, {
2792 cwd: repoDir,
2793 reference: "93",
2794 mode: "worktree",
2795 }).pipe(Effect.flip);
2796
2797 if (error._tag !== "GitPullRequestMaterializationError") {
2798 return yield* Effect.die(error);
2799 }
2800 expect(error).toMatchObject({
2801 cwd: repoDir,
2802 pullRequestNumber: 93,
2803 headRepository: "octocat/codething-mvp",
2804 headBranch: "feature/missing-fork-branch",
2805 localBranch: "t3code/pr-93/feature/missing-fork-branch",
2806 });
2807 if (!(error.cause instanceof AggregateError)) {
2808 return yield* Effect.die(error.cause);
2809 }
2810 expect(error.cause.errors).toHaveLength(2);
2811 expect(error.cause.errors).toEqual([
2812 expect.objectContaining({ _tag: "GitCommandError" }),
2813 expect.objectContaining({ _tag: "GitCommandError" }),
2814 ]);
2815 expect(error.cause.cause).toBe(error.cause.errors[0]);
2816 }),
2817 );
2818
2760 it.effect("launches setup only when creating a new PR worktree", () =>2819 it.effect("launches setup only when creating a new PR worktree", () =>
2761 Effect.gen(function* () {2820 Effect.gen(function* () {
2762 const repoDir = yield* makeTempDir("t3code-git-manager-");2821 const repoDir = yield* makeTempDir("t3code-git-manager-");
apps/server/src/git/GitManager.ts+36-13
...@@ -41,7 +41,7 @@ import {...@@ -41,7 +41,7 @@ import {
41 type ChangeRequestTerminology,41 type ChangeRequestTerminology,
42} from "@t3tools/shared/sourceControl";42} from "@t3tools/shared/sourceControl";
4343
44import { GitManagerError } from "@t3tools/contracts";44import { GitManagerError, GitPullRequestMaterializationError } from "@t3tools/contracts";
45import * as TextGeneration from "../textGeneration/TextGeneration.ts";45import * as TextGeneration from "../textGeneration/TextGeneration.ts";
46import * as ProjectSetupScriptRunner from "../project/ProjectSetupScriptRunner.ts";46import * as ProjectSetupScriptRunner from "../project/ProjectSetupScriptRunner.ts";
47import { extractBranchNameFromRemoteRef } from "./remoteRefs.ts";47import { extractBranchNameFromRemoteRef } from "./remoteRefs.ts";
...@@ -631,9 +631,12 @@ export const make = Effect.gen(function* () {...@@ -631,9 +631,12 @@ export const make = Effect.gen(function* () {
631 ) =>631 ) =>
632 configurePullRequestHeadUpstreamBase(cwd, pullRequest, localBranch).pipe(632 configurePullRequestHeadUpstreamBase(cwd, pullRequest, localBranch).pipe(
633 Effect.catch((error) =>633 Effect.catch((error) =>
634 Effect.logWarning(634 Effect.logWarning("GitManager.configurePullRequestHeadUpstream failed", {
635 `GitManager.configurePullRequestHeadUpstream: failed to configure upstream for ${localBranch} -> ${pullRequest.headBranch} in ${cwd}: ${error.message}`,635 cwd,
636 ).pipe(Effect.asVoid),636 localBranch,
637 headBranch: pullRequest.headBranch,
638 cause: error,
639 }).pipe(Effect.asVoid),
637 ),640 ),
638 );641 );
639642
...@@ -691,12 +694,30 @@ export const make = Effect.gen(function* () {...@@ -691,12 +694,30 @@ export const make = Effect.gen(function* () {
691 localBranch = pullRequest.headBranch,694 localBranch = pullRequest.headBranch,
692 ) =>695 ) =>
693 materializePullRequestHeadBranchBase(cwd, pullRequest, localBranch).pipe(696 materializePullRequestHeadBranchBase(cwd, pullRequest, localBranch).pipe(
694 Effect.catch(() =>697 Effect.catch((primaryCause) =>
695 gitCore.fetchPullRequestBranch({698 gitCore
696 cwd,699 .fetchPullRequestBranch({
697 prNumber: pullRequest.number,700 cwd,
698 branch: localBranch,701 prNumber: pullRequest.number,
699 }),702 branch: localBranch,
703 })
704 .pipe(
705 Effect.mapError(
706 (fallbackCause) =>
707 new GitPullRequestMaterializationError({
708 cwd,
709 pullRequestNumber: pullRequest.number,
710 headRepository: resolveHeadRepositoryNameWithOwner(pullRequest),
711 headBranch: pullRequest.headBranch,
712 localBranch,
713 cause: new AggregateError(
714 [primaryCause, fallbackCause],
715 `Repository-head and pull-request-ref fetches both failed for pull request #${pullRequest.number}.`,
716 { cause: primaryCause },
717 ),
718 }),
719 ),
720 ),
700 ),721 ),
701 );722 );
702 const fileSystem = yield* FileSystem.FileSystem;723 const fileSystem = yield* FileSystem.FileSystem;
...@@ -1452,9 +1473,11 @@ export const make = Effect.gen(function* () {...@@ -1452,9 +1473,11 @@ export const make = Effect.gen(function* () {
1452 })1473 })
1453 .pipe(1474 .pipe(
1454 Effect.catch((error) =>1475 Effect.catch((error) =>
1455 Effect.logWarning(1476 Effect.logWarning("GitManager.preparePullRequestThread setup script failed", {
1456 `GitManager.preparePullRequestThread: failed to launch worktree setup script for thread ${input.threadId} in ${worktreePath}: ${error.message}`,1477 threadId: input.threadId,
1457 ).pipe(Effect.asVoid),1478 worktreePath,
1479 cause: error,
1480 }).pipe(Effect.asVoid),
1458 ),1481 ),
1459 );1482 );
1460 };1483 };
packages/contracts/src/git.ts+17
...@@ -361,8 +361,25 @@ export class GitManagerError extends Schema.TaggedErrorClass<GitManagerError>()(...@@ -361,8 +361,25 @@ export class GitManagerError extends Schema.TaggedErrorClass<GitManagerError>()(
361 }361 }
362}362}
363363
364export class GitPullRequestMaterializationError extends Schema.TaggedErrorClass<GitPullRequestMaterializationError>()(
365 "GitPullRequestMaterializationError",
366 {
367 cwd: TrimmedNonEmptyStringSchema,
368 pullRequestNumber: PositiveInt,
369 headRepository: Schema.NullOr(TrimmedNonEmptyStringSchema),
370 headBranch: TrimmedNonEmptyStringSchema,
371 localBranch: TrimmedNonEmptyStringSchema,
372 cause: Schema.Defect(),
373 },
374) {
375 override get message(): string {
376 return `Failed to materialize pull request #${this.pullRequestNumber} branch ${this.headBranch} as ${this.localBranch}.`;
377 }
378}
379
364export const GitManagerServiceError = Schema.Union([380export const GitManagerServiceError = Schema.Union([
365 GitManagerError,381 GitManagerError,
382 GitPullRequestMaterializationError,
366 GitCommandError,383 GitCommandError,
367 SourceControlProviderError,384 SourceControlProviderError,
368 TextGenerationError,385 TextGenerationError,