Skip to content
95 linesCodeBlameRaw
1import type { GuardrailSettings, RunKind, WorkflowDomain } from "@g1t/contracts";
2import { microsOf, money } from "./money.ts";
3
4/** What a level's form field means: inherit, or a choice of its own. */
5export type Tri = "inherit" | "on" | "off";
6
7export function tri(value: boolean | null | undefined): Tri {
8 return value == null ? "inherit" : value ? "on" : "off";
9}
10
11/** Lines of a textarea, trimmed, without blanks or repeats. */
12export function lines(value: FormDataEntryValue | null): string[] {
13 const seen = new Set<string>();
14 for (const line of String(value ?? "").split(/\r?\n/)) {
15 const trimmed = line.trim();
16 if (trimmed) seen.add(trimmed);
17 }
18 return [...seen];
19}
20
21/**
22 * Workflow-only domains, one per line, as
23 * `domain | workflows | environments`: the last two comma-separated, and
24 * either left out or empty for any. `api.cloudflare.com | deploy.yml |
25 * production` lets only deploy.yml's jobs in production reach it.
26 */
27export function workflowDomains(value: FormDataEntryValue | null): WorkflowDomain[] {
28 const list = (text: string | undefined) =>
29 (text ?? "")
30 .split(",")
31 .map((part) => part.trim())
32 .filter(Boolean);
33 const out: WorkflowDomain[] = [];
34 for (const line of lines(value)) {
35 const [domain, workflows, environments] = line.split("|").map((part) => part.trim());
36 if (!domain) continue;
37 out.push({ domain, workflows: list(workflows), environments: list(environments) });
38 }
39 return out;
40}
41
42/** A workflow-only domain as a line of the form's field. */
43export function workflowDomainLine(entry: WorkflowDomain): string {
44 const parts = [entry.domain, entry.workflows.join(", "), entry.environments.join(", ")];
45 while (parts.length > 1 && !parts[parts.length - 1]) parts.pop();
46 return parts.join(" | ");
47}
48
49/** A number from a field; empty means inherit. Not a number is kept, to be refused. */
50function amount(value: FormDataEntryValue | null): number | null {
51 const text = String(value ?? "").trim().replace(/^\$/, "");
52 if (!text) return null;
53 const number = Number(text);
54 return Number.isFinite(number) ? number : Number.NaN;
55}
56
57/** The kinds of run that still use a sandbox: checks, the merge queue and merge checks. */
58export const SANDBOX_RUN_KINDS = ["checks", "queue", "mergecheck"] as const satisfies readonly RunKind[];
59
60/** What one level's form says, as the settings that level keeps. */
61export function settingsFromForm(
62 form: FormData,
63 catalog: { registries: string[]; kinds: readonly RunKind[] },
64): GuardrailSettings {
65 const choice = (name: string): boolean | null => {
66 const value = form.get(name);
67 return value === "on" ? true : value === "off" ? false : null;
68 };
69 const minutes: Record<string, number> = {};
70 for (const kind of catalog.kinds) {
71 const cap = amount(form.get(`minutes:${kind}`));
72 if (cap != null) minutes[kind] = Number.isNaN(cap) ? 0 : Math.trunc(cap);
73 }
74 return {
75 restrictNetwork: choice("restrictNetwork"),
76 registries:
77 form.get("registries") === "custom"
78 ? catalog.registries.filter((id) => form.get(`registry:${id}`) === "on")
79 : null,
80 domains: lines(form.get("domains")),
81 workflowDomains: workflowDomains(form.get("workflowDomains")),
82 minutes,
83 };
84}
85
86/** A cost in dollars, or "no cap". */
87export function formatCap(usd: number | null | undefined): string {
88 return usd == null ? "no cap" : money(microsOf(usd));
89}
90
91/** How full a cap is, from 0 to 1, or null with no cap. */
92export function capShare(used: number | null | undefined, cap: number | null | undefined): number | null {
93 if (cap == null || cap <= 0 || used == null) return null;
94 return Math.min(1, Math.max(0, used / cap));
95}