From 2cf7d1a2feb923e2d25d129b1b4c47139ca39f4f Mon Sep 17 00:00:00 2001 From: sidreyas Date: Sat, 7 Nov 2026 04:10:07 +0530 Subject: [PATCH] fix(github): explain rejected pull request base branches --- .../GitHubSourceControlProvider.test.ts | 48 ++++++++++++++++++ .../src/server/GitHubSourceControlProvider.ts | 49 +++++++++++++------ 2 files changed, 82 insertions(+), 15 deletions(-) diff --git a/packages/source-control-github/src/server/GitHubSourceControlProvider.test.ts b/packages/source-control-github/src/server/GitHubSourceControlProvider.test.ts index a5a08fcfc2cc4..82853504c5a2f 100644 --- a/packages/source-control-github/src/server/GitHubSourceControlProvider.test.ts +++ b/packages/source-control-github/src/server/GitHubSourceControlProvider.test.ts @@ -552,6 +552,54 @@ describe("GitHubSourceControlProvider writes", () => { }).pipe(Effect.provide(Layer.merge(layer, NodeServices.layer)), Effect.scoped); }); + it.effect.each([ + { host: "github.com", status: 422, field: "base", rejectedBase: true }, + { host: "code.example.test", status: 422, field: "base", rejectedBase: true }, + { host: "github.com", status: 422, field: "head", rejectedBase: false }, + { host: "github.com", status: 500, field: "base", rejectedBase: false }, + ])( + "explains a rejected PR base without replacing other API failures: $host $status $field", + ({ host, status, field, rejectedBase }) => { + const cause = new GitHubRequestPolicy.GitHubApiResponseError({ + host, + operation: "createPullRequest", + status, + githubErrors: ["Validation Failed", `${field} invalid`], + }); + const { layer } = harness({ + remotes: remotesOutput( + ["origin", `git@${host}:me/web.git`], + ["upstream", `git@${host}:acme/web.git`], + ), + api: { rest: () => Effect.fail(cause) }, + }); + return Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const bodyFile = yield* fs.makeTempFileScoped({ suffix: ".md" }); + yield* fs.writeFileString(bodyFile, "Body"); + const gh = yield* GitHubSourceControlProvider.make; + const error = yield* gh + .createChangeRequest({ + cwd: "/repo", + context: githubContext(host), + baseRefName: "stack/parent", + headSelector: "me:child", + title: "Title", + bodyFile, + }) + .pipe(Effect.flip); + assert.strictEqual(error.operation, "createChangeRequest"); + assert.strictEqual(error.cause, cause); + assert.strictEqual( + error.detail, + rejectedBase + ? `GitHub rejected base branch "stack/parent" for ${host}/acme/web. Push the intended base branch to that repository, or choose an existing base branch and retry.` + : cause.message, + ); + }).pipe(Effect.provide(layer), Effect.scoped); + }, + ); + it.effect("creates a repository under an organization the viewer is not", () => { const requests: Array = []; const { layer } = harness({ diff --git a/packages/source-control-github/src/server/GitHubSourceControlProvider.ts b/packages/source-control-github/src/server/GitHubSourceControlProvider.ts index 4abb3d8d6669f..a67854c254c03 100644 --- a/packages/source-control-github/src/server/GitHubSourceControlProvider.ts +++ b/packages/source-control-github/src/server/GitHubSourceControlProvider.ts @@ -985,21 +985,40 @@ export const make = Effect.gen(function* () { failure("The pull request description could not be read.", cause), ), ); - yield* rest({ - host: locator.host, - operation: "createPullRequest", - method: "POST", - allowReserve: true, - path: `repos/${encodeURIComponent(locator.owner)}/${encodeURIComponent(locator.name)}/pulls`, - // Explicitly allow maintainer edits to match gh; the API default is undocumented. - body: { - base: input.baseRefName, - head: input.headSelector, - title: input.title, - body, - maintainer_can_modify: true, - }, - }); + yield* api + .rest({ + host: locator.host, + operation: "createPullRequest", + method: "POST", + allowReserve: true, + path: `repos/${encodeURIComponent(locator.owner)}/${encodeURIComponent(locator.name)}/pulls`, + // Explicitly allow maintainer edits to match gh; the API default is undocumented. + body: { + base: input.baseRefName, + head: input.headSelector, + title: input.title, + body, + maintainer_can_modify: true, + }, + }) + .pipe( + Effect.mapError((error) => { + if ( + error._tag === "GitHubApiResponseError" && + error.status === 422 && + error.githubErrors?.includes("base invalid") + ) { + const base = SourceControlProvider.transportSafeSourceControlErrorValue( + input.baseRefName, + ); + return failure( + `GitHub rejected base branch ${JSON.stringify(base)} for ${locator.host}/${locator.owner}/${locator.name}. Push the intended base branch to that repository, or choose an existing base branch and retry.`, + error, + ); + } + return fromGitHubApiError(error); + }), + ); }, (effect, input) => effect.pipe(