swadhinbiswas feat: OpenCodeHub v1.1.0 - Enterprise Features & Storage Adapters · 2/15/2026
719a1cb
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
/**
* CODEOWNERS Enforcement Library
* Validates that all required code owners have approved before merge
*/
import { getDatabase, schema } from "@/db";
import { eq, and, inArray } from "drizzle-orm";
import { logger } from "./logger";
import { parseCodeOwners, findOwnersForFiles, CODEOWNERS_PATHS } from "./codeowners";
import { getFileContent } from "./git";
interface CodeOwnerCheck {
canMerge: boolean;
missingApprovals: {
path: string;
requiredOwners: string[];
approvedBy: string[];
}[];
}
/**
* Check if all required code owners have approved a PR
*/
export async function checkCodeOwnerApprovals(
repositoryId: string,
pullRequestId: string,
changedFiles: string[]
): Promise<CodeOwnerCheck> {
const db = getDatabase();
// Get repository info
const repo = await db.query.repositories.findFirst({
where: eq(schema.repositories.id, repositoryId),
});
if (!repo) {
return { canMerge: true, missingApprovals: [] };
}
// Try to find CODEOWNERS file
let codeOwnersContent: string | null = null;
for (const path of CODEOWNERS_PATHS) {
try {
const result = await getFileContent(repositoryId, repo.defaultBranch, path);
if (result && result.content) {
codeOwnersContent = result.content;
break;
}
} catch {
// File doesn't exist at this path
}
}
if (!codeOwnersContent) {
// No CODEOWNERS file - allow merge
return { canMerge: true, missingApprovals: [] };
}
// Parse CODEOWNERS
const codeOwners = parseCodeOwners(codeOwnersContent);
// Get all required owners for changed files
const allOwners = findOwnersForFiles(codeOwners, changedFiles);
if (allOwners.length === 0) {
return { canMerge: true, missingApprovals: [] };
}
// Get PR approvals
const reviews = await db.query.pullRequestReviews.findMany({
where: and(
eq(schema.pullRequestReviews.pullRequestId, pullRequestId),
eq(schema.pullRequestReviews.state, "approved")
),
});
// Get usernames of approvers
const approverIds = reviews.map(r => r.reviewerId);
const approverUsernames = new Set<string>();
if (approverIds.length > 0) {
const approvers = await db.query.users.findMany({
where: inArray(schema.users.id, approverIds),
});
approvers.forEach(u => approverUsernames.add(u.username.toLowerCase()));
}
// Check if all owners have approved
const missingApprovals: CodeOwnerCheck["missingApprovals"] = [];
// Group by path for better error messages
const pathOwnerMap = new Map<string, Set<string>>();
for (const file of changedFiles) {
const owners = findOwnersForFiles(codeOwners, [file]);
if (owners.length > 0) {
pathOwnerMap.set(file, new Set(owners.map(o => o.replace("@", "").toLowerCase())));
}
}
for (const [path, requiredOwners] of pathOwnerMap) {
const requiredArray = Array.from(requiredOwners);
const hasApproval = requiredArray.some(owner => approverUsernames.has(owner));
if (!hasApproval) {
missingApprovals.push({
path,
requiredOwners: requiredArray.map(o => `@${o}`),
approvedBy: Array.from(approverUsernames),
});
}
}
const canMerge = missingApprovals.length === 0;
if (!canMerge) {
logger.info({
pullRequestId,
missingCount: missingApprovals.length,
}, "CODEOWNERS approval check failed");
}
return { canMerge, missingApprovals };
}
/**
* Get a summary of CODEOWNERS requirements for a PR
*/
export async function getCodeOwnersSummary(
repositoryId: string,
changedFiles: string[]
): Promise<{ path: string; owners: string[] }[]> {
const db = getDatabase();
const repo = await db.query.repositories.findFirst({
where: eq(schema.repositories.id, repositoryId),
});
if (!repo) return [];
// Try to find CODEOWNERS file
for (const path of CODEOWNERS_PATHS) {
try {
const result = await getFileContent(repositoryId, repo.defaultBranch, path);
if (result?.content) {
const codeOwners = parseCodeOwners(result.content);
return changedFiles.map(file => ({
path: file,
owners: findOwnersForFiles(codeOwners, [file]),
}));
}
} catch {
// Continue to next path
}
}
return [];
}