authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-09 16:22:36-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-09 16:22:36-07:00
log2ce3e4ae74deb63bda7aaf059949a12aaaefed56
tree5292f030d633c2e15474f629e79ee9ae464809b1
parent49f849ecce3667abab7ce9e6aaa42dfd2e3c45d7
signature Signed by PGP key B5690EEEBB952194

refactor(source-control): PullRequestService reads GitHub resolvers, not its kind (#17619)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

11 files changed, 49 insertions(+), 31 deletions(-)

apps/server/src/pullRequest/PullRequestService.test.ts+2
...@@ -463,6 +463,8 @@ function fakeProvider(...@@ -463,6 +463,8 @@ function fakeProvider(
463 setReaction: () => Effect.void,463 setReaction: () => Effect.void,
464 listReviewerCandidates: () => Effect.succeed({ candidates: [], truncated: false }),464 listReviewerCandidates: () => Effect.succeed({ candidates: [], truncated: false }),
465 setReviewerRequest: () => Effect.void,465 setReviewerRequest: () => Effect.void,
466 // GitHub's own merge message rewrite, which the service reads instead of the kind.
467 ...(kind === "github" ? { mergeMessageRewrite: (message: string) => message } : {}),
466 ...overrides,468 ...overrides,
467 };469 };
468}470}
apps/server/src/pullRequest/PullRequestService.ts+12-7
...@@ -77,7 +77,6 @@ import {...@@ -77,7 +77,6 @@ import {
77import { resolveProjectSettings } from "@t3tools/shared/projectSettings";77import { resolveProjectSettings } from "@t3tools/shared/projectSettings";
78import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl";78import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl";
7979
80import { AllowGitHubReserve } from "@t3tools/source-control-github/server/GitHubApi";
81import * as ProjectService from "../project/ProjectService.ts";80import * as ProjectService from "../project/ProjectService.ts";
82import * as ServerSettings from "../serverSettings.ts";81import * as ServerSettings from "../serverSettings.ts";
83import * as PullRequestFilesViewed from "../persistence/PullRequestFilesViewed.ts";82import * as PullRequestFilesViewed from "../persistence/PullRequestFilesViewed.ts";
...@@ -557,7 +556,7 @@ function withRateLimitBackoff(...@@ -557,7 +556,7 @@ function withRateLimitBackoff(
557 ),556 ),
558 Effect.flatMap((lease) =>557 Effect.flatMap((lease) =>
559 effect.pipe(558 effect.pipe(
560 Effect.provideService(AllowGitHubReserve, allowPaused),559 Effect.provideService(SourceControlRateLimit.Interactive, allowPaused),
561 Effect.tap(() => limits.recordSuccess({ ...key, lease })),560 Effect.tap(() => limits.recordSuccess({ ...key, lease })),
562 Effect.tapError((error) =>561 Effect.tapError((error) =>
563 error.reason === "rate-limited"562 error.reason === "rate-limited"
...@@ -587,6 +586,9 @@ function withRateLimitBackoff(...@@ -587,6 +586,9 @@ function withRateLimitBackoff(
587 const wrapped = {586 const wrapped = {
588 kind: api.kind,587 kind: api.kind,
589 capabilities: api.capabilities,588 capabilities: api.capabilities,
589 ...(api.mergeMessageRewrite === undefined
590 ? {}
591 : { mergeMessageRewrite: api.mergeMessageRewrite }),
590 // Refused during a pause like any other read, except for the caller that asks for the592 // Refused during a pause like any other read, except for the caller that asks for the
591 // bypass: a lookup that failed is not held, so letting every background read through would593 // bypass: a lookup that failed is not held, so letting every background read through would
592 // spawn this host's CLI on each of them and re-extend the pause it was already in.594 // spawn this host's CLI on each of them and re-extend the pause it was already in.
...@@ -1558,8 +1560,10 @@ export const make = Effect.gen(function* () {...@@ -1558,8 +1560,10 @@ export const make = Effect.gen(function* () {
1558 )(function* (input) {1560 )(function* (input) {
1559 const host = input.host.toLowerCase();1561 const host = input.host.toLowerCase();
1560 const { supported } = yield* listWorkspaceProjects({ host });1562 const { supported } = yield* listWorkspaceProjects({ host });
1561 const project = supported.find((candidate) => candidate.api.kind === "github");1563 const project = supported.find(
1562 const api = registry.get("github");1564 (candidate) => registry.get(candidate.api.kind)?.getRoutingIdentity !== undefined,
1565 );
1566 const api = project === undefined ? null : registry.get(project.api.kind);
1563 if (project === undefined || api?.getRoutingIdentity === undefined) {1567 if (project === undefined || api?.getRoutingIdentity === undefined) {
1564 return yield* new PullRequestUnavailableError({ reason: "provider-unsupported" });1568 return yield* new PullRequestUnavailableError({ reason: "provider-unsupported" });
1565 }1569 }
...@@ -1569,6 +1573,7 @@ export const make = Effect.gen(function* () {...@@ -1569,6 +1573,7 @@ export const make = Effect.gen(function* () {
1569 host,1573 host,
1570 })1574 })
1571 .pipe(Effect.mapError(toPullRequestError("routeIdentity")));1575 .pipe(Effect.mapError(toPullRequestError("routeIdentity")));
1576 // Only GitHub reports a routing identity, and the contract names it.
1572 return { ...identity, host, provider: "github" as const };1577 return { ...identity, host, provider: "github" as const };
1573 });1578 });
15741579
...@@ -1584,7 +1589,7 @@ export const make = Effect.gen(function* () {...@@ -1584,7 +1589,7 @@ export const make = Effect.gen(function* () {
1584 detail: "The GitHub account could not be verified before starting the operation.",1589 detail: "The GitHub account could not be verified before starting the operation.",
1585 });1590 });
1586 const project = yield* requireProject(input).pipe(Effect.mapError(rejected));1591 const project = yield* requireProject(input).pipe(Effect.mapError(rejected));
1587 const api = project.api.kind === "github" ? registry.get("github") : null;1592 const api = registry.get(project.api.kind);
1588 if (1593 if (
1589 api?.withVerifiedCredential === undefined ||1594 api?.withVerifiedCredential === undefined ||
1590 input.host?.toLowerCase() !== project.host.toLowerCase()1595 input.host?.toLowerCase() !== project.host.toLowerCase()
...@@ -1605,7 +1610,7 @@ export const make = Effect.gen(function* () {...@@ -1605,7 +1610,7 @@ export const make = Effect.gen(function* () {
16051610
1606 const routing = Effect.fn("PullRequestService.routing")(function* (input: PullRequestRef) {1611 const routing = Effect.fn("PullRequestService.routing")(function* (input: PullRequestRef) {
1607 const project = yield* requireProject(input);1612 const project = yield* requireProject(input);
1608 const api = project.api.kind === "github" ? registry.get("github") : null;1613 const api = registry.get(project.api.kind);
1609 if (api?.getRoutingIdentity === undefined) {1614 if (api?.getRoutingIdentity === undefined) {
1610 return yield* new PullRequestUnavailableError({ reason: "provider-unsupported" });1615 return yield* new PullRequestUnavailableError({ reason: "provider-unsupported" });
1611 }1616 }
...@@ -2047,7 +2052,7 @@ export const make = Effect.gen(function* () {...@@ -2047,7 +2052,7 @@ export const make = Effect.gen(function* () {
2047 );2052 );
2048 }2053 }
2049 const mergeSettings =2054 const mergeSettings =
2050 project.api.kind === "github" &&2055 project.api.mergeMessageRewrite !== undefined &&
2051 input.stackNumber === undefined &&2056 input.stackNumber === undefined &&
2052 (input.action === "merge" || input.action === "enable-auto-merge")2057 (input.action === "merge" || input.action === "enable-auto-merge")
2053 ? serverSettings.getSettings.pipe(2058 ? serverSettings.getSettings.pipe(
packages/source-control-core/src/server/PullRequestProvider.ts+8-1
...@@ -345,6 +345,13 @@ export interface PullRequestProviderApi {...@@ -345,6 +345,13 @@ export interface PullRequestProviderApi {
345 >;345 >;
346 readonly kind: SourceControlProviderKind;346 readonly kind: SourceControlProviderKind;
347 readonly capabilities: PullRequestCapabilities;347 readonly capabilities: PullRequestCapabilities;
348 /**
349 * Rewrites the message a merge will use, for a host that lets the merge carry custom text.
350 * Absent means the host always writes its own message, so nothing is read to decide on one.
351 * Today's only rewrite strips agent credits (`mergeMessage.removeAgentCredits`) when the
352 * project asks for it.
353 */
354 readonly mergeMessageRewrite?: (message: string) => string;
348355
349 /** The signed-in account, which is what involvement filtering compares against. */356 /** The signed-in account, which is what involvement filtering compares against. */
350 readonly getViewer: (input: {357 readonly getViewer: (input: {
...@@ -561,7 +568,7 @@ export interface PullRequestProviderApi {...@@ -561,7 +568,7 @@ export interface PullRequestProviderApi {
561 readonly action: PullRequestAction;568 readonly action: PullRequestAction;
562 readonly stackNumber?: number;569 readonly stackNumber?: number;
563 readonly expectedStackHeads?: ReadonlyArray<PullRequestStackHead>;570 readonly expectedStackHeads?: ReadonlyArray<PullRequestStackHead>;
564 /** GitHub merge message cleanup; ignored by hosts without support. */571 /** Apply `mergeMessageRewrite` to the merge message; never sent to a host without one. */
565 readonly removeAgentCreditsOnMerge?: boolean;572 readonly removeAgentCreditsOnMerge?: boolean;
566 /** Meaningful for `merge` and `enable-auto-merge`; absent takes the host's own default. */573 /** Meaningful for `merge` and `enable-auto-merge`; absent takes the host's own default. */
567 readonly mergeMethod?: PullRequestMergeMethod;574 readonly mergeMethod?: PullRequestMergeMethod;
packages/source-control-core/src/server/SourceControlRateLimit.ts+10
...@@ -20,6 +20,16 @@ export const CredentialScope = Context.Reference<string>(...@@ -20,6 +20,16 @@ export const CredentialScope = Context.Reference<string>(
20 },20 },
21);21);
2222
23/**
24 * Set by interactive callers (a user's read or write, not a background sweep). A host may let
25 * requests made under it spend a reserved quota and go through a rate-limit pause: a user acting
26 * on a change request should not be refused because a background read exhausted the quota.
27 */
28export const Interactive = Context.Reference<boolean>(
29 "@t3tools/source-control-core/server/SourceControlRateLimit/Interactive",
30 { defaultValue: () => false },
31);
32
23interface RateLimitKey {33interface RateLimitKey {
24 readonly provider: SourceControlProviderKind;34 readonly provider: SourceControlProviderKind;
25 readonly host: string;35 readonly host: string;
packages/source-control-github/src/server/GitHubApi.test.ts+2-2
...@@ -275,7 +275,7 @@ describe("GitHubApi", () => {...@@ -275,7 +275,7 @@ describe("GitHubApi", () => {
275 retryAt: reset * 1000,275 retryAt: reset * 1000,
276 });276 });
277 // A user's own request may spend the reserve, and GraphQL has a quota of its own.277 // A user's own request may spend the reserve, and GraphQL has a quota of its own.
278 yield* api.rest(read).pipe(Effect.provideService(GitHubApi.AllowGitHubReserve, true));278 yield* api.rest(read).pipe(Effect.provideService(SourceControlRateLimit.Interactive, true));
279 yield* api.graphql({ host: "github.com", operation: "x", query: "query { viewer { id } }" });279 yield* api.graphql({ host: "github.com", operation: "x", query: "query { viewer { id } }" });
280 expect(requests).toHaveLength(3);280 expect(requests).toHaveLength(3);
281 // The reset gives the background its quota back.281 // The reset gives the background its quota back.
...@@ -344,7 +344,7 @@ describe("GitHubApi", () => {...@@ -344,7 +344,7 @@ describe("GitHubApi", () => {
344 method: "PUT",344 method: "PUT",
345 path: "repos/acme/web/pulls/7/merge",345 path: "repos/acme/web/pulls/7/merge",
346 })346 })
347 .pipe(Effect.provideService(GitHubApi.AllowGitHubReserve, true));347 .pipe(Effect.provideService(SourceControlRateLimit.Interactive, true));
348 expect(merged.status).toBe(200);348 expect(merged.status).toBe(200);
349 expect(requests).toHaveLength(2);349 expect(requests).toHaveLength(2);
350 }).pipe(Effect.provide(layer));350 }).pipe(Effect.provide(layer));
packages/source-control-github/src/server/GitHubApi.ts+3-13
...@@ -34,16 +34,6 @@ export const PinnedGitHubCredential = Context.Reference<{...@@ -34,16 +34,6 @@ export const PinnedGitHubCredential = Context.Reference<{
34 defaultValue: () => null,34 defaultValue: () => null,
35});35});
3636
37/**
38 * Set by interactive callers (a user's read or write, not a background sweep). Requests made
39 * under it may spend the GraphQL reserve and go through a rate-limit pause: a user acting on a
40 * pull request should not be refused because a background read exhausted the quota.
41 */
42export const AllowGitHubReserve = Context.Reference<boolean>(
43 "@t3tools/source-control-github/server/GitHubApi/AllowGitHubReserve",
44 { defaultValue: () => false },
45);
46
47export class GitHubApiRequestError extends Schema.TaggedError<GitHubApiRequestError>()(37export class GitHubApiRequestError extends Schema.TaggedError<GitHubApiRequestError>()(
48 "GitHubApiRequestError",38 "GitHubApiRequestError",
49 { host: Schema.String, operation: Schema.String, cause: Schema.Defect() },39 { host: Schema.String, operation: Schema.String, cause: Schema.Defect() },
...@@ -137,7 +127,7 @@ export interface GitHubRestInput {...@@ -137,7 +127,7 @@ export interface GitHubRestInput {
137 readonly maxResponseBytes?: number;127 readonly maxResponseBytes?: number;
138 /** Defaults to 30 seconds; a whole pull request's patch may need longer. */128 /** Defaults to 30 seconds; a whole pull request's patch may need longer. */
139 readonly timeout?: Duration.Input;129 readonly timeout?: Duration.Input;
140 /** Overrides `AllowGitHubReserve` for this one request. */130 /** Overrides `SourceControlRateLimit.Interactive` for this one request. */
141 readonly allowReserve?: boolean;131 readonly allowReserve?: boolean;
142}132}
143133
...@@ -525,7 +515,7 @@ export const make = Effect.gen(function* () {...@@ -525,7 +515,7 @@ export const make = Effect.gen(function* () {
525 input.body === undefined515 input.body === undefined
526 ? withEtag516 ? withEtag
527 : withEtag.pipe(HttpClientRequest.bodyJsonUnsafe(input.body));517 : withEtag.pipe(HttpClientRequest.bodyJsonUnsafe(input.body));
528 return AllowGitHubReserve.pipe(518 return SourceControlRateLimit.Interactive.pipe(
529 Effect.flatMap((interactive) =>519 Effect.flatMap((interactive) =>
530 send({520 send({
531 host: input.host,521 host: input.host,
...@@ -545,7 +535,7 @@ export const make = Effect.gen(function* () {...@@ -545,7 +535,7 @@ export const make = Effect.gen(function* () {
545 const host = normalizeHost(input.host);535 const host = normalizeHost(input.host);
546 const { fingerprint } = yield* credential(host);536 const { fingerprint } = yield* credential(host);
547 const scope = (yield* SourceControlRateLimit.CredentialScope) || fingerprint;537 const scope = (yield* SourceControlRateLimit.CredentialScope) || fingerprint;
548 const allowReserve = input.allowReserve ?? (yield* AllowGitHubReserve);538 const allowReserve = input.allowReserve ?? (yield* SourceControlRateLimit.Interactive);
549 // The document, never its variables: user text (bodies, search terms) travels as variables.539 // The document, never its variables: user text (bodies, search terms) travels as variables.
550 yield* Effect.annotateCurrentSpan({540 yield* Effect.annotateCurrentSpan({
551 "github.operation": input.operation,541 "github.operation": input.operation,
packages/source-control-github/src/server/GitHubPullRequestApi.test.ts+4-3
...@@ -12,7 +12,6 @@ import * as Redacted from "effect/Redacted";...@@ -12,7 +12,6 @@ import * as Redacted from "effect/Redacted";
1212
13import { HttpClient, HttpClientResponse } from "effect/http";13import { HttpClient, HttpClientResponse } from "effect/http";
1414
15import { AllowGitHubReserve } from "./GitHubApi.ts";
16import * as GitHubApi from "./GitHubApi.ts";15import * as GitHubApi from "./GitHubApi.ts";
17import * as GitHubCredentials from "./GitHubCredentials.ts";16import * as GitHubCredentials from "./GitHubCredentials.ts";
18import * as GitHubQuota from "./GitHubQuota.ts";17import * as GitHubQuota from "./GitHubQuota.ts";
...@@ -78,7 +77,7 @@ const mockApi = Layer.effect(...@@ -78,7 +77,7 @@ const mockApi = Layer.effect(
78 const quota = yield* GitHubQuota.GitHubQuota;77 const quota = yield* GitHubQuota.GitHubQuota;
79 return GitHubApi.GitHubApi.of({78 return GitHubApi.GitHubApi.of({
80 graphql: (input) =>79 graphql: (input) =>
81 GitHubApi.AllowGitHubReserve.pipe(80 SourceControlRateLimit.Interactive.pipe(
82 Effect.flatMap((interactive) =>81 Effect.flatMap((interactive) =>
83 quota.admit(input.host, "graphql", {82 quota.admit(input.host, "graphql", {
84 allowReserve: input.allowReserve ?? interactive,83 allowReserve: input.allowReserve ?? interactive,
...@@ -3864,7 +3863,9 @@ layer("GitHubPullRequestApi.layer", (it) => {...@@ -3864,7 +3863,9 @@ layer("GitHubPullRequestApi.layer", (it) => {
3864 const error = yield* Effect.flip(cli.getPullRequestDetail(input));3863 const error = yield* Effect.flip(cli.getPullRequestDetail(input));
3865 expect(error._tag).toBe("SourceControlRateLimitPausedError");3864 expect(error._tag).toBe("SourceControlRateLimitPausedError");
3866 expect(mockedExecute).toHaveBeenCalledOnce();3865 expect(mockedExecute).toHaveBeenCalledOnce();
3867 yield* cli.getPullRequestDetail(input).pipe(Effect.provideService(AllowGitHubReserve, true));3866 yield* cli
3867 .getPullRequestDetail(input)
3868 .pipe(Effect.provideService(SourceControlRateLimit.Interactive, true));
3868 expect(mockedExecute).toHaveBeenCalledTimes(2);3869 expect(mockedExecute).toHaveBeenCalledTimes(2);
3869 }),3870 }),
3870 );3871 );
packages/source-control-github/src/server/GitHubPullRequestApi.ts+1-1
...@@ -1526,7 +1526,7 @@ export const make = Effect.gen(function* () {...@@ -1526,7 +1526,7 @@ export const make = Effect.gen(function* () {
15261526
1527 const getPullRequestDetail: GitHubPullRequestApi["Service"]["getPullRequestDetail"] = (input) => {1527 const getPullRequestDetail: GitHubPullRequestApi["Service"]["getPullRequestDetail"] = (input) => {
1528 const { owner, name } = parseRepositorySelector(input.repository);1528 const { owner, name } = parseRepositorySelector(input.repository);
1529 return GitHubApi.AllowGitHubReserve.pipe(1529 return SourceControlRateLimit.Interactive.pipe(
1530 Effect.flatMap((allowReserve) =>1530 Effect.flatMap((allowReserve) =>
1531 graphqlRead({1531 graphqlRead({
1532 allowReserve,1532 allowReserve,
packages/source-control-github/src/server/GitHubPullRequestProvider.test.ts+2-1
...@@ -7,6 +7,7 @@ import type { PullRequestReaction } from "@t3tools/contracts";...@@ -7,6 +7,7 @@ import type { PullRequestReaction } from "@t3tools/contracts";
77
8import { decodePullRequestDetailJson } from "./gitHubPullRequestJson.ts";8import { decodePullRequestDetailJson } from "./gitHubPullRequestJson.ts";
9import * as GitHubApi from "./GitHubApi.ts";9import * as GitHubApi from "./GitHubApi.ts";
10import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit";
10import * as GitHubPullRequestApi from "./GitHubPullRequestApi.ts";11import * as GitHubPullRequestApi from "./GitHubPullRequestApi.ts";
11import type { GitHubPullRequestCore } from "./gitHubPullRequestJson.ts";12import type { GitHubPullRequestCore } from "./gitHubPullRequestJson.ts";
12import { gitHubViewerPermissions, loginAvatarUrl, make } from "./GitHubPullRequestProvider.ts";13import { gitHubViewerPermissions, loginAvatarUrl, make } from "./GitHubPullRequestProvider.ts";
...@@ -725,7 +726,7 @@ describe("getViewerPermissions", () => {...@@ -725,7 +726,7 @@ describe("getViewerPermissions", () => {
725 Layer.mock(GitHubPullRequestApi.GitHubPullRequestApi)({726 Layer.mock(GitHubPullRequestApi.GitHubPullRequestApi)({
726 revalidateChecks: (_input, read) => read,727 revalidateChecks: (_input, read) => read,
727 getPullRequestDetail: () =>728 getPullRequestDetail: () =>
728 GitHubApi.AllowGitHubReserve.pipe(729 SourceControlRateLimit.Interactive.pipe(
729 Effect.tap((allowReserve) => Effect.sync(() => onDetail(allowReserve))),730 Effect.tap((allowReserve) => Effect.sync(() => onDetail(allowReserve))),
730 Effect.flatMap(() => detail),731 Effect.flatMap(() => detail),
731 ),732 ),
packages/source-control-github/src/server/GitHubPullRequestProvider.ts+4-2
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1import { removeAgentCredits } from "@t3tools/source-control-core/server/mergeMessage";
1import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
2import type {3import type {
3 PullRequestActor,4 PullRequestActor,
...@@ -7,7 +8,7 @@ import type {...@@ -7,7 +8,7 @@ import type {
7 PullRequestViewerPermissions,8 PullRequestViewerPermissions,
8} from "@t3tools/contracts";9} from "@t3tools/contracts";
910
10import * as GitHubApi from "./GitHubApi.ts";11import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit";
11import * as GitHubPullRequestApi from "./GitHubPullRequestApi.ts";12import * as GitHubPullRequestApi from "./GitHubPullRequestApi.ts";
12import {13import {
13 PullRequestProviderError,14 PullRequestProviderError,
...@@ -262,6 +263,7 @@ export const make = Effect.gen(function* () {...@@ -262,6 +263,7 @@ export const make = Effect.gen(function* () {
262 const provider: PullRequestProviderApi = {263 const provider: PullRequestProviderApi = {
263 kind: "github",264 kind: "github",
264 capabilities: CAPABILITIES,265 capabilities: CAPABILITIES,
266 mergeMessageRewrite: removeAgentCredits,
265 getRoutingIdentity: (input) =>267 getRoutingIdentity: (input) =>
266 cli.getRoutingIdentity(input).pipe(Effect.mapError(fail("routeIdentity"))),268 cli.getRoutingIdentity(input).pipe(Effect.mapError(fail("routeIdentity"))),
267 withVerifiedCredential: (input, use) =>269 withVerifiedCredential: (input, use) =>
...@@ -526,7 +528,7 @@ export const make = Effect.gen(function* () {...@@ -526,7 +528,7 @@ export const make = Effect.gen(function* () {
526 // comparison, so one read usually answers what used to take three. When that heavier read528 // comparison, so one read usually answers what used to take three. When that heavier read
527 // fails, the light access read still answers, withholding only update-branch.529 // fails, the light access read still answers, withholding only update-branch.
528 return cli.getPullRequestDetail(input).pipe(530 return cli.getPullRequestDetail(input).pipe(
529 Effect.provideService(GitHubApi.AllowGitHubReserve, true),531 Effect.provideService(SourceControlRateLimit.Interactive, true),
530 Effect.map((pullRequest) =>532 Effect.map((pullRequest) =>
531 gitHubViewerPermissions({533 gitHubViewerPermissions({
532 ...pullRequest.viewerAccess,534 ...pullRequest.viewerAccess,
packages/source-control-github/src/server/GitHubSourceControlProvider.ts+1-1
...@@ -963,7 +963,7 @@ export const make = Effect.gen(function* () {...@@ -963,7 +963,7 @@ export const make = Effect.gen(function* () {
963 },963 },
964 listChangeRequests: (input) =>964 listChangeRequests: (input) =>
965 // An open lookup is a user waiting on a status; the rest may be a background sweep.965 // An open lookup is a user waiting on a status; the rest may be a background sweep.
966 (input.state === "open" ? Effect.succeed(true) : GitHubApi.AllowGitHubReserve).pipe(966 (input.state === "open" ? Effect.succeed(true) : SourceControlRateLimit.Interactive).pipe(
967 Effect.flatMap((allowReserve) =>967 Effect.flatMap((allowReserve) =>
968 listByHead({968 listByHead({
969 cwd: input.cwd,969 cwd: input.cwd,