From 2d9d78d7f5b185a724f327b0e8729c6013b7e352 Mon Sep 17 00:00:00 2001 From: Abdullah Abdulkarim Date: Wed, 9 Sep 2026 10:03:31 -0400 Subject: [PATCH] fix(server): pin GitLab merges to current SHA --- .../pullRequest/GitLabPullRequestCli.test.ts | 43 +++++++++++++++++-- .../src/pullRequest/GitLabPullRequestCli.ts | 21 +++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/apps/server/src/pullRequest/GitLabPullRequestCli.test.ts b/apps/server/src/pullRequest/GitLabPullRequestCli.test.ts index 014d91a02740..042caae2d0c3 100644 --- a/apps/server/src/pullRequest/GitLabPullRequestCli.test.ts +++ b/apps/server/src/pullRequest/GitLabPullRequestCli.test.ts @@ -69,6 +69,7 @@ function notes(count: number, firstId: number): string { /** Who opened the merge request, and somebody already reviewing it. */ const author = { id: 1, username: "bilal" }; const reviewer = { id: 5, username: "octocat" }; +const diffRefs = { base_sha: "base", head_sha: "head", start_sha: "start" }; /** One merge request as `/merge_requests/:iid` answers with it. */ function mergeRequestJson(overrides: Record): string { @@ -390,7 +391,9 @@ layer("GitLabPullRequestCli.layer", (it) => { it.effect("merges immediately rather than leaving auto-merge armed", () => Effect.gen(function* () { - mockedExecute.mockReturnValueOnce(Effect.succeed(output(""))); + mockedExecute + .mockReturnValueOnce(Effect.succeed(output(mergeRequestJson({ diff_refs: diffRefs })))) + .mockReturnValueOnce(Effect.succeed(output(""))); const cli = yield* GitLabPullRequestCli.GitLabPullRequestCli; yield* cli.runMergeRequestAction({ @@ -401,7 +404,7 @@ layer("GitLabPullRequestCli.layer", (it) => { mergeMethod: "squash", }); - expect(argsOfCall(0)).toEqual([ + expect(argsOfCall(1)).toEqual([ "mr", "merge", "7", @@ -410,13 +413,17 @@ layer("GitLabPullRequestCli.layer", (it) => { "--auto-merge=false", "--yes", "--squash", + "--sha", + "head", ]); }), ); it.effect("arms auto-merge with the same strategy a merge would have used", () => Effect.gen(function* () { - mockedExecute.mockReturnValueOnce(Effect.succeed(output(""))); + mockedExecute + .mockReturnValueOnce(Effect.succeed(output(mergeRequestJson({ diff_refs: diffRefs })))) + .mockReturnValueOnce(Effect.succeed(output(""))); const cli = yield* GitLabPullRequestCli.GitLabPullRequestCli; yield* cli.runMergeRequestAction({ @@ -427,7 +434,7 @@ layer("GitLabPullRequestCli.layer", (it) => { mergeMethod: "squash", }); - expect(argsOfCall(0)).toEqual([ + expect(argsOfCall(1)).toEqual([ "mr", "merge", "7", @@ -436,10 +443,38 @@ layer("GitLabPullRequestCli.layer", (it) => { "--auto-merge=true", "--yes", "--squash", + "--sha", + "head", ]); }), ); + it.effect.each([ + { action: "merge", label: "merge" }, + { action: "enable-auto-merge", label: "arm auto-merge" }, + ] as const)("does not $label without the current diff revisions", ({ action }) => + Effect.gen(function* () { + mockedExecute.mockReturnValueOnce( + Effect.succeed(output(mergeRequestJson({ diff_refs: null }))), + ); + const cli = yield* GitLabPullRequestCli.GitLabPullRequestCli; + + const error = yield* Effect.flip( + cli.runMergeRequestAction({ + cwd: "/w", + repository: "acme/web", + number: 7, + action, + mergeMethod: "squash", + }), + ); + + assert.strictEqual(error._tag, "GitLabDiffRefsUnavailableError"); + assert.strictEqual(mockedExecute.mock.calls.length, 1); + expect(argsOfCall(0)).toEqual(["api", "projects/acme%2Fweb/merge_requests/7"]); + }), + ); + it.effect("cancels an armed auto-merge through the API glab has no flag for", () => Effect.gen(function* () { mockedExecute.mockReturnValueOnce(Effect.succeed(output("{}"))); diff --git a/apps/server/src/pullRequest/GitLabPullRequestCli.ts b/apps/server/src/pullRequest/GitLabPullRequestCli.ts index 60bf2c55742e..9c184961c194 100644 --- a/apps/server/src/pullRequest/GitLabPullRequestCli.ts +++ b/apps/server/src/pullRequest/GitLabPullRequestCli.ts @@ -1273,6 +1273,27 @@ export const make = Effect.gen(function* () { }).pipe(Effect.asVoid); } const [subcommand, ...flags] = actionArgs(input.action, input.mergeMethod); + // GitLab projects can require the current source SHA for every merge attempt. + if (input.action === "merge" || input.action === "enable-auto-merge") { + return getDiffRefs(input).pipe( + Effect.flatMap((refs) => + gitlab.execute({ + cwd: input.cwd, + args: [ + "mr", + subcommand!, + String(input.number), + "--repo", + input.repository, + ...flags, + "--sha", + refs.headSha, + ], + }), + ), + Effect.asVoid, + ); + } return gitlab .execute({ cwd: input.cwd,