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.
| ec9e3e3 | 1 | /** |
| 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 | ||
| 10 | import { eq } from "drizzle-orm"; | |
| 11 | import { db } from "../db"; | |
| 12 | import { branchProtection, prReviews } from "../db/schema"; | |
| 13 | import { | |
| 14 | matchProtection, | |
| 15 | countHumanApprovals, | |
| 16 | } from "./branch-protection"; | |
| 17 | ||
| 18 | export 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 | */ | |
| 30 | export 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 | ||
| 49 | export 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 | */ | |
| 66 | export 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 | } |