Skip to content
520 linesCodeBlameRaw

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.

What g1t can't do yet, and an open letter to Cloudflare1import type { User, Viewer } from "./identity";
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb972import type { RepoPath } from "./repos";
What g1t can't do yet, and an open letter to Cloudflare3import type { Result } from "./result";
4
5/**
6 * Deployments: a project's production, deployed from its default branch on
7 * every push, and a live preview of every branch with an open pull request.
8 * Apps run as Workers in a Workers for Platforms namespace, so an app no
9 * one visits costs nothing. A paid feature: the workspace turns it on with
10 * a monthly plan (see `Feature` in `./billing`), and each project then
11 * chooses for itself.
12 */
13
14/** The domain apps are served on. Never g1t.sh, so they share no cookies with it. */
15export const DEPLOYMENTS_DOMAIN = "g1t.page";
16
17/** A project, as deployments name it. */
18export type ProjectRef = { workspace: string; slug: string };
19
20/** A project's deployment settings. */
21export type DeploySettings = {
22 /** Whether this project deploys at all. Off until someone turns it on. */
23 enabled: boolean;
24 /** A preview for every branch with an open pull request. */
25 previews: boolean;
26 /** The default branch deployed to production on every push. */
27 production: boolean;
28 /** Runs instead of the project's own `build` script. */
29 buildCommand: string | null;
30 /** What to serve, for a static site; found by itself when null. */
31 outputDir: string | null;
32 /** A preview no one has visited in this many days is taken down. */
33 idleDays: number;
A project can build with a workflow. Turning on **Build with a workflow** under Settings, Deployments installs a managed workflow named `g1t` that calls g1t's reusable deploy workflow (`g1t/workflows/.g1t/workflows/deploy.yml@v1`, published from `workflows/` by the actions release) with the project's settings, and pushes and pull requests then start it rather than a build of Deployments' own: the same previews and production, at the same addresses, reported as `g1t / deploy`, now the check of the workflow's job, listed on the Actions page with its runs and logs. A pull request from a fork runs it with a read-only token, as on GitHub, and still gets its preview without the project's build secrets: a read-only job token may open a preview build only from a run of a managed workflow. Fork pull requests run the repository's managed workflows too. Its jobs are Actions time, at cost plus 20% on g1t's machines and free on a self-hosted runner. A workflow named `g1t` in `.g1t/workflows/` takes its place. The deployments and Actions guides say so.34 /**
35 * Whether it builds through the deploy workflow g1t manages for it, a
36 * workflow run on each push and pull request like any other, rather
37 * than through Deployments' own build.
38 */
39 viaWorkflow?: boolean;
What g1t can't do yet, and an open letter to Cloudflare40 /** Where production is served. */
41 productionUrl: string;
42 /**
43 * The project's own domain for production, once it is active: the first
44 * custom domain added that serves the app rather than redirecting. Null
45 * until one is.
46 */
47 primaryDomain: string | null;
48 /**
49 * What the last finished build found the project to be: a Workers
Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r950 * project (it has a Workers config), a static site its build wrote,
51 * plain HTML served as it is, or a container. Null until a build has
52 * finished. Read-only.
What g1t can't do yet, and an open letter to Cloudflare53 */
54 detected: DetectedKind | null;
Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r955 /**
56 * What the last finished build found it built with: a framework's id
57 * (`astro`…), or for a container `dockerfile` or the server g1t wrote a
58 * Dockerfile for (`flask`…). See `FRAMEWORK_NAMES`. Read-only.
59 */
60 framework?: string | null;
61 /** What it is built as: found from its files (`auto`), or as the project says. */
62 buildAs?: BuildAs;
63 /** Its Dockerfile, relative to the project, when not `Dockerfile` or `Containerfile`. */
64 dockerfile?: string | null;
65 /** The size its container runs at (`APP_INSTANCES`), when it is one. */
66 instanceType?: string;
67 /** Whether g1t.page runs containers here yet, and what each size costs. Read-only. */
68 containers?: ContainerOffer;
What g1t can't do yet, and an open letter to Cloudflare69};
70
Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r971export type DetectedKind = "workers" | "static" | "html" | "container";
72
73/**
74 * What a project is built as. `auto` finds it from its files: a Wrangler
75 * config is Workers, a framework that builds a static site or a Worker is
76 * that, a framework that needs a server or a project with a Dockerfile
77 * (or a server g1t can write one for) is a container.
78 */
79export type BuildAs = "auto" | "static" | "workers" | "docker";
80
81export const BUILD_AS: readonly BuildAs[] = ["auto", "static", "workers", "docker"];
What g1t can't do yet, and an open letter to Cloudflare82
Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r983/**
84 * A size a container app runs at: one of Cloudflare Containers' instance
85 * types. Memory and disk are paid for while it is awake, CPU as it is
86 * used, and its image can be as large as its disk.
87 */
88export type AppInstance = { type: string; vcpu: number; memoryGib: number; diskGb: number };
89
90/** Cloudflare Containers' instance types, smallest first. */
91export const APP_INSTANCES: readonly AppInstance[] = [
92 { type: "lite", vcpu: 1 / 16, memoryGib: 0.25, diskGb: 2 },
93 { type: "basic", vcpu: 1 / 4, memoryGib: 1, diskGb: 4 },
94 { type: "standard-1", vcpu: 1 / 2, memoryGib: 4, diskGb: 8 },
95 { type: "standard-2", vcpu: 1, memoryGib: 6, diskGb: 12 },
96 { type: "standard-3", vcpu: 2, memoryGib: 8, diskGb: 16 },
97 { type: "standard-4", vcpu: 4, memoryGib: 12, diskGb: 20 },
98];
99
100/** What a container app runs at unless its project says: the smallest. */
101export const DEFAULT_APP_INSTANCE = "lite";
102
103/** A container app sleeps after this long with no request, and costs nothing while asleep. */
104export const CONTAINER_SLEEP_MINUTES = 5;
105
106/** An instance type by name; the default one for anything else. */
107export function appInstance(type: string | null | undefined): AppInstance {
108 return APP_INSTANCES.find((instance) => instance.type === type) ?? APP_INSTANCES[0];
109}
110
111/** What container apps cost g1t, in millionths of a dollar, from the price book. */
112export type ContainerCosts = {
113 vcpuSecond: number;
114 memoryGibSecond: number;
115 diskGbSecond: number;
116 /** The Durable Object in front of each instance, billed while the instance runs. */
117 instanceSecond: number;
118 egressGb: number;
119};
120
121/**
122 * What one second awake of `instance` costs g1t at most: its memory, disk
123 * and Durable Object, and every vCPU busy. Cloudflare bills CPU as it is
124 * used, so an app that is mostly waiting costs g1t less; g1t charges the
125 * whole vCPU while it is awake, so it never sells below cost.
126 */
127export function containerSecondMicros(instance: AppInstance, costs: ContainerCosts): number {
128 return (
129 instance.vcpu * costs.vcpuSecond +
130 instance.memoryGib * costs.memoryGibSecond +
131 instance.diskGb * costs.diskGbSecond +
132 costs.instanceSecond
133 );
134}
135
136/** One size as Settings, Deployments shows it: what an hour awake is charged, the margin included. */
137export type ContainerPrice = AppInstance & { hourlyMicros: number };
138
139export type ContainerOffer = {
140 /** False until Docker deployments are switched on for g1t.page. */
141 available: boolean;
142 prices: ContainerPrice[];
143 /** What one GB a container sends is charged, the margin included. */
144 egressGbMicros: number;
145 sleepMinutes: number;
146};
147
148/**
149 * What a build found a project built with, by the id `g1t-runner detect`
150 * prints as `framework`, for people.
151 */
152export const FRAMEWORK_NAMES: Record<string, string> = {
153 vite: "Vite",
154 "vite-react": "React (Vite)",
155 "vite-vue": "Vue (Vite)",
156 "vite-svelte": "Svelte (Vite)",
157 "vite-solid": "Solid (Vite)",
158 "vite-preact": "Preact (Vite)",
159 astro: "Astro",
160 nextjs: "Next.js",
161 sveltekit: "SvelteKit",
162 nuxt: "Nuxt",
163 "react-router": "React Router",
164 remix: "Remix",
165 gatsby: "Gatsby",
166 docusaurus: "Docusaurus",
167 eleventy: "Eleventy",
168 angular: "Angular",
169 "create-react-app": "Create React App",
170 hugo: "Hugo",
171 jekyll: "Jekyll",
172 mdbook: "mdBook",
173 dockerfile: "Dockerfile",
174 node: "Node.js",
175 django: "Django",
176 flask: "Flask",
177 fastapi: "FastAPI",
178 python: "Python",
179 go: "Go",
180 rails: "Rails",
181 rack: "Rack",
182 laravel: "Laravel",
183 php: "PHP",
184};
185
186/** A framework id a build reported, if it is one g1t knows. */
187export function frameworkId(value: unknown): string | null {
188 return typeof value === "string" && Object.hasOwn(FRAMEWORK_NAMES, value) ? value : null;
189}
190
What g1t can't do yet, and an open letter to Cloudflare191export type DeployKind = "preview" | "production";
192
193export type DeployStatus =
194 /** Waiting for a sandbox. */
195 | "queued"
196 | "building"
197 /** What its app serves now. */
198 | "ready"
199 /** Built and served, until a newer build of the same app replaced it. */
200 | "replaced"
201 /** Built and served, until its app was taken down. */
202 | "down"
203 | "failed"
204 /** Not built: the workspace's plan is off, or the build was replaced. */
205 | "skipped";
206
207/** One build of one commit, and where it went. */
208export type Deployment = {
209 id: string;
210 kind: DeployKind;
211 /** For a preview: the branch, or `pr-<n>` for a pull request from a fork. */
212 branch: string | null;
213 /** For a preview: its pull request. */
214 number: number | null;
215 commit: string;
216 status: DeployStatus;
217 url: string;
218 /** Why it failed or was skipped. */
219 error: string | null;
220 /** What the build could not provide, such as bindings not provisioned yet. */
221 warnings: string[];
222 /** How long the build ran, in seconds; charged at the container price. */
223 buildSeconds: number | null;
Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9224 /** What the build found it to be and built with, once it has finished (see `DeploySettings`). */
225 detected?: DetectedKind | null;
226 framework?: string | null;
What g1t can't do yet, and an open letter to Cloudflare227 createdBy: string;
228 /** RFC 3339. */
229 createdAt: string;
230 finishedAt: string | null;
231};
232
233/** An app that is up: production, or one branch's preview. */
234export type LiveApp = {
235 kind: DeployKind;
236 branch: string | null;
237 number: number | null;
238 url: string;
239 commit: string;
240 /** RFC 3339: when it was last deployed. */
241 deployedAt: string;
242};
243
244/** One project at a glance, for the workspace's page. */
245export type ProjectDeploys = {
246 slug: string;
247 enabled: boolean;
248 production: LiveApp | null;
249 previews: number;
250 /** The newest build, whatever its status. */
251 latest: Deployment | null;
252};
253
254/** What a workspace's apps used this month. Every request, CPU millisecond and build second is metered; apps are not. */
255export type DeployUsage = {
256 /** `YYYY-MM`. */
257 month: string;
258 requests: number;
259 cpuMs: number;
260 /** Apps up now (production and previews), and the most at once this month: for information, never charged. */
261 apps: number;
262 peakApps: number;
263 buildSeconds: number;
264 /** Charged so far this month for builds, in millionths of a dollar. */
265 buildMicros: number;
Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9266 /** Seconds container apps have been awake this month, and what that has cost g1t so far (charged when the month closes). */
267 containerSeconds?: number;
268 containerMicros?: number;
Merge branch 'worktree-agent-ab9543c492a7ed481' into spend-guardrails269 /** RFC 3339: when requests and CPU time last went up. */
What g1t can't do yet, and an open letter to Cloudflare270 countedAt: string | null;
271};
272
273/** Where custom domains point: a hostname on g1t.page that routes to the dispatcher. */
274export const CUSTOM_DOMAIN_TARGET = "domains.g1t.page";
275
276/**
277 * A custom domain's state: `pending` until its DNS points at g1t (or its
278 * ownership record is found), `verifying` while its certificate is
279 * issued, then `active`. `failed` says why in `error`; `removing` is on
280 * its way out.
281 */
282export type DomainStatus = "pending" | "verifying" | "active" | "failed" | "removing";
283
284/** A DNS record the domain's owner adds at their DNS provider. */
285export type DomainRecord = {
286 /** `ALIAS` stands for a flattened CNAME at the apex, whatever the provider calls it. */
287 type: "CNAME" | "TXT" | "ALIAS";
288 /** The record's full name. */
289 name: string;
290 value: string;
291 /** What it is for, in a few words. */
292 purpose: string;
293};
294
295/** A hostname of the project's own, serving its production. */
296export type Domain = {
297 id: string;
298 hostname: string;
299 /** What it serves: `production`. */
300 target: "production";
301 status: DomainStatus;
302 /** Cloudflare's state for its certificate, as given. */
303 sslStatus: string | null;
304 /** Whether it is a registrable domain itself (`example.com`), which needs a flattened CNAME. */
305 apex: boolean;
306 /** Every record to add: where traffic goes, then any Cloudflare asks for. */
307 records: DomainRecord[];
308 /** A hostname this one redirects to (308, path and query kept), for a www/apex pair. */
309 redirectTo: string | null;
310 /** Why it is not active yet, or failed. */
311 error: string | null;
312 createdBy: string;
313 createdAt: string;
314 verifiedAt: string | null;
315};
316
317export type ProjectDomains = {
318 domains: Domain[];
319 /** The hostname every domain points at. */
320 target: string;
321 /** False until custom domains are switched on for g1t.page; `notice` says so. */
322 available: boolean;
323 notice: string | null;
324 /** What one custom domain costs a month on the plan, in millionths of a dollar: g1t's cost plus 20%. */
325 monthlyMicros: number;
326 /** The workspace's custom domains now. */
327 used: number;
328};
329
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97330// ---- A repository's deployments, wherever they run ---------------------
331//
332// Every deployment of a repository, in one model: g1t.page builds (read
333// from the builds above, never copied), deployments g1t Actions makes for
334// a job with an `environment:`, and deployments any other system reports
335// through the API. These travel in `snake_case` between services too, as
336// the API shows them, so a deployment's `payload` reaches the API with its
337// keys as they were given.
338
339/** Where a deployment is, as its latest status says. */
340export type DeploymentState = "queued" | "in_progress" | "success" | "failure" | "error" | "inactive";
341
342export const DEPLOYMENT_STATES: readonly DeploymentState[] = ["queued", "in_progress", "success", "failure", "error", "inactive"];
343
344/**
345 * What made a deployment: `api` (reported with a token), `actions` (a g1t
346 * Actions job with an `environment:`) or `g1t_page` (a build on g1t.page).
347 */
348export type DeploymentSource = "api" | "actions" | "g1t_page";
349
350/** One deployment of one commit to one environment. */
351export type RepoDeployment = {
352 /** `dep_…` for a reported deployment, `dpl_…` for a g1t.page build. */
353 id: string;
354 /** Such as `production`, `staging` or `preview`. */
355 environment: string;
356 /** The branch, tag or commit asked for. */
357 ref: string;
358 sha: string;
359 /** What kind of deployment: `deploy` unless given, such as `deploy:migrations`. */
360 task: string;
361 description: string | null;
362 /** Whatever the reporter attached, as given. */
363 payload: Record<string, unknown>;
364 /** An environment that goes away, such as a pull request's preview. */
365 transient_environment: boolean;
366 /** An environment people use directly. */
367 production_environment: boolean;
368 /** Its latest status's state. */
369 state: DeploymentState;
370 /** Where it is served, from its latest status that gave one. */
371 environment_url: string | null;
372 /** Where its output can be read, from its latest status that gave one. */
373 log_url: string | null;
374 /** The username that created it, or `g1t`. */
375 creator: string;
376 source: DeploymentSource;
377 /** For `actions`: the workflow run, and its address on the site. */
378 run_id: string | null;
379 run_url: string | null;
380 /** For `g1t_page`: the project it built, and a preview's pull request. */
381 project: string | null;
382 number: number | null;
383 /** RFC 3339. */
384 created_at: string;
385 /** RFC 3339: its latest status. */
386 updated_at: string;
387};
388
389/** One status of a deployment: what it said, and when. */
390export type DeploymentStatus = {
391 id: string;
392 deployment_id: string;
393 state: DeploymentState;
394 description: string | null;
395 environment_url: string | null;
396 log_url: string | null;
397 creator: string;
398 /** RFC 3339. */
399 created_at: string;
400};
401
402/** A deployment with every status it has had, oldest first. */
403export type DeploymentDetail = RepoDeployment & { statuses: DeploymentStatus[] };
404
405/** An environment: a name deployments go to, made by the first. */
406export type DeploymentEnvironment = {
407 name: string;
408 /** Where its current deployment is served. */
409 url: string | null;
410 production_environment: boolean;
411 transient_environment: boolean;
412 /** How many deployments it has had. */
413 deployments_count: number;
414 /** Its newest deployment, whatever its state. */
415 latest: RepoDeployment | null;
416 /** The newest deployment that succeeded and is still active: what it serves. */
417 current: RepoDeployment | null;
418 /** RFC 3339: its newest deployment's latest status. */
419 updated_at: string;
420};
421
422/** A repository's environments, production first, and its count of deployments. */
423export type DeploymentEnvironments = {
424 /** Deployments across every environment. */
425 total_count: number;
426 environments: DeploymentEnvironment[];
427};
428
429/** Which deployments to list. Every field narrows the list. */
430export type DeploymentFilter = {
431 environment?: string | null;
432 ref?: string | null;
433 sha?: string | null;
434 task?: string | null;
435 state?: DeploymentState | null;
436 source?: DeploymentSource | null;
437 creator?: string | null;
438 /** From 1. */
439 page?: number | null;
440 /** 1 to 100; 30 unless given. */
441 per_page?: number | null;
442};
443
444/** One page of deployments, newest first. */
445export type DeploymentPage = {
446 deployments: RepoDeployment[];
447 total_count: number;
448 page: number;
449 per_page: number;
450};
451
452/** What `create_deployment` takes, as the API does. */
453export type NewDeployment = {
454 ref: string;
455 /** The commit; resolved from `ref` when left out. */
456 sha?: string | null;
457 environment?: string | null;
458 task?: string | null;
459 description?: string | null;
460 payload?: Record<string, unknown> | null;
461 transient_environment?: boolean | null;
462 production_environment?: boolean | null;
463 /** The state of its first status: `queued` unless given. */
464 state?: DeploymentState | null;
465 environment_url?: string | null;
466 log_url?: string | null;
467};
468
469/** What `create_deployment_status` takes. */
470export type NewDeploymentStatus = {
471 state: DeploymentState;
472 description?: string | null;
473 environment_url?: string | null;
474 log_url?: string | null;
475 /**
476 * On a success: the environment's older deployments that succeeded get
477 * an `inactive` status. True unless given.
478 */
479 auto_inactive?: boolean | null;
480};
481
What g1t can't do yet, and an open letter to Cloudflare482export interface DeploymentsApi {
483 /** Members of the workspace only. */
484 settings(project: ProjectRef, viewer: Viewer): Promise<Result<DeploySettings>>;
485 /** Members only. Turning deployments on needs the workspace's plan. */
486 updateSettings(actor: User, project: ProjectRef, changes: Partial<DeploySettings>): Promise<Result<DeploySettings>>;
487 /** The newest builds first, and what is up now. Members only. */
488 list(project: ProjectRef, viewer: Viewer): Promise<Result<{ deployments: Deployment[]; live: LiveApp[] }>>;
489 /** One build, with its log. Members only. */
490 get(project: ProjectRef, id: string, viewer: Viewer): Promise<Result<Deployment & { log: string | null }>>;
491 /** Builds production (`branch` null), or a branch's preview, again from its head. */
492 redeploy(actor: User, project: ProjectRef, branch: string | null): Promise<Result<Deployment>>;
493 /** Takes production (`branch` null), or a branch's preview, down now. */
494 takeDown(actor: User, project: ProjectRef, branch: string | null): Promise<Result<true>>;
495 /** Every project of a workspace at a glance. Members only. */
496 overview(workspace: string, viewer: Viewer): Promise<Result<ProjectDeploys[]>>;
497 /** What the workspace's apps used this month. Members only. */
498 usage(workspace: string, viewer: Viewer): Promise<Result<DeployUsage>>;
499 /** The project's custom domains. Members only. */
500 domains(project: ProjectRef, viewer: Viewer): Promise<Result<ProjectDomains>>;
501 /**
502 * Adds a custom domain for production. With `twin`, its www or apex twin
503 * is added too, redirecting to it. Members only; needs the Deployments plan.
504 */
505 addDomain(actor: User, project: ProjectRef, hostname: string, options?: { twin?: boolean }): Promise<Result<Domain[]>>;
506 /** Removes a custom domain, and any domain redirecting to it. Members only. */
507 removeDomain(actor: User, project: ProjectRef, id: string): Promise<Result<true>>;
508 /** Asks Cloudflare to check the domain again now. Members only. */
509 refreshDomain(actor: User, project: ProjectRef, id: string): Promise<Result<Domain>>;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97510 /** A repository's deployments, newest first, filtered. Anyone who can read it. */
511 repoDeployments(repo: RepoPath, viewer: Viewer, filter?: DeploymentFilter): Promise<Result<DeploymentPage>>;
512 /** One deployment with its statuses, oldest first. Anyone who can read the repository. */
513 repoDeployment(repo: RepoPath, id: string, viewer: Viewer): Promise<Result<DeploymentDetail>>;
514 /** A repository's environments with their current and latest deployments. Anyone who can read it. */
515 environments(repo: RepoPath, viewer: Viewer): Promise<Result<DeploymentEnvironments>>;
516 /** Reports a deployment. Takes the Write role. */
517 createDeployment(actor: User, repo: RepoPath, input: NewDeployment): Promise<Result<DeploymentDetail>>;
518 /** Adds a status to a reported deployment. Takes the Write role. */
519 createDeploymentStatus(actor: User, repo: RepoPath, id: string, input: NewDeploymentStatus): Promise<Result<DeploymentStatus>>;
What g1t can't do yet, and an open letter to Cloudflare520}

This file's history is long; its oldest lines are credited to the oldest commit read.