Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions apps/server/src/git/GitManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import * as ServerSettings from "../serverSettings.ts";
import type { GitManagerServiceError } from "@t3tools/contracts";
import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts";
import { makeGitHubProjectAccount } from "../sourceControl/gitHubProjectAccount.ts";
import { detectPrTemplate } from "../sourceControl/PrTemplateDetection.ts";
import type { ChangeRequest } from "@t3tools/contracts";

Expand Down Expand Up @@ -2923,18 +2924,24 @@ export const make = Effect.gen(function* () {
},
);

// Everything that reaches the host runs as the checkout's project account.
const { actAs } = yield* makeGitHubProjectAccount;
return GitManager.of({
createWorktree,
localStatus,
remoteStatus,
status,
branchPullRequest,
remoteStatus: (input, options) => actAs(input, remoteStatus(input, options)),
status: (input) => actAs(input, status(input)),
branchPullRequest: (input, options) => actAs(input, branchPullRequest(input, options)),
invalidateLocalStatus,
invalidateRemoteStatus,
invalidateStatus,
resolvePullRequest,
preparePullRequestThread,
runStackedAction,
resolvePullRequest: (input) => actAs(input, resolvePullRequest(input)),
preparePullRequestThread: (input) => actAs(input, preparePullRequestThread(input)),
runStackedAction: (input, options) =>
actAs(
{ cwd: input.cwd, projectId: input.projectId ?? null },
runStackedAction(input, options),
),
subscribePullRequestStateChanges: PubSub.subscribe(pullRequestStateChanges).pipe(
Effect.map((subscription) => Stream.fromSubscription(subscription)),
),
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/pullRequest/GitHubPullRequestApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1753,6 +1753,8 @@ export const make = Effect.gen(function* () {
Context.getOrElse(context, GitHubApi.PinnedGitHubCredential, () => null)
?.credentialFingerprint ?? null,
Context.getOrElse(context, SourceControlRateLimit.CredentialScope, () => ""),
// A project with its own account is read with its own token, never batched with others.
Context.getOrElse(context, GitHubApi.GitHubAccount, () => null),
]),
resolver: (entries) => {
const [first] = entries;
Expand Down Expand Up @@ -1833,6 +1835,8 @@ export const make = Effect.gen(function* () {
Context.getOrElse(context, GitHubApi.PinnedGitHubCredential, () => null)
?.credentialFingerprint ?? null,
Context.getOrElse(context, SourceControlRateLimit.CredentialScope, () => ""),
// A project with its own account is read with its own token, never batched with others.
Context.getOrElse(context, GitHubApi.GitHubAccount, () => null),
]),
resolver: (entries) => {
const [first] = entries;
Expand Down
64 changes: 64 additions & 0 deletions apps/server/src/pullRequest/PullRequestService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
import * as KeyValueStore from "effect/persistence/KeyValueStore";
import { assert, it } from "@effect/vitest";
import { GitHubAccount } from "../sourceControl/GitHubApi.ts";
import * as Cause from "effect/Cause";
import * as Clock from "effect/Clock";
import * as Deferred from "effect/Deferred";
Expand Down Expand Up @@ -1279,6 +1280,69 @@ it.effect("tries another workspace on the same host for the viewer", () =>
}),
);

it.effect("runs a project with its own GitHub account as that account, apart from the rest", () =>
Effect.gen(function* () {
const searches: Array<{
readonly viewer: string;
readonly account: string | null;
readonly repositories: ReadonlyArray<string>;
}> = [];
const statReads: Array<{ readonly account: string | null; readonly count: number }> = [];
const service = yield* makeService({
settings: {
...DEFAULT_SERVER_SETTINGS,
projectSettingsOverrides: { ["w1" as ProjectId]: { githubAccount: "work" } },
},
projects: [
project({ id: "p1", title: "personal", workspaceRoot: "/p1", repository: "me/one" }),
project({ id: "p2", title: "other", workspaceRoot: "/p2", repository: "me/two" }),
project({ id: "w1", title: "work", workspaceRoot: "/w1", repository: "Acme/web" }),
],
providers: [
fakeProvider("github", {
getViewer: () =>
Effect.map(GitHubAccount, (account) => (account === null ? "personal" : account)),
listChangeRequestsAcross: (input) =>
Effect.map(GitHubAccount, (account) => {
searches.push({ viewer: input.viewer, account, repositories: input.repositories });
return {
items: input.repositories.map((repository, index) =>
batchedChangeRequest(index + 1, repository, "2026-07-02T00:00:00Z"),
),
truncated: false,
};
}),
listChangeRequestStats: (input) =>
Effect.map(GitHubAccount, (account) => {
statReads.push({ account, count: input.changeRequests.length });
return input.changeRequests.map((ref) => ({ ...ref, additions: 1, deletions: 1 }));
}),
}),
],
});
const result = yield* service.list({ state: "open", involvement: "all" });
assert.strictEqual(result.entries.length, 3);
assert.sameDeepMembers(searches, [
{ viewer: "personal", account: null, repositories: ["me/one", "me/two"] },
{ viewer: "work", account: "work", repositories: ["Acme/web"] },
]);
assert.strictEqual(result.viewers["github.com"], "personal");
assert.strictEqual(result.viewers["project:w1"], "work");

yield* service.listStats({
refs: result.entries.map(({ projectId, repository, number }) => ({
projectId,
repository,
number,
})),
});
assert.sameDeepMembers(statReads, [
{ account: null, count: 2 },
{ account: "work", count: 1 },
]);
}),
);

it.effect("routing verifies the current account on the requested host without caching it", () =>
Effect.gen(function* () {
let viewer = "first-account";
Expand Down
Loading
Loading