Swadhin Biswas feat(api): add runner, analytics, packages, and security services · 3/6/2026
28f7954
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import { and, eq } from "drizzle-orm";
import { schema } from "@/db";
import { resolveRepoPath } from "@/lib/git-storage";
import { CODEOWNERS_PATHS, parseCodeOwners, findOwnersForFile, expandOwnersToUsernames } from "@/lib/codeowners";
export async function checkCodeOwnerApprovalsForPR(
db: NodePgDatabase<typeof schema>,
pullRequestId: string
): Promise<{ ok: boolean; reason?: string }> {
const pr = await db.query.pullRequests.findFirst({
where: eq(schema.pullRequests.id, pullRequestId),
});
if (!pr) return { ok: false, reason: "Pull request not found" };
const repo = await db.query.repositories.findFirst({
where: eq(schema.repositories.id, pr.repositoryId),
});
if (!repo) return { ok: false, reason: "Repository not found" };
const localRepoPath = await resolveRepoPath(repo.diskPath);
const { getChangedFiles } = await import("./git");
const changedFiles = await getChangedFiles(localRepoPath, pr.baseBranch, pr.headBranch);
if (changedFiles.length === 0) return { ok: true };
const fs = await import("node:fs/promises");
const path = await import("node:path");
let codeOwnersContent: string | null = null;
for (const candidatePath of CODEOWNERS_PATHS) {
try {
codeOwnersContent = await fs.readFile(path.join(localRepoPath, candidatePath), "utf-8");
break;
} catch {
// continue
}
}
if (!codeOwnersContent) {
return { ok: true };
}
const codeOwners = parseCodeOwners(codeOwnersContent);
const approvals = await db.query.pullRequestReviews.findMany({
where: and(
eq(schema.pullRequestReviews.pullRequestId, pr.id),
eq(schema.pullRequestReviews.state, "approved")
),
orderBy: (reviews, { desc }) => [desc(reviews.submittedAt)],
});
const approverUserIds = approvals.map((r) => r.reviewerId);
if (approverUserIds.length === 0) {
return { ok: false, reason: "Code owner approval required" };
}
const approverUsers = await db.query.users.findMany({
where: (users, { inArray }) => inArray(users.id, approverUserIds),
});
const approverUsernames = new Set(approverUsers.map((u) => u.username));
for (const file of changedFiles) {
const owners = findOwnersForFile(codeOwners, file);
if (owners.length === 0) continue;
const expandedOwners = await expandOwnersToUsernames({
db,
repository: repo,
owners,
});
const hasOwnerApproval = Array.from(expandedOwners).some((owner) =>
approverUsernames.has(owner)
);
if (!hasOwnerApproval) {
return {
ok: false,
reason: `Missing Code Owner approval for ${file}`,
};
}
}
return { ok: true };
}