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.

549 linesCodeBlameRaw
1import type { User, Viewer } from "./identity";
2import type { RepoPath } from "./repos";
3import 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;
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;
40 /** 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
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.
53 */
54 detected: DetectedKind | null;
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;
69};
70
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"];
82
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 instance.vcpu * costs.vcpuSecond + instance.memoryGib * costs.memoryGibSecond + instance.diskGb * costs.diskGbSecond + costs.instanceSecond;
129}
130
131/** One size as Settings, Deployments shows it: what an hour awake is charged, the margin included. */
132export type ContainerPrice = AppInstance & { hourlyMicros: number };
133
134export type ContainerOffer = {
135 /** False until Docker deployments are switched on for g1t.page. */
136 available: boolean;
137 prices: ContainerPrice[];
138 /** What one GB a container sends is charged, the margin included. */
139 egressGbMicros: number;
140 sleepMinutes: number;
141};
142
143/**
144 * What a build found a project built with, by the id `g1t-runner detect`
145 * prints as `framework`, for people.
146 */
147export const FRAMEWORK_NAMES: Record<string, string> = {
148 vite: "Vite",
149 "vite-react": "React (Vite)",
150 "vite-vue": "Vue (Vite)",
151 "vite-svelte": "Svelte (Vite)",
152 "vite-solid": "Solid (Vite)",
153 "vite-preact": "Preact (Vite)",
154 astro: "Astro",
155 nextjs: "Next.js",
156 sveltekit: "SvelteKit",
157 nuxt: "Nuxt",
158 "react-router": "React Router",
159 remix: "Remix",
160 gatsby: "Gatsby",
161 docusaurus: "Docusaurus",
162 eleventy: "Eleventy",
163 angular: "Angular",
164 "create-react-app": "Create React App",
165 hugo: "Hugo",
166 jekyll: "Jekyll",
167 mdbook: "mdBook",
168 dockerfile: "Dockerfile",
169 node: "Node.js",
170 django: "Django",
171 flask: "Flask",
172 fastapi: "FastAPI",
173 python: "Python",
174 go: "Go",
175 rails: "Rails",
176 rack: "Rack",
177 laravel: "Laravel",
178 php: "PHP",
179 rust: "Rust",
180};
181
182/** A framework id a build reported, if it is one g1t knows. */
183export function frameworkId(value: unknown): string | null {
184 return typeof value === "string" && Object.hasOwn(FRAMEWORK_NAMES, value) ? value : null;
185}
186
187export type DeployKind = "preview" | "production";
188
189export type DeployStatus =
190 /** Waiting for a sandbox. */
191 | "queued"
192 | "building"
193 /** What its app serves now. */
194 | "ready"
195 /** Built and served, until a newer build of the same app replaced it. */
196 | "replaced"
197 /** Built and served, until its app was taken down. */
198 | "down"
199 | "failed"
200 /** Not built: the workspace's plan is off, or the build was replaced. */
201 | "skipped";
202
203/** One build of one commit, and where it went. */
204export type Deployment = {
205 id: string;
206 kind: DeployKind;
207 /** For a preview: the branch, or `pr-<n>` for a pull request from a fork. */
208 branch: string | null;
209 /** For a preview: its pull request. */
210 number: number | null;
211 commit: string;
212 status: DeployStatus;
213 url: string;
214 /** Why it failed or was skipped. */
215 error: string | null;
216 /** What the build could not provide, such as bindings not provisioned yet. */
217 warnings: string[];
218 /** How long the build ran, in seconds; charged at the container price. */
219 buildSeconds: number | null;
220 /** What the build found it to be and built with, once it has finished (see `DeploySettings`). */
221 detected?: DetectedKind | null;
222 framework?: string | null;
223 createdBy: string;
224 /** RFC 3339. */
225 createdAt: string;
226 finishedAt: string | null;
227};
228
229/** An app that is up: production, or one branch's preview. */
230export type LiveApp = {
231 kind: DeployKind;
232 branch: string | null;
233 number: number | null;
234 url: string;
235 commit: string;
236 /** RFC 3339: when it was last deployed. */
237 deployedAt: string;
238};
239
240/** One project at a glance, for the workspace's page. */
241export type ProjectDeploys = {
242 slug: string;
243 enabled: boolean;
244 production: LiveApp | null;
245 previews: number;
246 /** The newest build, whatever its status. */
247 latest: Deployment | null;
248};
249
250/** What a workspace's apps used this month. Every request, CPU millisecond and build second is metered; apps are not. */
251export type DeployUsage = {
252 /** `YYYY-MM`. */
253 month: string;
254 requests: number;
255 cpuMs: number;
256 /** Apps up now (production and previews), and the most at once this month: for information, never charged. */
257 apps: number;
258 peakApps: number;
259 buildSeconds: number;
260 /** Charged so far this month for builds, in millionths of a dollar. */
261 buildMicros: number;
262 /** Seconds container apps have been awake this month, and what that has cost g1t so far (charged when the month closes). */
263 containerSeconds?: number;
264 containerMicros?: number;
265 /** RFC 3339: when requests and CPU time last went up. */
266 countedAt: string | null;
267};
268
269/** Where custom domains point: a hostname on g1t.page that routes to the dispatcher. */
270export const CUSTOM_DOMAIN_TARGET = "domains.g1t.page";
271
272/**
273 * A custom domain's state: `pending` until its DNS points at g1t (or its
274 * ownership record is found), `verifying` while its certificate is
275 * issued, then `active`. `failed` says why in `error`; `removing` is on
276 * its way out.
277 */
278export type DomainStatus = "pending" | "verifying" | "active" | "failed" | "removing";
279
280/** A DNS record the domain's owner adds at their DNS provider. */
281export type DomainRecord = {
282 /** `ALIAS` stands for a flattened CNAME at the apex, whatever the provider calls it. */
283 type: "CNAME" | "TXT" | "ALIAS";
284 /** The record's full name. */
285 name: string;
286 value: string;
287 /** What it is for, in a few words. */
288 purpose: string;
289};
290
291/** A hostname of the project's own, serving its production. */
292export type Domain = {
293 id: string;
294 hostname: string;
295 /** What it serves: `production`. */
296 target: "production";
297 status: DomainStatus;
298 /** Cloudflare's state for its certificate, as given. */
299 sslStatus: string | null;
300 /** Whether it is a registrable domain itself (`example.com`), which needs a flattened CNAME. */
301 apex: boolean;
302 /** Every record to add: where traffic goes, then any Cloudflare asks for. */
303 records: DomainRecord[];
304 /** A hostname this one redirects to (308, path and query kept), for a www/apex pair. */
305 redirectTo: string | null;
306 /** Why it is not active yet, or failed. */
307 error: string | null;
308 createdBy: string;
309 createdAt: string;
310 verifiedAt: string | null;
311};
312
313export type ProjectDomains = {
314 domains: Domain[];
315 /** The hostname every domain points at. */
316 target: string;
317 /** False until custom domains are switched on for g1t.page; `notice` says so. */
318 available: boolean;
319 notice: string | null;
320 /** What one custom domain costs a month on the plan, in millionths of a dollar: g1t's cost plus 20%. */
321 monthlyMicros: number;
322 /** The workspace's custom domains now. */
323 used: number;
324};
325
326// ---- A repository's deployments, wherever they run ---------------------
327//
328// Every deployment of a repository, in one model: g1t.page builds (read
329// from the builds above, never copied), deployments g1t Actions makes for
330// a job with an `environment:`, and deployments any other system reports
331// through the API. These travel in `snake_case` between services too, as
332// the API shows them, so a deployment's `payload` reaches the API with its
333// keys as they were given.
334
335/** Where a deployment is, as its latest status says. */
336export type DeploymentState = "queued" | "in_progress" | "success" | "failure" | "error" | "inactive";
337
338export const DEPLOYMENT_STATES: readonly DeploymentState[] = ["queued", "in_progress", "success", "failure", "error", "inactive"];
339
340/**
341 * What made a deployment: `api` (reported with a token), `actions` (a g1t
342 * Actions job with an `environment:`) or `g1t_page` (a build on g1t.page).
343 */
344export type DeploymentSource = "api" | "actions" | "g1t_page";
345
346/** One deployment of one commit to one environment. */
347export type RepoDeployment = {
348 /** `dep_…` for a reported deployment, `dpl_…` for a g1t.page build. */
349 id: string;
350 /** Such as `production`, `staging` or `preview`. */
351 environment: string;
352 /** The branch, tag or commit asked for. */
353 ref: string;
354 sha: string;
355 /** What kind of deployment: `deploy` unless given, such as `deploy:migrations`. */
356 task: string;
357 description: string | null;
358 /** Whatever the reporter attached, as given. */
359 payload: Record<string, unknown>;
360 /** An environment that goes away, such as a pull request's preview. */
361 transient_environment: boolean;
362 /** An environment people use directly. */
363 production_environment: boolean;
364 /** Its latest status's state. */
365 state: DeploymentState;
366 /** Where it is served, from its latest status that gave one. */
367 environment_url: string | null;
368 /** Where its output can be read, from its latest status that gave one. */
369 log_url: string | null;
370 /** The username that created it, or `g1t`. */
371 creator: string;
372 source: DeploymentSource;
373 /** For `actions`: the workflow run, and its address on the site. */
374 run_id: string | null;
375 run_url: string | null;
376 /** For `g1t_page`: the project it built, and a preview's pull request. */
377 project: string | null;
378 number: number | null;
379 /** RFC 3339. */
380 created_at: string;
381 /** RFC 3339: its latest status. */
382 updated_at: string;
383};
384
385/** One status of a deployment: what it said, and when. */
386export type DeploymentStatus = {
387 id: string;
388 deployment_id: string;
389 state: DeploymentState;
390 description: string | null;
391 environment_url: string | null;
392 log_url: string | null;
393 creator: string;
394 /** RFC 3339. */
395 created_at: string;
396};
397
398/** A deployment with every status it has had, oldest first. */
399export type DeploymentDetail = RepoDeployment & { statuses: DeploymentStatus[] };
400
401/** An environment: a name deployments go to, made by the first. */
402export type DeploymentEnvironment = {
403 name: string;
404 /** Where its current deployment is served. */
405 url: string | null;
406 production_environment: boolean;
407 transient_environment: boolean;
408 /** How many deployments it has had. */
409 deployments_count: number;
410 /** Its newest deployment, whatever its state. */
411 latest: RepoDeployment | null;
412 /** The newest deployment that succeeded and is still active: what it serves. */
413 current: RepoDeployment | null;
414 /** RFC 3339: its newest deployment's latest status. */
415 updated_at: string;
416};
417
418/** A repository's environments, production first, and its count of deployments. */
419export type DeploymentEnvironments = {
420 /** Deployments across every environment. */
421 total_count: number;
422 environments: DeploymentEnvironment[];
423};
424
425/** Which deployments to list. Every field narrows the list. */
426export type DeploymentFilter = {
427 environment?: string | null;
428 ref?: string | null;
429 sha?: string | null;
430 task?: string | null;
431 state?: DeploymentState | null;
432 source?: DeploymentSource | null;
433 creator?: string | null;
434 /** From 1. */
435 page?: number | null;
436 /** 1 to 100; 30 unless given. */
437 per_page?: number | null;
438};
439
440/** One page of deployments, newest first. */
441export type DeploymentPage = {
442 deployments: RepoDeployment[];
443 total_count: number;
444 page: number;
445 per_page: number;
446};
447
448/** What `create_deployment` takes, as the API does. */
449export type NewDeployment = {
450 ref: string;
451 /** The commit; resolved from `ref` when left out. */
452 sha?: string | null;
453 environment?: string | null;
454 task?: string | null;
455 description?: string | null;
456 payload?: Record<string, unknown> | null;
457 transient_environment?: boolean | null;
458 production_environment?: boolean | null;
459 /** The state of its first status: `queued` unless given. */
460 state?: DeploymentState | null;
461 environment_url?: string | null;
462 log_url?: string | null;
463};
464
465/** What `create_deployment_status` takes. */
466export type NewDeploymentStatus = {
467 state: DeploymentState;
468 description?: string | null;
469 environment_url?: string | null;
470 log_url?: string | null;
471 /**
472 * On a success: the environment's older deployments that succeeded get
473 * an `inactive` status. True unless given.
474 */
475 auto_inactive?: boolean | null;
476};
477
478/**
479 * Whether a change that landed on a default branch is live in production:
480 * shipped by a production deployment that succeeded (`live`), not yet
481 * deployed (`waiting`), on its way (`deploying`), or the production
482 * deployment after it failed and none has shipped it since (`failed`).
483 */
484export type ShipState = "live" | "waiting" | "deploying" | "failed";
485
486/** One change asked about: its commit on the default branch, and when it landed there. */
487export type ShippedAsk = { sha: string; landed_at: string };
488
489/** A change's ship state, from what deployments recorded when production deployed. */
490export type ShippedCommit = {
491 sha: string;
492 state: ShipState;
493 /** RFC 3339: when the deployment that shipped it went live; null until then. */
494 deployed_at: string | null;
495 /** Where that deployment is on the site (or the failed or running one, for `failed` and `deploying`). */
496 url: string | null;
497 /** The environment it went to, such as `production`. */
498 environment: string | null;
499};
500
501/** A repository's answer: whether it deploys to production at all, and each change asked about. */
502export type ShippedRepo = { repo_id: string; deploys: boolean; commits: ShippedCommit[] };
503
504export interface DeploymentsApi {
505 /** Members of the workspace only. */
506 settings(project: ProjectRef, viewer: Viewer): Promise<Result<DeploySettings>>;
507 /** Members only. Turning deployments on needs the workspace's plan. */
508 updateSettings(actor: User, project: ProjectRef, changes: Partial<DeploySettings>): Promise<Result<DeploySettings>>;
509 /** The newest builds first, and what is up now. Members only. */
510 list(project: ProjectRef, viewer: Viewer): Promise<Result<{ deployments: Deployment[]; live: LiveApp[] }>>;
511 /** One build, with its log. Members only. */
512 get(project: ProjectRef, id: string, viewer: Viewer): Promise<Result<Deployment & { log: string | null }>>;
513 /** Builds production (`branch` null), or a branch's preview, again from its head. */
514 redeploy(actor: User, project: ProjectRef, branch: string | null): Promise<Result<Deployment>>;
515 /** Takes production (`branch` null), or a branch's preview, down now. */
516 takeDown(actor: User, project: ProjectRef, branch: string | null): Promise<Result<true>>;
517 /** Every project of a workspace at a glance. Members only. */
518 overview(workspace: string, viewer: Viewer): Promise<Result<ProjectDeploys[]>>;
519 /** What the workspace's apps used this month. Members only. */
520 usage(workspace: string, viewer: Viewer): Promise<Result<DeployUsage>>;
521 /** The project's custom domains. Members only. */
522 domains(project: ProjectRef, viewer: Viewer): Promise<Result<ProjectDomains>>;
523 /**
524 * Adds a custom domain for production. With `twin`, its www or apex twin
525 * is added too, redirecting to it. Members only; needs the Deployments plan.
526 */
527 addDomain(actor: User, project: ProjectRef, hostname: string, options?: { twin?: boolean }): Promise<Result<Domain[]>>;
528 /** Removes a custom domain, and any domain redirecting to it. Members only. */
529 removeDomain(actor: User, project: ProjectRef, id: string): Promise<Result<true>>;
530 /** Asks Cloudflare to check the domain again now. Members only. */
531 refreshDomain(actor: User, project: ProjectRef, id: string): Promise<Result<Domain>>;
532 /** A repository's deployments, newest first, filtered. Anyone who can read it. */
533 repoDeployments(repo: RepoPath, viewer: Viewer, filter?: DeploymentFilter): Promise<Result<DeploymentPage>>;
534 /** One deployment with its statuses, oldest first. Anyone who can read the repository. */
535 repoDeployment(repo: RepoPath, id: string, viewer: Viewer): Promise<Result<DeploymentDetail>>;
536 /** A repository's environments with their current and latest deployments. Anyone who can read it. */
537 environments(repo: RepoPath, viewer: Viewer): Promise<Result<DeploymentEnvironments>>;
538 /** Reports a deployment. Takes the Write role. */
539 createDeployment(actor: User, repo: RepoPath, input: NewDeployment): Promise<Result<DeploymentDetail>>;
540 /** Adds a status to a reported deployment. Takes the Write role. */
541 createDeploymentStatus(actor: User, repo: RepoPath, id: string, input: NewDeploymentStatus): Promise<Result<DeploymentStatus>>;
542 /**
543 * Whether changes that landed on default branches went live, and when:
544 * read from what was recorded as each production deployment succeeded
545 * (src/shipped.ts). Members of the workspace only; at most 20
546 * repositories and 300 commits a call.
547 */
548 shipped(workspace: string, viewer: Viewer, repos: { repo_id: string; commits: ShippedAsk[] }[]): Promise<Result<ShippedRepo[]>>;
549}