Skip to content
Merged
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
13 changes: 5 additions & 8 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import {
listRepoLabels,
listRepoSyncSegments,
listRepoSyncStates,
summarizeRepoSyncOpenPullRequests,
listSignalSnapshots,
listPullRequests,
listRepositories,
Expand Down Expand Up @@ -827,12 +828,11 @@ export function createApp() {
const summary = await getRoleSummaryForIdentity(c.env, identity);
if (!summary.roles.some((role) => ["maintainer", "owner", "operator"].includes(role))) return c.json({ error: "insufficient_role" }, 403);

const [allRepositories, allInstallations, allHealth, allRateLimits, allSyncStates] = await Promise.all([
const [allRepositories, allInstallations, allHealth, allRateLimits] = await Promise.all([
listRepositories(c.env),
listInstallations(c.env),
listInstallationHealth(c.env),
listLatestGitHubRateLimitObservations(c.env, 20),
listRepoSyncStates(c.env),
]);
const scope = identity.kind === "session" && !summary.roles.includes("operator") ? await loadControlPanelAccessScope(c.env, identity.actor) : null;
const scopedRepoNames = new Set(scope?.repositoryFullNames.map((repo) => repo.toLowerCase()) ?? []);
Expand All @@ -846,13 +846,10 @@ export function createApp() {
? allHealth.filter((record) => scopedInstallationIds.has(record.installationId) || scopedAccountLogins.has(record.accountLogin.toLowerCase()))
: allHealth;
const rateLimits = scope ? allRateLimits.filter((record) => record.repoFullName !== undefined && record.repoFullName !== null && scopedRepoNames.has(record.repoFullName.toLowerCase())) : allRateLimits;
// Cached open-PR count is summed across ALL in-scope repos from sync state (a single query) so the
// headline metric is a true global count like its siblings. The per-repo PR fetch below is capped at
// Cached open-PR count is aggregated across ALL in-scope repos from sync state without using the
// capped sync-state listing that powers previews elsewhere. The per-repo PR fetch below is capped at
// 12 only to bound the `reviewability` preview list, not the metric.
const scopedRepoNameSet = new Set(repositories.map((repo) => repo.fullName.toLowerCase()));
const scopedSyncStates = allSyncStates.filter((state) => scopedRepoNameSet.has(state.repoFullName.toLowerCase()));
const totalOpenPullRequestsCached = scopedSyncStates.reduce((sum, state) => sum + Math.max(0, state.openPullRequestsCount), 0);
const reposWithOpenPullRequests = scopedSyncStates.filter((state) => state.openPullRequestsCount > 0).length;
const { totalOpenPullRequestsCached, reposWithOpenPullRequests } = await summarizeRepoSyncOpenPullRequests(c.env, repositories.map((repo) => repo.fullName));
const openPullRequests = (
await Promise.all(repositories.slice(0, 12).map((repo) => listOpenPullRequests(c.env, repo.fullName).then((rows) => rows.map((pull) => ({ repoFullName: repo.fullName, pull })))))
).flat();
Expand Down
32 changes: 31 additions & 1 deletion src/db/repositories.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { and, desc, eq, gte, not, or, sql, type SQL } from "drizzle-orm";
import { and, desc, eq, gte, inArray, not, or, sql, type SQL } from "drizzle-orm";
import { getDb } from "./client";
import {
advisories,
Expand Down Expand Up @@ -512,6 +512,36 @@ export async function listRepoSyncStates(env: Env): Promise<RepoSyncStateRecord[
return rows.map(toRepoSyncStateRecord);
}

export async function summarizeRepoSyncOpenPullRequests(env: Env, repoFullNames?: string[]): Promise<{ totalOpenPullRequestsCached: number; reposWithOpenPullRequests: number }> {
const db = getDb(env.DB);
const aggregate = async (repoNames?: string[]) => {
const query = db
.select({
totalOpenPullRequestsCached: sql<number>`coalesce(sum(case when ${repoSyncState.openPullRequestsCount} > 0 then ${repoSyncState.openPullRequestsCount} else 0 end), 0)`,
reposWithOpenPullRequests: sql<number>`coalesce(sum(case when ${repoSyncState.openPullRequestsCount} > 0 then 1 else 0 end), 0)`,
})
.from(repoSyncState);
const [row] = repoNames ? await query.where(inArray(sql`lower(${repoSyncState.repoFullName})`, repoNames)) : await query;
return {
totalOpenPullRequestsCached: Number(row?.totalOpenPullRequestsCached ?? 0),
reposWithOpenPullRequests: Number(row?.reposWithOpenPullRequests ?? 0),
};
};

if (repoFullNames === undefined) return aggregate();

const normalizedRepoNames = Array.from(new Set(repoFullNames.map((name) => name.toLowerCase())));
const summary = { totalOpenPullRequestsCached: 0, reposWithOpenPullRequests: 0 };
for (let index = 0; index < normalizedRepoNames.length; index += 450) {
const chunk = normalizedRepoNames.slice(index, index + 450);
if (chunk.length === 0) continue;
const chunkSummary = await aggregate(chunk);
summary.totalOpenPullRequestsCached += chunkSummary.totalOpenPullRequestsCached;
summary.reposWithOpenPullRequests += chunkSummary.reposWithOpenPullRequests;
}
return summary;
}

export async function upsertRepoSyncSegment(env: Env, segment: RepoSyncSegmentRecord): Promise<void> {
const db = getDb(env.DB);
await db
Expand Down
43 changes: 43 additions & 0 deletions test/integration/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1397,6 +1397,49 @@ describe("api routes", () => {
expect(body.metrics.find((metric) => metric.label === "Open PRs cached")?.value).toBe(8);
});

it("counts cached open PRs from sync states beyond the latest 500 rows", async () => {
const app = createApp();
const env = createTestEnv();

vi.setSystemTime(new Date("2026-05-27T00:00:00.000Z"));
await upsertRepositoryFromGitHub(env, { name: "oldest", full_name: "entrius/oldest", private: false, owner: { login: "entrius" }, default_branch: "main" });
await upsertRepoSyncState(env, {
repoFullName: "entrius/oldest",
status: "success",
sourceKind: "github",
primaryLanguage: "TypeScript",
defaultBranch: "main",
isPrivate: false,
openIssuesCount: 0,
openPullRequestsCount: 7,
recentMergedPullRequestsCount: 0,
warnings: [],
});

vi.setSystemTime(new Date("2026-05-28T00:00:00.000Z"));
for (let index = 0; index < 500; index += 1) {
const name = `newer-${index}`;
await upsertRepositoryFromGitHub(env, { name, full_name: `entrius/${name}`, private: false, owner: { login: "entrius" }, default_branch: "main" });
await upsertRepoSyncState(env, {
repoFullName: `entrius/${name}`,
status: "success",
sourceKind: "github",
primaryLanguage: "TypeScript",
defaultBranch: "main",
isPrivate: false,
openIssuesCount: 0,
openPullRequestsCount: 1,
recentMergedPullRequestsCount: 0,
warnings: [],
});
}

const res = await app.request("/v1/app/maintainer-dashboard", { headers: apiHeaders(env) }, env);
expect(res.status).toBe(200);
const body = (await res.json()) as { metrics: Array<{ label: string; value: number }> };
expect(body.metrics.find((metric) => metric.label === "Open PRs cached")?.value).toBe(507);
});

it("serves live app dashboards, digest subscriptions, commands, and extension context", async () => {
const app = createApp();
const env = createTestEnv({ ADMIN_GITHUB_LOGINS: "oktofeesh1,other", PRODUCT_USAGE_HASH_SALT: "usage-adoption-test-salt" });
Expand Down