From 2c6c9368ffc081de8406367fec86f6119971eecb Mon Sep 17 00:00:00 2001 From: Tejas Parmar Date: Thu, 22 Oct 2026 15:36:44 +0000 Subject: [PATCH 1/3] fix(gitlab): keep pull request actions on the selected host --- .../src/server/GitLabCli.ts | 3 + .../src/server/GitLabPullRequestCli.ts | 77 +++++++++++++++++-- .../server/GitLabPullRequestProvider.test.ts | 74 ++++++++++++++++++ .../src/server/GitLabPullRequestProvider.ts | 22 +++--- 4 files changed, 162 insertions(+), 14 deletions(-) diff --git a/packages/source-control-gitlab/src/server/GitLabCli.ts b/packages/source-control-gitlab/src/server/GitLabCli.ts index aea251144e8e0..e5e14b1e4124e 100644 --- a/packages/source-control-gitlab/src/server/GitLabCli.ts +++ b/packages/source-control-gitlab/src/server/GitLabCli.ts @@ -266,6 +266,7 @@ export class GitLabCli extends Context.Service< { readonly execute: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly args: ReadonlyArray; readonly timeoutMs?: number; /** Piped to the child's stdin, for payloads that must never appear in argv. */ @@ -423,6 +424,8 @@ export const make = Effect.gen(function* () { command: "glab", args: input.args, cwd: input.cwd, + // PR identity owns the host; glab may prefer a different checkout remote. + ...(input.host === undefined ? {} : { env: { GITLAB_HOST: input.host } }), timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS, ...(input.stdin === undefined ? {} : { stdin: input.stdin }), ...(input.maxOutputBytes === undefined ? {} : { maxOutputBytes: input.maxOutputBytes }), diff --git a/packages/source-control-gitlab/src/server/GitLabPullRequestCli.ts b/packages/source-control-gitlab/src/server/GitLabPullRequestCli.ts index d66992141f908..e10d5e69e5cd4 100644 --- a/packages/source-control-gitlab/src/server/GitLabPullRequestCli.ts +++ b/packages/source-control-gitlab/src/server/GitLabPullRequestCli.ts @@ -212,10 +212,12 @@ export class GitLabPullRequestCli extends Context.Service< { readonly getViewerUsername: (input: { readonly cwd: string; + readonly host?: string | undefined; }) => Effect.Effect; readonly listMergeRequests: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly state: PullRequestListState; readonly involvement: PullRequestInvolvement; @@ -227,12 +229,14 @@ export class GitLabPullRequestCli extends Context.Service< readonly getMergeRequestDetail: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; }) => Effect.Effect; readonly listNotes: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; }) => Effect.Effect< @@ -242,12 +246,14 @@ export class GitLabPullRequestCli extends Context.Service< readonly listCommits: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; }) => Effect.Effect, GitLabPullRequestCliError>; readonly getMergeRequestDiff: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly cursor?: string | undefined; @@ -256,6 +262,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly getMergeRequestDiffFileContents: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly commit?: string | undefined; @@ -269,11 +276,13 @@ export class GitLabPullRequestCli extends Context.Service< readonly getProjectMergeCapabilities: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; }) => Effect.Effect; readonly getFileRevisions: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly paths: ReadonlyArray; @@ -281,12 +290,14 @@ export class GitLabPullRequestCli extends Context.Service< readonly listReviewerCandidates: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; }) => Effect.Effect; readonly setReviewerRequest: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly reviewers: ReadonlyArray<{ readonly id: string }>; @@ -295,6 +306,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly runMergeRequestAction: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly action: PullRequestAction; @@ -303,6 +315,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly updateMergeRequest: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly title?: string | undefined; @@ -311,6 +324,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly commentOnMergeRequest: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly body: string; @@ -318,6 +332,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly updateNote: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly noteId: string; @@ -326,6 +341,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly listDiscussions: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; }) => Effect.Effect< @@ -335,6 +351,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly submitReview: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly verdict: PullRequestReviewVerdict; @@ -344,6 +361,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly replyToDiscussion: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly discussionId: string; @@ -352,6 +370,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly setDiscussionResolution: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly discussionId: string; @@ -360,6 +379,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly listReactions: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; }) => Effect.Effect< @@ -372,6 +392,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly setReaction: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly noteId?: string | undefined; @@ -481,6 +502,7 @@ export const make = Effect.gen(function* () { const api = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly path: string; readonly method?: string; readonly stdin?: string; @@ -489,6 +511,7 @@ export const make = Effect.gen(function* () { }) => gitlab.execute({ cwd: input.cwd, + host: input.host, args: [ "api", input.path, @@ -507,6 +530,7 @@ export const make = Effect.gen(function* () { const listPage = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly state: PullRequestListState; readonly involvement: PullRequestInvolvement; @@ -531,6 +555,7 @@ export const make = Effect.gen(function* () { const lastPage = Math.floor((delivered + input.limit) / perPage) + 1; return api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests?${query([ ["state", stateParam(input.state)], ...involvementParams(input), @@ -611,6 +636,7 @@ export const make = Effect.gen(function* () { const diffPage = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly page: number; @@ -618,6 +644,7 @@ export const make = Effect.gen(function* () { }): Effect.Effect => api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/${ input.commit === undefined ? `merge_requests/${input.number}/diffs` @@ -643,6 +670,7 @@ export const make = Effect.gen(function* () { // GitLab before 15.7 has no /diffs endpoint. /changes returns one complete slice. return api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}/changes?access_raw_diffs=true`, maxOutputBytes: DIFF_MAX_OUTPUT_BYTES, timeoutMs: DIFF_TIMEOUT_MS, @@ -694,6 +722,7 @@ export const make = Effect.gen(function* () { const notesPage = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly page: number; @@ -704,6 +733,7 @@ export const make = Effect.gen(function* () { > => api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}/notes?${query( [ ["per_page", String(MAX_PAGE_SIZE)], @@ -737,6 +767,7 @@ export const make = Effect.gen(function* () { const discussionsPage = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly page: number; @@ -747,6 +778,7 @@ export const make = Effect.gen(function* () { > => api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}/discussions?${query( [ ["per_page", String(MAX_PAGE_SIZE)], @@ -780,11 +812,13 @@ export const make = Effect.gen(function* () { const getDiffRefs = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; }): Effect.Effect => api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}`, }).pipe( Effect.flatMap((result): Effect.Effect => { @@ -813,12 +847,14 @@ export const make = Effect.gen(function* () { const getCommitDiffRefs = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly commit: string; readonly allowRoot: boolean; }): Effect.Effect => api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/repository/commits/${input.commit}`, }).pipe( Effect.flatMap((result): Effect.Effect => { @@ -853,11 +889,13 @@ export const make = Effect.gen(function* () { const mergeRequestDetail = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; }): Effect.Effect => api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}?${query([ ["include_diverged_commits_count", "true"], ])}`, @@ -879,10 +917,12 @@ export const make = Effect.gen(function* () { const projectUsers = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; }): Effect.Effect => api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/users?${query([ ["per_page", String(MAX_PAGE_SIZE)], ])}`, @@ -915,6 +955,7 @@ export const make = Effect.gen(function* () { const awardsPage = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly cursor: string | null; @@ -932,6 +973,7 @@ export const make = Effect.gen(function* () { > => api({ cwd: input.cwd, + host: input.host, path: "graphql", method: "POST", stdin: JSON.stringify({ @@ -980,12 +1022,14 @@ export const make = Effect.gen(function* () { const blobsAt = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly ref: string; readonly paths: ReadonlyArray; }): Effect.Effect | null, GitLabPullRequestCliError> => api({ cwd: input.cwd, + host: input.host, path: "graphql", method: "POST", stdin: JSON.stringify({ @@ -1012,6 +1056,7 @@ export const make = Effect.gen(function* () { const fileRevisions = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly paths: ReadonlyArray; @@ -1051,8 +1096,8 @@ export const make = Effect.gen(function* () { }), ); - const viewerUsername = (input: { readonly cwd: string }) => - api({ cwd: input.cwd, path: "user" }).pipe( + const viewerUsername = (input: { readonly cwd: string; readonly host?: string | undefined }) => + api({ cwd: input.cwd, host: input.host, path: "user" }).pipe( Effect.flatMap((result): Effect.Effect => { const decoded = decodeViewerJson(result.stdout.trim()); if (!Result.isSuccess(decoded)) { @@ -1093,6 +1138,7 @@ export const make = Effect.gen(function* () { if (input.reacted) { yield* api({ cwd: input.cwd, + host: input.host, path: `${subject}?${query([["name", gitLabAwardName(input.content)]])}`, method: "POST", }); @@ -1100,8 +1146,8 @@ export const make = Effect.gen(function* () { } // GitLab deletes an award by its id and takes no emoji name there, so the reader's own // award of that name is looked up first. - const viewer = yield* viewerUsername({ cwd: input.cwd }); - const listed = yield* api({ cwd: input.cwd, path: subject }); + const viewer = yield* viewerUsername(input); + const listed = yield* api({ cwd: input.cwd, host: input.host, path: subject }); const own = decodeOwnAwardIdJson(listed.stdout.trim(), { content: input.content, viewer, @@ -1117,6 +1163,7 @@ export const make = Effect.gen(function* () { if (own.success === null) return; yield* api({ cwd: input.cwd, + host: input.host, path: `${subject}/${own.success}`, method: "DELETE", }); @@ -1125,6 +1172,7 @@ export const make = Effect.gen(function* () { listCommits: (input) => api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}/commits?${query( [ ["per_page", String(COMMIT_PAGE_SIZE)], @@ -1153,6 +1201,7 @@ export const make = Effect.gen(function* () { } const target = { cwd: input.cwd, + host: input.host, repository: input.repository, number: input.number, ...(input.commit === undefined ? {} : { commit: input.commit }), @@ -1176,6 +1225,7 @@ export const make = Effect.gen(function* () { ? getDiffRefs(input) : getCommitDiffRefs({ cwd: input.cwd, + host: input.host, repository: input.repository, commit: input.commit, allowRoot: input.changeType === "new", @@ -1184,6 +1234,7 @@ export const make = Effect.gen(function* () { const readFile = (revision: string, filePath: string) => api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/repository/files/${encodeURIComponent( filePath, )}/raw?ref=${encodeURIComponent(revision)}`, @@ -1221,6 +1272,7 @@ export const make = Effect.gen(function* () { getProjectMergeCapabilities: (input) => api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}?license=false`, }).pipe( Effect.flatMap((result) => { @@ -1271,6 +1323,7 @@ export const make = Effect.gen(function* () { } return api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}`, method: "PUT", stdin: JSON.stringify({ reviewer_ids: [...ids] }), @@ -1287,6 +1340,7 @@ export const make = Effect.gen(function* () { if (input.action === "disable-auto-merge") { return yield* api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}/cancel_merge_when_pipeline_succeeds`, method: "POST", }).pipe(Effect.asVoid); @@ -1294,6 +1348,7 @@ export const make = Effect.gen(function* () { const executeMr = (subcommand: string, ...flags: ReadonlyArray) => gitlab.execute({ cwd: input.cwd, + host: input.host, args: ["mr", subcommand, String(input.number), "--repo", input.repository, ...flags], }); const [subcommand, ...flags] = actionArgs(input.action, input.mergeMethod); @@ -1312,6 +1367,7 @@ export const make = Effect.gen(function* () { updateMergeRequest: (input) => api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}`, method: "PUT", // Only the fields the caller asked to change: GitLab leaves out what it is not sent, and @@ -1326,6 +1382,7 @@ export const make = Effect.gen(function* () { commentOnMergeRequest: (input) => api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}/notes`, method: "POST", // A JSON body rather than a `--raw-field`: glab coerces a field that reads as a @@ -1336,6 +1393,7 @@ export const make = Effect.gen(function* () { updateNote: (input) => api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}/notes/${encodeURIComponent( input.noteId, )}`, @@ -1359,6 +1417,7 @@ export const make = Effect.gen(function* () { (comment) => api({ cwd: input.cwd, + host: input.host, path: `${mergeRequest}/discussions`, method: "POST", stdin: JSON.stringify({ @@ -1383,6 +1442,7 @@ export const make = Effect.gen(function* () { if (input.body.trim().length > 0) { yield* api({ cwd: input.cwd, + host: input.host, path: `${mergeRequest}/notes`, method: "POST", // @effect-diagnostics-next-line preferSchemaOverJson:off @@ -1390,13 +1450,19 @@ export const make = Effect.gen(function* () { }); } if (input.verdict === "approve") { - yield* api({ cwd: input.cwd, path: `${mergeRequest}/approve`, method: "POST" }); + yield* api({ + cwd: input.cwd, + host: input.host, + path: `${mergeRequest}/approve`, + method: "POST", + }); } }), replyToDiscussion: (input) => api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}/discussions/${encodeURIComponent( input.discussionId, )}/notes`, @@ -1407,6 +1473,7 @@ export const make = Effect.gen(function* () { setDiscussionResolution: (input) => api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}/discussions/${encodeURIComponent( input.discussionId, )}`, diff --git a/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.test.ts b/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.test.ts index 5106fe006ce8d..5342758069b5d 100644 --- a/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.test.ts +++ b/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.test.ts @@ -1,11 +1,85 @@ import { describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; +import { ChildProcessSpawner } from "effect/process"; + +import * as TestSourceControlHost from "@t3tools/source-control-testing/TestSourceControlHost"; import * as GitLabCli from "./GitLabCli.ts"; import * as GitLabPullRequestCli from "./GitLabPullRequestCli.ts"; import { gitLabViewerPermissions, make } from "./GitLabPullRequestProvider.ts"; +it.effect.each(["merge", "disable-auto-merge"] as const)( + "keeps reads and %s on the selected host when glab prefers another remote", + (action) => + Effect.gen(function* () { + const selectedHost = "gitlab.example.test:8443"; + const otherHost = "other.gitlab.example.test"; + const mergedHosts: string[] = []; + const native = yield* GitLabCli.make.pipe( + Effect.provide( + TestSourceControlHost.layer({ + process: { + run: (input) => { + // glab prefers a remote named `gitlab` to `origin`. The checkout's + // selected repository must still own every request and mutation. + const host = input.env?.GITLAB_HOST ?? otherHost; + const path = input.args[1]; + const mutation = input.args[0] === "mr" || input.args.includes("POST"); + if (mutation) mergedHosts.push(host); + const response = mutation + ? {} + : path?.includes("merge_requests/") + ? { + iid: 7, + title: "Merge request 7", + web_url: `https://${host}/acme/web/-/merge_requests/7`, + source_branch: "feature", + target_branch: "main", + created_at: "2026-07-01T00:00:00Z", + updated_at: "2026-07-02T00:00:00Z", + user: { can_merge: true }, + diff_refs: { + head_sha: "a".repeat(40), + base_sha: "b".repeat(40), + start_sha: "b".repeat(40), + }, + } + : { merge_method: "merge", squash_option: "default_off" }; + return Effect.succeed({ + exitCode: ChildProcessSpawner.ExitCode(0), + stdout: JSON.stringify(response), + stderr: "", + stdoutTruncated: false, + stderrTruncated: false, + }); + }, + }, + }), + ), + ); + const cli = yield* GitLabPullRequestCli.make.pipe( + Effect.provideService(GitLabCli.GitLabCli, native), + ); + const provider = yield* make.pipe( + Effect.provideService(GitLabPullRequestCli.GitLabPullRequestCli, cli), + ); + const reference = { + cwd: "/repo", + repository: "acme/web", + host: selectedHost, + number: 7, + }; + const detail = yield* provider.getChangeRequest(reference); + yield* provider.runAction({ ...reference, action }); + + expect({ url: detail.url, mutatedHosts: mergedHosts }).toEqual({ + url: `https://${selectedHost}/acme/web/-/merge_requests/7`, + mutatedHosts: [selectedHost], + }); + }), +); + describe("gitLabViewerPermissions", () => { it("offers everything to a viewer GitLab says can merge", () => { expect(gitLabViewerPermissions({ viewerCanMerge: true })).toEqual({ diff --git a/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.ts b/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.ts index 54b9e96c7e49e..95afc1b3d2a6d 100644 --- a/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.ts +++ b/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.ts @@ -116,13 +116,13 @@ export const make = Effect.gen(function* () { kind: SourceControlProviderKind.make("gitlab"), capabilities: CAPABILITIES, - getViewer: (input) => - cli.getViewerUsername({ cwd: input.cwd }).pipe(Effect.mapError(fail("getViewer"))), + getViewer: (input) => cli.getViewerUsername(input).pipe(Effect.mapError(fail("getViewer"))), listChangeRequests: (input) => cli .listMergeRequests({ cwd: input.cwd, + host: input.host, repository: input.repository, state: input.state, involvement: input.involvement, @@ -143,13 +143,9 @@ export const make = Effect.gen(function* () { ), getChangeRequest: (input) => - Effect.all( - [ - cli.getMergeRequestDetail(input), - cli.getProjectMergeCapabilities({ cwd: input.cwd, repository: input.repository }), - ], - { concurrency: 2 }, - ).pipe( + Effect.all([cli.getMergeRequestDetail(input), cli.getProjectMergeCapabilities(input)], { + concurrency: 2, + }).pipe( Effect.mapError(fail("getChangeRequest")), Effect.map(([mergeRequest, mergeCapabilities]): ProviderChangeRequestDetail => ({ ...mergeRequest, @@ -226,6 +222,7 @@ export const make = Effect.gen(function* () { cli .listReviewerCandidates({ cwd: input.cwd, + host: input.host, repository: input.repository, number: input.number, }) @@ -235,6 +232,7 @@ export const make = Effect.gen(function* () { cli .setReviewerRequest({ cwd: input.cwd, + host: input.host, repository: input.repository, number: input.number, reviewers: input.reviewers, @@ -246,6 +244,7 @@ export const make = Effect.gen(function* () { cli .runMergeRequestAction({ cwd: input.cwd, + host: input.host, repository: input.repository, number: input.number, action: input.action, @@ -257,6 +256,7 @@ export const make = Effect.gen(function* () { cli .updateMergeRequest({ cwd: input.cwd, + host: input.host, repository: input.repository, number: input.number, ...(input.title === undefined ? {} : { title: input.title }), @@ -270,6 +270,7 @@ export const make = Effect.gen(function* () { cli .updateNote({ cwd: input.cwd, + host: input.host, repository: input.repository, number: input.number, noteId: input.commentId, @@ -283,6 +284,7 @@ export const make = Effect.gen(function* () { cli .replyToDiscussion({ cwd: input.cwd, + host: input.host, repository: input.repository, number: input.number, discussionId: input.threadId, @@ -294,6 +296,7 @@ export const make = Effect.gen(function* () { cli .setReaction({ cwd: input.cwd, + host: input.host, repository: input.repository, number: input.number, ...(input.subjectId === undefined ? {} : { noteId: input.subjectId }), @@ -306,6 +309,7 @@ export const make = Effect.gen(function* () { cli .setDiscussionResolution({ cwd: input.cwd, + host: input.host, repository: input.repository, number: input.number, discussionId: input.threadId, From 12a2a7d670f587a45f8d7860bf4c6399362f364b Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 26 Oct 2026 10:32:16 +0000 Subject: [PATCH 2/3] fix(gitlab): keep label and author lookups on the selected host --- .../server/GitLabPullRequestProvider.test.ts | 23 +++++++++++-------- .../src/server/GitLabPullRequestProvider.ts | 14 +++++++++-- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.test.ts b/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.test.ts index e2789e03e44c5..f61ba56becf98 100644 --- a/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.test.ts +++ b/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.test.ts @@ -1,7 +1,6 @@ import { assert, describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; -import { ChildProcessSpawner } from "effect/process"; import * as TestSourceControlHost from "@t3tools/source-control-testing/TestSourceControlHost"; @@ -15,6 +14,7 @@ it.effect.each(["merge", "disable-auto-merge"] as const)( Effect.gen(function* () { const selectedHost = "gitlab.example.test:8443"; const otherHost = "other.gitlab.example.test"; + const readHosts: string[] = []; const mergedHosts: string[] = []; const native = yield* GitLabCli.make.pipe( Effect.provide( @@ -27,6 +27,7 @@ it.effect.each(["merge", "disable-auto-merge"] as const)( const path = input.args[1]; const mutation = input.args[0] === "mr" || input.args.includes("POST"); if (mutation) mergedHosts.push(host); + else readHosts.push(host); const response = mutation ? {} : path?.includes("merge_requests/") @@ -45,14 +46,14 @@ it.effect.each(["merge", "disable-auto-merge"] as const)( start_sha: "b".repeat(40), }, } - : { merge_method: "merge", squash_option: "default_off" }; - return Effect.succeed({ - exitCode: ChildProcessSpawner.ExitCode(0), - stdout: JSON.stringify(response), - stderr: "", - stdoutTruncated: false, - stderrTruncated: false, - }); + : path === "user" + ? { username: "reviewer" } + : path?.includes("/labels?") || path?.includes("/users?") + ? [] + : { merge_method: "merge", squash_option: "default_off" }; + return Effect.succeed( + TestSourceControlHost.processOutput(JSON.stringify(response)), + ); }, }, }), @@ -71,8 +72,12 @@ it.effect.each(["merge", "disable-auto-merge"] as const)( number: 7, }; const detail = yield* provider.getChangeRequest(reference); + expect(yield* provider.getViewer(reference)).toBe("reviewer"); + yield* provider.listRepositoryLabels(reference); + yield* provider.searchRepositoryAuthors(reference); yield* provider.runAction({ ...reference, action }); + expect(readHosts).toEqual(Array(action === "merge" ? 6 : 5).fill(selectedHost)); expect({ url: detail.url, mutatedHosts: mergedHosts }).toEqual({ url: `https://${selectedHost}/acme/web/-/merge_requests/7`, mutatedHosts: [selectedHost], diff --git a/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.ts b/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.ts index 0f23a72831f21..f30e48588daed 100644 --- a/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.ts +++ b/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.ts @@ -234,12 +234,22 @@ export const make = Effect.gen(function* () { listRepositoryLabels: (input) => cli - .listProjectLabels({ cwd: input.cwd, repository: input.repository, search: input.query }) + .listProjectLabels({ + cwd: input.cwd, + host: input.host, + repository: input.repository, + search: input.query, + }) .pipe(Effect.mapError(fail("listRepositoryLabels"))), searchRepositoryAuthors: (input) => cli - .listProjectUsers({ cwd: input.cwd, repository: input.repository, search: input.query }) + .listProjectUsers({ + cwd: input.cwd, + host: input.host, + repository: input.repository, + search: input.query, + }) .pipe(Effect.mapError(fail("searchRepositoryAuthors"))), setReviewerRequest: (input) => From ffeeedc47e0c9949c481836b47506a98d8f6bb16 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 26 Oct 2026 10:33:22 +0000 Subject: [PATCH 3/3] test(gitlab): narrow optional provider lookups --- .../src/server/GitLabPullRequestProvider.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.test.ts b/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.test.ts index f61ba56becf98..3191984cd3d82 100644 --- a/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.test.ts +++ b/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.test.ts @@ -73,7 +73,9 @@ it.effect.each(["merge", "disable-auto-merge"] as const)( }; const detail = yield* provider.getChangeRequest(reference); expect(yield* provider.getViewer(reference)).toBe("reviewer"); + assert.isDefined(provider.listRepositoryLabels); yield* provider.listRepositoryLabels(reference); + assert.isDefined(provider.searchRepositoryAuthors); yield* provider.searchRepositoryAuthors(reference); yield* provider.runAction({ ...reference, action });