Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

branch-rules.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

branch-rules.tsBlame98 lines · 1 contributor
ec9e3e3Claude1/**
2 * Branch rules — higher-level wrapper around the existing `branch_protection`
3 * table + `pr_reviews` table. Provides `checkMergeEligible` for the merge
4 * route to call, and `getBranchRules` for the repo-settings UI.
5 *
6 * The underlying enforcement engine lives in `src/lib/branch-protection.ts`.
7 * This module exposes a simplified interface used by the route layer.
8 */
9
10import { eq } from "drizzle-orm";
11import { db } from "../db";
12import { branchProtection, prReviews } from "../db/schema";
13import {
14 matchProtection,
15 countHumanApprovals,
16} from "./branch-protection";
17
18export interface BranchRule {
19 id: string;
20 pattern: string; // branch name pattern (e.g. "main", "release/*")
21 requiredReviews: number; // 0 = no requirement
22 requireCodeownerReview: boolean;
23 dismissStaleReviews: boolean;
24}
25
26/**
27 * Return all branch-protection rules for a repository, mapped to the
28 * simplified BranchRule interface the UI and merge-check use.
29 */
30export async function getBranchRules(repoId: string): Promise<BranchRule[]> {
31 try {
32 const rows = await db
33 .select()
34 .from(branchProtection)
35 .where(eq(branchProtection.repositoryId, repoId));
36
37 return rows.map((r) => ({
38 id: r.id,
39 pattern: r.pattern,
40 requiredReviews: r.requiredApprovals,
41 requireCodeownerReview: r.requireHumanReview, // closest semantic match
42 dismissStaleReviews: r.dismissStaleReviews,
43 }));
44 } catch {
45 return [];
46 }
47}
48
49export interface MergeEligibleResult {
50 eligible: boolean;
51 reason?: string;
52 approvalCount: number;
53 requiredCount: number;
54}
55
56/**
57 * Check whether a PR is eligible to be merged, based on the branch-protection
58 * rules for `targetBranch`. This supplements (does not replace) the full gate
59 * check in the merge route — it specifically handles the human-review count
60 * requirement coming from `branch_protection.required_approvals`.
61 *
62 * Returns `eligible: true` when:
63 * - No rule matches the target branch, OR
64 * - The rule's `requiredApprovals` threshold is satisfied.
65 */
66export async function checkMergeEligible(
67 prId: string,
68 repoId: string,
69 targetBranch: string
70): Promise<MergeEligibleResult> {
71 try {
72 const rule = await matchProtection(repoId, targetBranch);
73
74 if (!rule || rule.requiredApprovals === 0) {
75 // Count anyway for informational display
76 const approvalCount = await countHumanApprovals(prId);
77 return { eligible: true, approvalCount, requiredCount: 0 };
78 }
79
80 const approvalCount = await countHumanApprovals(prId);
81 const requiredCount = rule.requiredApprovals;
82
83 if (approvalCount < requiredCount) {
84 return {
85 eligible: false,
86 reason: `This PR requires ${requiredCount} approval${requiredCount !== 1 ? "s" : ""} before merging. Currently: ${approvalCount} approval${approvalCount !== 1 ? "s" : ""}.`,
87 approvalCount,
88 requiredCount,
89 };
90 }
91
92 return { eligible: true, approvalCount, requiredCount };
93 } catch {
94 // On error, allow the merge to proceed — the full gate check is the
95 // primary enforcement mechanism.
96 return { eligible: true, approvalCount: 0, requiredCount: 0 };
97 }
98}