Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
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.

The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor1import type { User, Viewer } from "./identity";
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb972import type { RepoPath } from "./repos";
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor3import 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;
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor40 /** 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 GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.50 * 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.
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor53 */
54 detected: DetectedKind | null;
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.55 /**
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;
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor69};
70
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.71export 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"];
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor82
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.83/**
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
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor191export 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 GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.224 /** What the build found it to be and built with, once it has finished (see `DeploySettings`). */
225 detected?: DetectedKind | null;
226 framework?: string | null;
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor227 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 GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.266 /** 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. */
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor270 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
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor482export 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>>;
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor520}

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