Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 1 | /** |
| 2 | * An environment's protection rules as the Environments settings page | |
| 3 | * shows and posts them: what a form says, and a rule set in a few words. | |
| 4 | */ | |
| 5 | import type { BranchPattern, Environment, EnvironmentChange, EnvironmentReviewer } from "@g1t/contracts"; | |
| 6 | ||
| 7 | /** What an environment may be called: lowercase letters, digits, `-` and `_`, up to 40. */ | |
| 8 | export const ENVIRONMENT_NAME = /^[a-z0-9_-]{1,40}$/; | |
| 9 | ||
| 10 | /** An environment's name as someone typed it, lowercased; null when it cannot be one. */ | |
| 11 | export function environmentName(value: string | null | undefined): string | null { | |
| 12 | const name = (value ?? "").trim().toLowerCase(); | |
| 13 | return ENVIRONMENT_NAME.test(name) ? name : null; | |
| 14 | } | |
| 15 | ||
| 16 | /** The limits the actions service holds rules to. */ | |
| 17 | export type EnvironmentLimits = { reviewers: number; waitMinutes: number }; | |
| 18 | ||
| 19 | const POLICIES: Environment["branchPolicy"][] = ["all", "protected", "selected"]; | |
| 20 | ||
| 21 | /** | |
| 22 | * The rules a settings form posts: `reviewerType`/`reviewerName` and | |
| 23 | * `patternType`/`patternName` pairs in order, blanks left out. Reviewers | |
| 24 | * past the limit are an error; numbers are kept within theirs. | |
| 25 | */ | |
| 26 | export function environmentFromForm(form: FormData, limits: EnvironmentLimits): { change: EnvironmentChange } | { error: string } { | |
| 27 | const types = form.getAll("reviewerType").map(String); | |
| 28 | const reviewers: EnvironmentReviewer[] = []; | |
| 29 | const seen = new Set<string>(); | |
| 30 | form.getAll("reviewerName").forEach((value, index) => { | |
| 31 | const name = String(value).trim().replace(/^@/, ""); | |
| 32 | const type = types[index] === "team" ? "team" : "user"; | |
| 33 | const key = `${type}:${name.toLowerCase()}`; | |
| 34 | if (!name || seen.has(key)) return; | |
| 35 | seen.add(key); | |
| 36 | reviewers.push({ type, name }); | |
| 37 | }); | |
| 38 | if (reviewers.length > limits.reviewers) { | |
| 39 | return { error: `An environment can have up to ${limits.reviewers} reviewers.` }; | |
| 40 | } | |
| 41 | ||
| 42 | const minutes = Math.trunc(Number(form.get("waitMinutes") ?? 0)); | |
| 43 | if (!Number.isFinite(minutes) || minutes < 0 || minutes > limits.waitMinutes) { | |
| 44 | return { error: `The wait timer is a number of minutes from 0 to ${limits.waitMinutes.toLocaleString("en-US")}.` }; | |
| 45 | } | |
| 46 | ||
| 47 | const policy = String(form.get("branchPolicy") ?? "all") as Environment["branchPolicy"]; | |
| 48 | const branchPolicy = POLICIES.includes(policy) ? policy : "all"; | |
| 49 | const patternTypes = form.getAll("patternType").map(String); | |
| 50 | const branchPatterns: BranchPattern[] = []; | |
| 51 | form.getAll("patternName").forEach((value, index) => { | |
| 52 | const name = String(value).trim(); | |
| 53 | if (name) branchPatterns.push({ name, type: patternTypes[index] === "tag" ? "tag" : "branch" }); | |
| 54 | }); | |
| 55 | if (branchPolicy === "selected" && branchPatterns.length === 0) { | |
| 56 | return { error: "Add at least one branch or tag pattern, or choose another option for deployment branches." }; | |
| 57 | } | |
| 58 | ||
| 59 | return { | |
| 60 | change: { | |
| 61 | reviewers, | |
| 62 | preventSelfReview: form.get("preventSelfReview") === "on", | |
| 63 | waitMinutes: minutes, | |
| 64 | branchPolicy, | |
| 65 | // Patterns only mean something for selected branches and tags. | |
| 66 | branchPatterns: branchPolicy === "selected" ? branchPatterns : [], | |
| 67 | adminsBypass: form.get("adminsBypass") === "on", | |
| 68 | }, | |
| 69 | }; | |
| 70 | } | |
| 71 | ||
| 72 | /** `30m`, `2h`, `1d 4h`: a wait timer in short. */ | |
| 73 | export function waitWords(minutes: number): string { | |
| 74 | if (minutes < 60) return `${minutes}m`; | |
| 75 | const days = Math.floor(minutes / 1440); | |
| 76 | const hours = Math.floor((minutes % 1440) / 60); | |
| 77 | const rest = minutes % 60; | |
| 78 | return [days && `${days}d`, hours && `${hours}h`, rest && `${rest}m`].filter(Boolean).join(" "); | |
| 79 | } | |
| 80 | ||
| 81 | /** A reviewer as the pages show one: a user by name, a team as @team. */ | |
| 82 | export function reviewerLabel(reviewer: EnvironmentReviewer): string { | |
| 83 | return reviewer.type === "team" ? `@${reviewer.name}` : reviewer.name; | |
| 84 | } | |
| 85 | ||
| 86 | /** An environment's rules in a few words each, for its row in the list. */ | |
| 87 | export function environmentSummary(environment: Environment): string[] { | |
| 88 | if (!environment.protected) return []; | |
| 89 | const parts: string[] = []; | |
| 90 | const { reviewers } = environment; | |
| 91 | if (reviewers.length > 0) { | |
| 92 | parts.push( | |
| 93 | reviewers.length <= 2 | |
| 94 | ? `Reviewers: ${reviewers.map(reviewerLabel).join(", ")}` | |
| 95 | : `${reviewers.length} reviewers`, | |
| 96 | ); | |
| 97 | } | |
| 98 | if (environment.waitMinutes > 0) parts.push(`Wait ${waitWords(environment.waitMinutes)}`); | |
| 99 | if (environment.branchPolicy === "protected") parts.push("Protected branches only"); | |
| 100 | if (environment.branchPolicy === "selected") { | |
| 101 | const count = environment.branchPatterns.length; | |
| 102 | parts.push(`${count} branch and tag pattern${count === 1 ? "" : "s"}`); | |
| 103 | } | |
| 104 | if (parts.length === 0) parts.push("No reviewers, wait timer or branch limits"); | |
| 105 | return parts; | |
| 106 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.