From 1d9caa6ee32c19c80fa02b9cca7d8a4b35c6c53c Mon Sep 17 00:00:00 2001 From: Kalven Schraut Date: Fri, 4 Sep 2026 23:20:38 -0500 Subject: [PATCH 1/2] feat(pull-requests): approve native Gitea workflow runs --- .../pullRequest/GiteaPullRequestApi.test.ts | 134 ++++++++++++++++++ .../src/pullRequest/GiteaPullRequestApi.ts | 65 ++++++++- .../GiteaPullRequestProvider.test.ts | 18 +++ .../pullRequest/GiteaPullRequestProvider.ts | 46 +++++- .../src/pullRequest/GiteaWorkflows.test.ts | 77 ++++++++++ apps/server/src/pullRequest/GiteaWorkflows.ts | 69 +++++++++ 6 files changed, 401 insertions(+), 8 deletions(-) create mode 100644 apps/server/src/pullRequest/GiteaWorkflows.test.ts create mode 100644 apps/server/src/pullRequest/GiteaWorkflows.ts diff --git a/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts b/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts index f272999522e2..a58b15a52495 100644 --- a/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts +++ b/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts @@ -147,6 +147,140 @@ layer("GiteaPullRequestApi", (it) => { expect(yield* api.getAutoMergeEnabled({host: "forge.example.test", repository: "acme/web", number: 7})).toBe(true); expect(callAt(1).path).toContain("/timeline?"); })); + it.effect("approves only the current pull request's waiting workflow runs", () => + Effect.gen(function* () { + const pull = rawPullRequest(7); + mockedRequest + .mockReturnValueOnce(Effect.succeed(response(pull))) + .mockReturnValueOnce(Effect.succeed(response({ features: ["actions-run-approve"] }))) + .mockReturnValueOnce(Effect.succeed(response(pull))) + .mockReturnValueOnce( + Effect.succeed( + response({ + total_count: 1, + workflow_runs: [ + { + id: 42, + needs_approval: true, + pull_request_head_sha: "head-sha", + head_sha: "merge-sha", + event: "pull_request", + html_url: "https://forge.example.test/run/42", + pull_requests: [{ number: 7 }], + }, + ], + }), + ), + ) + .mockReturnValueOnce(Effect.succeed(response(pull))) + .mockReturnValueOnce(Effect.succeed(response({}))); + const api = yield* GiteaPullRequestApi.make.pipe( + Effect.provideService( + GiteaApi.GiteaApi, + GiteaApi.GiteaApi.of({ + baseUrl: Option.some("https://forge.example.test/gitea"), + sshHosts: ["work-forge"], + request: mockedRequest, + probeAuth: Effect.die("not used"), + }), + ), + ); + yield* api.runAction({ + host: "forge.example.test", + repository: "acme/web", + number: 7, + action: "approve-workflows", + }); + expect( + mockedRequest.mock.calls + .filter(([call]) => call.method === "POST") + .map(([call]) => call.path), + ).toEqual(["/repos/acme/web/actions/runs/42/approve"]); + }), + ); + + it.effect("rejects workflow approval on servers without native approval metadata", () => + Effect.gen(function* () { + mockedRequest + .mockReturnValueOnce(Effect.succeed(response(rawPullRequest(7)))) + .mockReturnValueOnce(Effect.succeed(response({}))); + const api = yield* GiteaPullRequestApi.make.pipe( + Effect.provideService( + GiteaApi.GiteaApi, + GiteaApi.GiteaApi.of({ + baseUrl: Option.some("https://forge.example.test/gitea"), + sshHosts: ["work-forge"], + request: mockedRequest, + probeAuth: Effect.die("not used"), + }), + ), + ); + const error = yield* api + .runAction({ + host: "forge.example.test", + repository: "acme/web", + number: 7, + action: "approve-workflows", + }) + .pipe(Effect.flip); + expect(error.detail).toContain("does not expose workflow approval metadata"); + expect(mockedRequest.mock.calls.every(([call]) => call.method === "GET")).toBe(true); + }), + ); + + it.effect("never approves a workflow after the pull request head changes", () => + Effect.gen(function* () { + mockedRequest.mockReturnValueOnce(Effect.succeed(response(rawPullRequest(7)))); + mockedRequest.mockReturnValueOnce( + Effect.succeed(response({ features: ["actions-run-approve"] })), + ); + mockedRequest.mockReturnValueOnce(Effect.succeed(response(rawPullRequest(7)))); + mockedRequest.mockReturnValueOnce( + Effect.succeed( + response({ + total_count: 1, + workflow_runs: [ + { + id: 42, + needs_approval: true, + pull_request_head_sha: "head-sha", + head_sha: "merge-sha", + event: "pull_request", + html_url: "https://forge.example.test/run/42", + pull_requests: [{ number: 7 }], + }, + ], + }), + ), + ); + mockedRequest.mockReturnValueOnce( + Effect.succeed( + response(rawPullRequest(7, { head: { ref: "feature", sha: "changed-head" } })), + ), + ); + const api = yield* GiteaPullRequestApi.make.pipe( + Effect.provideService( + GiteaApi.GiteaApi, + GiteaApi.GiteaApi.of({ + baseUrl: Option.some("https://forge.example.test/gitea"), + sshHosts: ["work-forge"], + request: mockedRequest, + probeAuth: Effect.die("not used"), + }), + ), + ); + const error = yield* api + .runAction({ + host: "forge.example.test", + repository: "acme/web", + number: 7, + action: "approve-workflows", + }) + .pipe(Effect.flip); + expect(error.detail).toContain("head changed"); + expect(mockedRequest.mock.calls.every(([call]) => call.method === "GET")).toBe(true); + }), + ); it.effect("validates the requested host before making an HTTP request", () => Effect.gen(function* () { diff --git a/apps/server/src/pullRequest/GiteaPullRequestApi.ts b/apps/server/src/pullRequest/GiteaPullRequestApi.ts index 8411ee84f9bb..a0d105e94639 100644 --- a/apps/server/src/pullRequest/GiteaPullRequestApi.ts +++ b/apps/server/src/pullRequest/GiteaPullRequestApi.ts @@ -28,6 +28,7 @@ import type { import * as GiteaApi from "../sourceControl/GiteaApi.ts"; import * as GiteaLifecycle from "./GiteaLifecycle.ts"; +import * as GiteaWorkflows from "./GiteaWorkflows.ts"; import { editableCommentId, type GiteaConversationReactionTarget, @@ -445,6 +446,14 @@ function nextPagePath(input: { export class GiteaPullRequestApi extends Context.Service< GiteaPullRequestApi, { + readonly getWorkflowApprovals: (input: { + host: string; + repository: string; + number: number; + }) => Effect.Effect< + { supported: boolean; runs: ReadonlyArray }, + GiteaPullRequestApiError + >; readonly getViewer: () => Effect.Effect; readonly getFeatures: () => Effect.Effect, GiteaPullRequestApiError>; readonly listPullRequests: (input: { @@ -741,6 +750,58 @@ export const make = Effect.gen(function* () { }, ); + const getWorkflowApprovals = Effect.fn("GiteaPullRequestApi.getWorkflowApprovals")( + function* (input: { host: string; repository: string; number: number }) { + yield* validateHost(input.host); + if (!(yield* getFeatures).includes("actions-run-approve")) + return { supported: false, runs: [] }; + const pull = yield* getPullRequest(input); + if (pull.state !== "open") return { supported: true, runs: [] }; + const runs = yield* GiteaWorkflows.list(gitea, { ...input, headSha: pull.headSha }).pipe( + Effect.mapError((error) => failure("getWorkflowApprovals", error)), + ); + return { supported: true, runs }; + }, + ); + + const approveWorkflows = Effect.fn("GiteaPullRequestApi.approveWorkflows")(function* (input: { + host: string; + repository: string; + number: number; + }) { + const before = yield* getPullRequest(input); + const approvals = yield* getWorkflowApprovals(input); + if (!approvals.supported) + return yield* new GiteaPullRequestApiError({ + operation: "approveWorkflows", + reason: "failed", + detail: "This Gitea server does not expose workflow approval metadata.", + }); + for (const run of approvals.runs) { + const current = yield* getPullRequest(input); + if ( + current.state !== "open" || + current.headSha !== before.headSha || + !GiteaWorkflows.isCurrentPullWorkflow(run, { + number: input.number, + headSha: current.headSha, + }) + ) { + return yield* new GiteaPullRequestApiError({ + operation: "approveWorkflows", + reason: "failed", + detail: "The pull request head changed; refresh before approving workflows.", + }); + } + yield* request({ + operation: "approveWorkflows", + ...input, + method: "POST", + path: `${basePath(input.repository)}/actions/runs/${run.id}/approve`, + }); + } + }); + const readUnknownPage = Effect.fn("GiteaPullRequestApi.readUnknownPage")(function* (input: { operation: string; host: string; @@ -1558,6 +1619,7 @@ export const make = Effect.gen(function* () { return GiteaPullRequestApi.of({ getFeatures: () => getFeatures, + getWorkflowApprovals, getViewer: Effect.fn("GiteaPullRequestApi.getViewer")(function* () { const response = yield* gitea .request({ @@ -1747,8 +1809,9 @@ export const make = Effect.gen(function* () { number: input.number, action: input.action, }); - case "revert": case "approve-workflows": + return approveWorkflows(input); + case "revert": return Effect.fail(unsupportedAction(input.action)); } }, diff --git a/apps/server/src/pullRequest/GiteaPullRequestProvider.test.ts b/apps/server/src/pullRequest/GiteaPullRequestProvider.test.ts index df0867a7c2ed..33493f55a7f5 100644 --- a/apps/server/src/pullRequest/GiteaPullRequestProvider.test.ts +++ b/apps/server/src/pullRequest/GiteaPullRequestProvider.test.ts @@ -8,6 +8,24 @@ import { import { GiteaPullRequestApiError } from "./GiteaPullRequestApi.ts"; describe("giteaViewerPermissions", () => { + it("offers workflow approval only when the server supports it and the viewer can write", () => { + expect( + giteaViewerPermissions({ + canWrite: true, + ownsPullRequest: false, + updateMethods: [], + workflowApprovalSupported: true, + }).actions, + ).toContain("approve-workflows"); + expect( + giteaViewerPermissions({ + canWrite: false, + ownsPullRequest: true, + updateMethods: [], + workflowApprovalSupported: true, + }).actions, + ).not.toContain("approve-workflows"); + }); it("offers repository writes and only the configured branch update strategies", () => { expect( giteaViewerPermissions({ diff --git a/apps/server/src/pullRequest/GiteaPullRequestProvider.ts b/apps/server/src/pullRequest/GiteaPullRequestProvider.ts index 30e8664726a9..108aa3ab9022 100644 --- a/apps/server/src/pullRequest/GiteaPullRequestProvider.ts +++ b/apps/server/src/pullRequest/GiteaPullRequestProvider.ts @@ -24,6 +24,7 @@ const CAPABILITIES: PullRequestCapabilities = { "update-branch", "enable-auto-merge", "disable-auto-merge", + "approve-workflows", ], mergeMethods: ["merge", "squash", "rebase"], updateMethods: ["merge", "rebase"], @@ -64,11 +65,14 @@ export function giteaProviderFailure( export function giteaViewerPermissions(input: { readonly canWrite: boolean; + readonly workflowApprovalSupported?: boolean; readonly ownsPullRequest: boolean; readonly updateMethods: ReadonlyArray<"merge" | "rebase">; }): PullRequestViewerPermissions { return { actions: CAPABILITIES.actions.filter((action) => { + if (action === "approve-workflows") + return input.canWrite && input.workflowApprovalSupported === true; if (action === "ready" || action === "draft" || action === "close" || action === "reopen") return input.canWrite || input.ownsPullRequest; return input.canWrite; @@ -126,9 +130,11 @@ export const make = Effect.gen(function* () { readonly access: GiteaPullRequestApi.GiteaRepositoryAccess; readonly viewer: string; readonly author: string | undefined; + readonly workflowApprovalSupported?: boolean; }) => giteaViewerPermissions({ canWrite: input.access.canWrite, + workflowApprovalSupported: input.workflowApprovalSupported, ownsPullRequest: input.author !== undefined && input.author.toLowerCase() === input.viewer.toLowerCase(), updateMethods: input.access.updateMethods, @@ -174,10 +180,13 @@ export const make = Effect.gen(function* () { api.getRepositoryAccess(input), api.getViewer(), api.getAutoMergeEnabled(input), + api + .getWorkflowApprovals(input) + .pipe(Effect.orElseSucceed(() => ({ supported: false, runs: [] }))), ], { concurrency: 4 }, ).pipe( - Effect.flatMap(([pullRequest, access, viewer, autoMergeEnabled]) => + Effect.flatMap(([pullRequest, access, viewer, autoMergeEnabled, workflows]) => api.listChecks({ ...input, sha: pullRequest.headSha }).pipe( Effect.orElseSucceed(() => []), Effect.map((checks): ProviderChangeRequestDetail => ({ @@ -187,7 +196,16 @@ export const make = Effect.gen(function* () { mergedAt: pullRequest.mergedAt, closedAt: pullRequest.closedAt, reviewers: pullRequest.reviewers, - checks, + checks: [ + ...checks, + ...workflows.runs.map((run) => ({ + name: run.display_title?.trim() || `Workflow ${run.id}`, + status: "action-required" as const, + description: "Awaiting approval", + url: run.html_url, + })), + ], + ...(workflows.supported ? { workflowApprovalsRequired: workflows.runs.length } : {}), mergeCapabilities: access.mergeCapabilities, baseComparison: giteaBaseComparison(pullRequest), ...(autoMergeEnabled === undefined ? {} : { autoMergeEnabled }), @@ -198,6 +216,7 @@ export const make = Effect.gen(function* () { access, viewer, author: pullRequest.author?.login, + workflowApprovalSupported: workflows.supported, }), })), ), @@ -288,12 +307,25 @@ export const make = Effect.gen(function* () { ), getViewerPermissions: (input) => - Effect.all([api.getPullRequest(input), api.getRepositoryAccess(input), api.getViewer()], { - concurrency: 3, - }).pipe( + Effect.all( + [ + api.getPullRequest(input), + api.getRepositoryAccess(input), + api.getViewer(), + api.getFeatures().pipe(Effect.orElseSucceed(() => [])), + ], + { + concurrency: 3, + }, + ).pipe( Effect.mapError(fail("getViewerPermissions")), - Effect.map(([pullRequest, access, viewer]) => - permissions({ access, viewer, author: pullRequest.author?.login }), + Effect.map(([pullRequest, access, viewer, features]) => + permissions({ + access, + viewer, + author: pullRequest.author?.login, + workflowApprovalSupported: features.includes("actions-run-approve"), + }), ), ), diff --git a/apps/server/src/pullRequest/GiteaWorkflows.test.ts b/apps/server/src/pullRequest/GiteaWorkflows.test.ts new file mode 100644 index 000000000000..594ec5435eba --- /dev/null +++ b/apps/server/src/pullRequest/GiteaWorkflows.test.ts @@ -0,0 +1,77 @@ +import { assert, expect, it, vi } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as Option from "effect/Option"; + +import * as GiteaApi from "../sourceControl/GiteaApi.ts"; +import { isCurrentPullWorkflow, list } from "./GiteaWorkflows.ts"; + +const run = { + id: 7, + needs_approval: true, + pull_request_head_sha: "contributor-head", + head_sha: "synthetic-merge", + event: "pull_request", + html_url: "https://forge.test/org/repo/actions/runs/7", + pull_requests: [{ number: 3 }], +}; +const input = { repository: "org/repo", number: 3, headSha: "contributor-head" }; + +it("matches the recorded contributor commit rather than a synthetic merge revision", () => { + assert.isTrue(isCurrentPullWorkflow(run, input)); + for (const candidate of [ + { ...run, needs_approval: false }, + { ...run, pull_request_head_sha: "previous-head" }, + { ...run, pull_request_head_sha: undefined }, + { ...run, pull_requests: [{ number: 4 }] }, + { ...run, event: "push" }, + ]) + assert.isFalse(isCurrentPullWorkflow(candidate, input)); +}); + +it.effect("reads capped pages completely and selects only this PR's current blocked runs", () => + Effect.gen(function* () { + const request = vi.fn(); + request.mockReturnValueOnce( + Effect.succeed({ + body: JSON.stringify({ + total_count: 2, + workflow_runs: [{ ...run, pull_request_head_sha: "old" }], + }), + truncated: false, + headers: {}, + }), + ); + request.mockReturnValueOnce( + Effect.succeed({ + body: JSON.stringify({ total_count: 2, workflow_runs: [run] }), + truncated: false, + headers: {}, + }), + ); + const api = GiteaApi.GiteaApi.of({ + baseUrl: Option.some("https://forge.test"), + request, + probeAuth: Effect.die("not used"), + }); + expect(yield* list(api, input)).toEqual([run]); + expect(request.mock.calls[1]?.[0].path).toContain("page=2"); + }), +); + +it.effect("fails incomplete pagination instead of reporting no approvals", () => + Effect.gen(function* () { + const request = vi.fn(() => + Effect.succeed({ + body: JSON.stringify({ total_count: 1, workflow_runs: [] }), + truncated: false, + headers: {}, + }), + ); + const api = GiteaApi.GiteaApi.of({ + baseUrl: Option.some("https://forge.test"), + request, + probeAuth: Effect.die("not used"), + }); + assert.strictEqual((yield* list(api, input).pipe(Effect.flip)).reason, "failed"); + }), +); diff --git a/apps/server/src/pullRequest/GiteaWorkflows.ts b/apps/server/src/pullRequest/GiteaWorkflows.ts new file mode 100644 index 000000000000..b072bc065da2 --- /dev/null +++ b/apps/server/src/pullRequest/GiteaWorkflows.ts @@ -0,0 +1,69 @@ +import * as Effect from "effect/Effect"; +import * as Schema from "effect/Schema"; +import { NonNegativeInt, PositiveInt } from "@t3tools/contracts"; + +import * as GiteaApi from "../sourceControl/GiteaApi.ts"; +import { giteaRepositoryPath, parseGiteaRepository } from "../sourceControl/giteaRepository.ts"; + +const WorkflowRun = Schema.Struct({ + id: PositiveInt, + needs_approval: Schema.Boolean, + pull_request_head_sha: Schema.optional(Schema.String), + head_sha: Schema.String, + event: Schema.String, + display_title: Schema.optional(Schema.String), + html_url: Schema.String, + pull_requests: Schema.Array(Schema.Struct({ number: PositiveInt })), +}); +const WorkflowPage = Schema.Struct({ + total_count: NonNegativeInt, + workflow_runs: Schema.Array(WorkflowRun), +}); +export type GiteaWorkflowRun = typeof WorkflowRun.Type; + +export function isCurrentPullWorkflow( + run: GiteaWorkflowRun, + input: { readonly number: number; readonly headSha: string }, +): boolean { + return ( + run.needs_approval && + run.event === "pull_request" && + run.pull_request_head_sha === input.headSha && + input.headSha !== "" && + run.pull_requests.some((pull) => pull.number === input.number) + ); +} + +export const list = Effect.fn("GiteaWorkflows.list")(function* ( + api: GiteaApi.GiteaApi["Service"], + input: { readonly repository: string; readonly number: number; readonly headSha: string }, +) { + const operation = "listWorkflowApprovals"; + const repository = parseGiteaRepository(input.repository); + if (repository === null) { + return yield* new GiteaApi.GiteaApiError({ + operation, + reason: "failed", + detail: "Invalid Gitea repository.", + }); + } + const runs: GiteaWorkflowRun[] = []; + let seen = 0; + for (let page = 1; page <= 100; page += 1) { + const response = yield* api.request({ + operation, + method: "GET", + path: `${giteaRepositoryPath(repository)}/actions/runs?event=pull_request&page=${page}&limit=50`, + }); + const result = yield* GiteaApi.decodeGiteaResponse(operation, WorkflowPage, response); + seen += result.workflow_runs.length; + runs.push(...result.workflow_runs.filter((run) => isCurrentPullWorkflow(run, input))); + if (seen >= result.total_count) return runs; + if (result.workflow_runs.length === 0) break; + } + return yield* new GiteaApi.GiteaApiError({ + operation, + reason: "failed", + detail: "Gitea's workflow listing could not be read completely.", + }); +}); From 85dd5287722ae696a05cf1c5317208faba0c1715 Mon Sep 17 00:00:00 2001 From: Kalven Schraut Date: Fri, 4 Sep 2026 23:27:54 -0500 Subject: [PATCH 2/2] fix(pull-requests): validate Gitea feature discovery and action gating --- .../pullRequest/GiteaForkCapabilities.test.ts | 13 ++++ .../src/pullRequest/GiteaForkCapabilities.ts | 17 ++++-- .../pullRequest/GiteaPullRequestApi.test.ts | 59 +++++-------------- .../src/pullRequest/GiteaPullRequestApi.ts | 11 +++- 4 files changed, 51 insertions(+), 49 deletions(-) diff --git a/apps/server/src/pullRequest/GiteaForkCapabilities.test.ts b/apps/server/src/pullRequest/GiteaForkCapabilities.test.ts index 105a5869b177..f604473d50cc 100644 --- a/apps/server/src/pullRequest/GiteaForkCapabilities.test.ts +++ b/apps/server/src/pullRequest/GiteaForkCapabilities.test.ts @@ -21,6 +21,19 @@ const base: PullRequestCapabilities = { }; describe("GiteaForkCapabilities", () => { + it("offers fork-only actions only when the server advertises them", () => { + const available: PullRequestCapabilities = { + ...base, + actions: ["merge", "approve-workflows", "revert"], + }; + expect(giteaForkCapabilities(available, []).actions).toEqual(["merge"]); + expect(giteaForkCapabilities(available, ["actions-run-approve"]).actions).toEqual([ + "merge", + "approve-workflows", + ]); + expect(giteaForkCapabilities(available, ["pull-revert"]).actions).toEqual(["merge", "revert"]); + expect(available.actions).toEqual(["merge", "approve-workflows", "revert"]); + }); it("enables review-summary reactions only for an advertising server", () => { expect(giteaForkCapabilities(base, []).reactionSubjects?.review).toBe(false); expect(giteaForkCapabilities(base, ["pull-review-reactions"]).reactionSubjects?.review).toBe( diff --git a/apps/server/src/pullRequest/GiteaForkCapabilities.ts b/apps/server/src/pullRequest/GiteaForkCapabilities.ts index 5dada5cd8536..e6580bde7448 100644 --- a/apps/server/src/pullRequest/GiteaForkCapabilities.ts +++ b/apps/server/src/pullRequest/GiteaForkCapabilities.ts @@ -1,13 +1,22 @@ import type { PullRequestCapabilities } from "@t3tools/contracts"; -/** Capabilities added by the companion Gitea API extension, discovered lazily per server. */ export function giteaForkCapabilities( base: PullRequestCapabilities, features: ReadonlyArray, ): PullRequestCapabilities { - return features.includes("pull-review-reactions") - ? { ...base, reactionSubjects: { ...base.reactionSubjects!, review: true } } - : base; + return { + ...base, + actions: base.actions.filter((action) => + action === "approve-workflows" + ? features.includes("actions-run-approve") + : action === "revert" + ? features.includes("pull-revert") + : true, + ), + ...(features.includes("pull-review-reactions") + ? { reactionSubjects: { ...base.reactionSubjects!, review: true } } + : {}), + }; } export function giteaHasFeature(features: ReadonlyArray, feature: string): boolean { diff --git a/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts b/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts index a58b15a52495..81c201979066 100644 --- a/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts +++ b/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts @@ -11,12 +11,15 @@ const mockedRequest = vi.fn(); const decodeJson = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown)); const layer = it.layer( - Layer.succeed(GiteaApi.GiteaApi, GiteaApi.GiteaApi.of({ - baseUrl: Option.some("https://forge.example.test/gitea"), - sshHosts: ["work-forge"], - request: mockedRequest, - probeAuth: Effect.die("not used"), - })), + Layer.succeed( + GiteaApi.GiteaApi, + GiteaApi.GiteaApi.of({ + baseUrl: Option.some("https://forge.example.test/gitea"), + sshHosts: ["work-forge"], + request: mockedRequest, + probeAuth: Effect.die("not used"), + }), + ), ); function response(value: unknown, headers: Readonly> = {}) { @@ -174,17 +177,7 @@ layer("GiteaPullRequestApi", (it) => { ) .mockReturnValueOnce(Effect.succeed(response(pull))) .mockReturnValueOnce(Effect.succeed(response({}))); - const api = yield* GiteaPullRequestApi.make.pipe( - Effect.provideService( - GiteaApi.GiteaApi, - GiteaApi.GiteaApi.of({ - baseUrl: Option.some("https://forge.example.test/gitea"), - sshHosts: ["work-forge"], - request: mockedRequest, - probeAuth: Effect.die("not used"), - }), - ), - ); + const api = yield* GiteaPullRequestApi.make; yield* api.runAction({ host: "forge.example.test", repository: "acme/web", @@ -204,17 +197,7 @@ layer("GiteaPullRequestApi", (it) => { mockedRequest .mockReturnValueOnce(Effect.succeed(response(rawPullRequest(7)))) .mockReturnValueOnce(Effect.succeed(response({}))); - const api = yield* GiteaPullRequestApi.make.pipe( - Effect.provideService( - GiteaApi.GiteaApi, - GiteaApi.GiteaApi.of({ - baseUrl: Option.some("https://forge.example.test/gitea"), - sshHosts: ["work-forge"], - request: mockedRequest, - probeAuth: Effect.die("not used"), - }), - ), - ); + const api = yield* GiteaPullRequestApi.make; const error = yield* api .runAction({ host: "forge.example.test", @@ -258,17 +241,7 @@ layer("GiteaPullRequestApi", (it) => { response(rawPullRequest(7, { head: { ref: "feature", sha: "changed-head" } })), ), ); - const api = yield* GiteaPullRequestApi.make.pipe( - Effect.provideService( - GiteaApi.GiteaApi, - GiteaApi.GiteaApi.of({ - baseUrl: Option.some("https://forge.example.test/gitea"), - sshHosts: ["work-forge"], - request: mockedRequest, - probeAuth: Effect.die("not used"), - }), - ), - ); + const api = yield* GiteaPullRequestApi.make; const error = yield* api .runAction({ host: "forge.example.test", @@ -1345,7 +1318,7 @@ layer("GiteaPullRequestApi", (it) => { it.effect("reports Gitea's missing review-summary reaction route", () => Effect.gen(function* () { - mockedRequest.mockReturnValueOnce(Effect.succeed(response({features: []}))); + mockedRequest.mockReturnValueOnce(Effect.succeed(response({ features: [] }))); const api = yield* GiteaPullRequestApi.make; const error = yield* api .setReaction({ @@ -1438,7 +1411,7 @@ layer("GiteaPullRequestApi", (it) => { Effect.gen(function* () { mockedRequest .mockReturnValueOnce(Effect.succeed(response({}))) - .mockReturnValueOnce(Effect.succeed(response({features: []}))) + .mockReturnValueOnce(Effect.succeed(response({ features: [] }))) .mockReturnValueOnce(Effect.succeed(response(rawPullRequest(7)))) .mockReturnValueOnce(Effect.succeed(response({}))) .mockReturnValueOnce( @@ -1498,7 +1471,7 @@ layer("GiteaPullRequestApi", (it) => { it.effect("restores the title when Gitea does not recognize the configured draft prefix", () => Effect.gen(function* () { mockedRequest - .mockReturnValueOnce(Effect.succeed(response({features: []}))) + .mockReturnValueOnce(Effect.succeed(response({ features: [] }))) .mockReturnValueOnce(Effect.succeed(response(rawPullRequest(7)))) .mockReturnValueOnce(Effect.succeed(response({}))) .mockReturnValueOnce( @@ -1586,7 +1559,7 @@ layer("GiteaPullRequestApi", (it) => { it.effect("honors a server timeline page-size cap before reading the final merge state", () => Effect.gen(function* () { - mockedRequest.mockReturnValueOnce(Effect.succeed(response({features: []}))); + mockedRequest.mockReturnValueOnce(Effect.succeed(response({ features: [] }))); mockedRequest.mockReturnValueOnce( Effect.succeed( response([{ id: 1, type: "pull_scheduled_merge" }], { "x-total-count": "2" }), diff --git a/apps/server/src/pullRequest/GiteaPullRequestApi.ts b/apps/server/src/pullRequest/GiteaPullRequestApi.ts index a0d105e94639..9885b7f44879 100644 --- a/apps/server/src/pullRequest/GiteaPullRequestApi.ts +++ b/apps/server/src/pullRequest/GiteaPullRequestApi.ts @@ -915,7 +915,9 @@ export const make = Effect.gen(function* () { ); const getFeatures = yield* Effect.cachedWithTTL( - Effect.suspend(() => gitea.request({ operation: "getFeatures", method: "GET", path: "/settings/api" })).pipe( + Effect.suspend(() => + gitea.request({ operation: "getFeatures", method: "GET", path: "/settings/api" }), + ).pipe( Effect.mapError((error) => failure("getFeatures", error)), Effect.flatMap((response) => decode( @@ -2017,7 +2019,12 @@ export const make = Effect.gen(function* () { ); } return Effect.gen(function* () { - if (target.kind === "review" && !(yield* getFeatures.pipe(Effect.orElseSucceed(() => []))).includes("pull-review-reactions")) { + if ( + target.kind === "review" && + !(yield* getFeatures.pipe(Effect.orElseSucceed(() => []))).includes( + "pull-review-reactions", + ) + ) { return yield* new GiteaPullRequestApiError({ operation: "setReaction", reason: "failed",