authorgravatar for 56691628+Mnigos@users.noreply.github.comIgor Makowski <56691628+Mnigos@users.noreply.github.com> 2026-09-17 03:42:59+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-16 18:42:59-07:00
log8130a9f13a1ef506f4393423ded21d71f3c9b5e4
treee5f760c3da0f2058893e57eb8343afde33023b93
parent4abb07eac3d47543b5804e8039633ac8ad7f67d2
signature Signed by PGP key B5690EEEBB952194

fix(server): flush checkpoint objects and refs before publishing them (#10944)


2 files changed, 87 insertions(+), 4 deletions(-)

apps/server/src/vcs/GitVcsDriver.test.ts+64
......@@ -623,3 +623,67 @@ it.effect("GitVcsDriver forwards execute env to the VCS process", () => {
623623 ),
624624 );
625625});
626
627it.effect("GitVcsDriver flushes checkpoint objects and refs to disk before publishing them", () => {
628 const observedArgs: ReadonlyArray<string>[] = [];
629
630 return Effect.gen(function* () {
631 const driver = yield* GitVcsDriver.makeVcsDriverShape();
632
633 yield* driver.checkpoints.captureCheckpoint({
634 cwd: "/repo",
635 checkpointRef: CheckpointRef.make("refs/t3/checkpoints/thread/turn/1"),
636 });
637
638 const writeCommands = ["add", "write-tree", "commit-tree", "update-ref"];
639 const writes = observedArgs.filter((args) =>
640 writeCommands.some((command) => args.includes(command)),
641 );
642 assert.strictEqual(writes.length, 4);
643 for (const args of writes) {
644 const command = args.findIndex((arg) => writeCommands.includes(arg));
645 for (const setting of ["core.fsync=objects,reference", "core.fsyncMethod=fsync"]) {
646 const index = args.indexOf(setting);
647 assert.strictEqual(args[index - 1], "-c", args.join(" "));
648 assert.isBelow(index, command);
649 }
650 }
651 assert.deepStrictEqual(observedArgs.at(-1), [
652 "-C",
653 "/repo",
654 "-c",
655 "core.fsync=objects,reference",
656 "-c",
657 "core.fsyncMethod=fsync",
658 "update-ref",
659 "refs/t3/checkpoints/thread/turn/1",
660 "commit0000",
661 ]);
662 }).pipe(
663 Effect.provide(
664 Layer.mergeAll(
665 NodeServices.layer,
666 Layer.mock(VcsProcess.VcsProcess)({
667 run: (input) =>
668 Effect.sync(() => {
669 observedArgs.push(input.args);
670 const stdout = input.args.includes("write-tree")
671 ? "tree0000\n"
672 : input.args.includes("commit-tree")
673 ? "commit0000\n"
674 : input.args.includes("--git-common-dir")
675 ? ".git\n"
676 : "";
677 return {
678 exitCode: ChildProcessSpawner.ExitCode(0),
679 stdout,
680 stderr: "",
681 stdoutTruncated: false,
682 stderrTruncated: false,
683 };
684 }),
685 }),
686 ),
687 ),
688 );
689});
apps/server/src/vcs/GitVcsDriver.ts+23-4
......@@ -750,6 +750,17 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (
750750 return path.isAbsolute(gitCommonDir) ? gitCommonDir : path.resolve(cwd, gitCommonDir);
751751 });
752752
753 // Git renames loose objects and refs into place without fsync by default, so
754 // an unclean restart can leave 0-byte files under refs/t3/** that break every
755 // later fetch and push. Checkpoint writes flush before they are published;
756 // macOS defaults to writeout-only, which does not reach the disk either.
757 const durableWrite = [
758 "-c",
759 "core.fsync=objects,reference",
760 "-c",
761 "core.fsyncMethod=fsync",
762 ] as const;
763
753764 const checkpoints: VcsDriver.VcsCheckpointOps = {
754765 captureCheckpoint: Effect.fn("GitVcsDriver.checkpoints.captureCheckpoint")(function* (input) {
755766 const operation = "GitVcsDriver.checkpoints.captureCheckpoint";
......@@ -907,14 +918,22 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (
907918 operation,
908919 cwd: input.cwd,
909920 // Preserve absent skipped entries, but capture present nonignored files outside the cone.
910 args: [...indexConfig, "add", ...(sparseCheckout ? ["--sparse"] : []), "-A", "--", "."],
921 args: [
922 ...indexConfig,
923 ...durableWrite,
924 "add",
925 ...(sparseCheckout ? ["--sparse"] : []),
926 "-A",
927 "--",
928 ".",
929 ],
911930 env: commitEnv,
912931 });
913932
914933 const writeTreeResult = yield* execute({
915934 operation,
916935 cwd: input.cwd,
917 args: [...indexConfig, "write-tree"],
936 args: [...indexConfig, ...durableWrite, "write-tree"],
918937 env: commitEnv,
919938 });
920939 const treeOid = writeTreeResult.stdout.trim();
......@@ -932,7 +951,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (
932951 const commitTreeResult = yield* execute({
933952 operation,
934953 cwd: input.cwd,
935 args: ["commit-tree", treeOid, "-m", message],
954 args: [...durableWrite, "commit-tree", treeOid, "-m", message],
936955 env: commitEnv,
937956 });
938957 const commitOid = commitTreeResult.stdout.trim();
......@@ -949,7 +968,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (
949968 yield* execute({
950969 operation,
951970 cwd: input.cwd,
952 args: ["update-ref", input.checkpointRef, commitOid],
971 args: [...durableWrite, "update-ref", input.checkpointRef, commitOid],
953972 });
954973 }).pipe(Effect.ensuring(cleanupTempIndex));
955974 }),