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
455 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.

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.1/**
2 * Container apps: a project built as a Docker image (crates/runner
3 * deploy/container.rs) runs on Cloudflare Containers behind a Worker of
4 * g1t's own, one per app, in the same Workers for Platforms namespace as
5 * every other app, so the `*.g1t.page` dispatcher (services/pages) and
6 * custom domains reach it by its script's name as they reach any app.
7 *
8 * Each app's Worker is `APP_WORKER` below, never the project's code: its
9 * Durable Object class `G1tApp` has the app's container (a Container
10 * application with the default scheduling policy, the app's image and
11 * instance type, at most one instance), starts it on the first request
12 * with the project's variables and secrets as its environment and `PORT`,
13 * waits for it to answer on its port, proxies requests to it, and stops it
14 * after `CONTAINER_SLEEP_MINUTES` without a request. While it runs it
15 * meters itself by the started minute, and reports to g1t with a token of
16 * its own (`POST /deployments/apps/<script>/meter`), which billing charges
17 * at cost plus the margin.
18 *
19 * The build's sandbox pushes the image with a login this service mints
20 * from Cloudflare's registry API: push only, for 15 minutes. Cloudflare's
21 * registry credentials cannot be held to one repository, so g1t holds the
22 * build to its app's: it names the repository, deploys only an image by
23 * digest in that repository, and checks the digest is there before it
24 * does. The login can never pull, so no build can read another app's image.
Merge branch 'worktree-agent-a4d0c5ee9c2f877aa'25 * Once a deploy is live, the app's older images are deleted, keeping the
26 * one it replaced; an app taken down takes its images with it.
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.27 *
28 * Pure, with no Workers imports, so it can be tested under Node.
29 */
30
Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry.31import { TAIL_CONSUMERS } from "./telemetry";
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.32import {
33 APP_INSTANCES,
34 CONTAINER_SLEEP_MINUTES,
35 DEFAULT_APP_INSTANCE,
36 appInstance,
37 containerSecondMicros,
38 type ContainerCosts,
39 type ContainerOffer,
40} from "@g1t/contracts";
41
42/** Cloudflare's container registry, which Containers pulls from. */
43export const REGISTRY_HOST = "registry.cloudflare.com";
44
45/** The Durable Object class of every container app's Worker. */
46export const APP_CLASS = "G1tApp";
47
48/** How long a build's push login lasts. */
49export const PUSH_LOGIN_MINUTES = 15;
50
Merge branch 'worktree-agent-a4d0c5ee9c2f877aa'51/**
52 * What Cloudflare Containers holds of an account's images, in all. Each
53 * app keeps only the image it runs and the one before it
54 * (`Cloudflare.pruneImages`), and none once it is taken down.
55 */
56export const IMAGE_STORAGE_GB = 50;
57
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.58/** The repository an app's images are pushed to: `<account>/g1t-<script>`. */
59export function repositoryFor(account: string, script: string): string {
60 return `${account}/g1t-${script}`;
61}
62
63/** What `POST /accounts/{id}/containers/registries/{domain}/credentials` is asked for: a push-only login, for minutes. */
64export function pushLoginRequest(): { permissions: ["push"]; expiration_minutes: number } {
65 return { permissions: ["push"], expiration_minutes: PUSH_LOGIN_MINUTES };
66}
67
68/** The most an image may weigh to run on `instanceType`: its disk, as Cloudflare limits it. */
69export function maxImageBytes(instanceType: string): number {
70 return appInstance(instanceType).diskGb * 1_000_000_000;
71}
72
73/** An instance type the project may choose, or the default. */
74export function instanceTypeOf(value: unknown): string {
75 return typeof value === "string" && APP_INSTANCES.some((instance) => instance.type === value) ? value : DEFAULT_APP_INSTANCE;
76}
77
78/**
79 * What a build reported it pushed, if it is an image this build may run:
80 * a digest in its app's own repository and nothing else, with the port
81 * it listens on.
82 */
83export function pushedImage(
84 value: unknown,
85 repository: string,
86): { image: string; digest: string; port: number; sizeBytes: number } | null {
87 if (!value || typeof value !== "object") return null;
88 const report = value as Record<string, unknown>;
89 const image = typeof report.image === "string" ? report.image : "";
90 const match = image.match(/^([a-z0-9.-]+)\/(.+)@(sha256:[0-9a-f]{64})$/);
91 if (!match || match[1] !== REGISTRY_HOST || match[2] !== repository) return null;
92 const port = Number(report.port);
93 return {
94 image,
95 digest: match[3],
96 port: Number.isInteger(port) && port > 0 && port < 65536 ? port : 8080,
97 sizeBytes: Math.max(0, Number(report.sizeBytes) || 0),
98 };
99}
100
101/** The settings an app's Worker reads, besides the project's variables and secrets. */
102export type AppWorkerSettings = {
103 mode: "serve" | "paused" | "down" | "redirect";
104 /** For `redirect`: the hostname to send visitors to. */
105 redirect?: string;
106 port: number;
107 instanceType: string;
108 /** Where it reports what it ran, with `meterToken`. */
109 meterUrl: string;
110 meterToken: string;
111};
112
113/** Names the app's Worker keeps for itself: never passed to the container. */
114export const INTERNAL_BINDINGS = ["APP", "G1T_MODE", "G1T_REDIRECT", "G1T_PORT", "G1T_SLEEP_MS", "G1T_METER_URL", "G1T_METER_TOKEN", "G1T_INSTANCE"];
115
116/**
117 * The upload's metadata for a container app's Worker: `APP_WORKER` as its
118 * one module, its Durable Object class with a container, g1t's settings,
119 * and the project's variables and secrets, which the Worker passes to the
120 * container as its environment. A variable named like one of g1t's own is
121 * left out.
122 */
123export function appWorkerMetadata(
124 settings: AppWorkerSettings,
125 runtime: { secrets: Record<string, string>; variables: Record<string, string> },
126 tags: string[],
127): Record<string, unknown> {
128 const own = new Set(INTERNAL_BINDINGS);
129 const bindings: object[] = [{ type: "durable_object_namespace", name: "APP", class_name: APP_CLASS }];
130 const plain: Record<string, string> = {
131 G1T_MODE: settings.mode,
132 G1T_PORT: String(settings.port),
133 G1T_SLEEP_MS: String(CONTAINER_SLEEP_MINUTES * 60 * 1000),
134 G1T_METER_URL: settings.meterUrl,
135 G1T_INSTANCE: settings.instanceType,
136 };
137 if (settings.redirect) plain.G1T_REDIRECT = settings.redirect;
138 for (const [name, text] of Object.entries(plain)) bindings.push({ type: "plain_text", name, text });
139 bindings.push({ type: "secret_text", name: "G1T_METER_TOKEN", text: settings.meterToken });
140 for (const [name, text] of Object.entries(runtime.variables)) {
141 if (!own.has(name) && !(name in runtime.secrets)) bindings.push({ type: "plain_text", name, text });
142 }
143 for (const [name, text] of Object.entries(runtime.secrets)) {
144 if (!own.has(name)) bindings.push({ type: "secret_text", name, text });
145 }
146 return {
147 main_module: "index.js",
148 compatibility_date: "2026-09-26",
149 compatibility_flags: [],
150 bindings,
151 tags,
Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry.152 tail_consumers: TAIL_CONSUMERS,
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.153 // Declared, so uploading it again changes nothing about the class.
154 exports: { [APP_CLASS]: { type: "durable-object", storage: "sqlite" } },
155 containers: [{ class_name: APP_CLASS }],
156 };
157}
158
Merge branch 'worktree-agent-a4d0c5ee9c2f877aa'159/** A container's output, kept in Workers Logs. */
160const CONTAINER_LOGS = { logs: { enabled: true } };
161
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.162/** `POST /accounts/{id}/containers/applications`: the app's container, for its Durable Object namespace. */
163export function applicationRequest(name: string, image: string, instanceType: string, namespaceId: string): Record<string, unknown> {
164 return {
165 name,
166 scheduling_policy: "default",
Merge branch 'worktree-agent-a4d0c5ee9c2f877aa'167 // Its output goes to Workers Logs on g1t's account, where it can be read.
168 configuration: { image, instance_type: instanceTypeOf(instanceType), observability: CONTAINER_LOGS },
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.169 instances: 0,
170 // One instance per app: its Durable Object is named `app`.
171 max_instances: 1,
172 durable_objects: { namespace_id: namespaceId },
173 };
174}
175
176/** `POST /accounts/{id}/containers/applications/{app}/rollouts`: a new image or size, all at once. */
177export function rolloutRequest(image: string, instanceType: string, description: string): Record<string, unknown> {
178 return {
179 description,
180 strategy: "rolling",
181 kind: "full_auto",
182 step_percentage: 100,
Merge branch 'worktree-agent-a4d0c5ee9c2f877aa'183 target_configuration: { image, instance_type: instanceTypeOf(instanceType), observability: CONTAINER_LOGS },
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.184 };
185}
186
187/** The name of an app's Container application. */
188export function applicationName(script: string): string {
189 return `g1t-${script}`;
190}
191
192/** What an app's Worker reports: how long its container has run this month, and what it sent. */
193export type MeterReport = {
194 token: string;
195 /** The Durable Object's own counter: a fresh one starts again at zero. */
196 epoch: string;
197 month: string;
198 awakeSeconds: number;
199 egressBytes: number;
200 instanceType: string;
201};
202
203/** A report from an app's Worker, if it is one. */
204export function meterReport(body: unknown): MeterReport | null {
205 if (!body || typeof body !== "object") return null;
206 const b = body as Record<string, unknown>;
207 const awake = Number(b.awakeSeconds);
208 const egress = Number(b.egressBytes ?? 0);
209 if (typeof b.token !== "string" || typeof b.epoch !== "string" || !/^[\w-]{8,64}$/.test(b.epoch)) return null;
210 if (typeof b.month !== "string" || !/^\d{4}-\d{2}$/.test(b.month)) return null;
211 if (!Number.isFinite(awake) || awake < 0 || !Number.isFinite(egress) || egress < 0) return null;
212 // No month has more seconds than this.
213 if (awake > 31 * 24 * 60 * 60) return null;
214 return {
215 token: b.token,
216 epoch: b.epoch,
217 month: b.month,
218 awakeSeconds: Math.ceil(awake),
219 egressBytes: Math.ceil(egress),
220 instanceType: instanceTypeOf(b.instanceType),
221 };
222}
223
224/** One app's container time in a month, as the `container_meters` table keeps it. */
225export type ContainerMeter = { instance_type: string; awake_seconds: number; egress_bytes: number };
226
227/** What a month of container time cost g1t, in millionths of a dollar, rounded up, and in words. */
228export function containerCost(rows: ContainerMeter[], costs: ContainerCosts): { micros: number; seconds: number; detail: string | null } {
229 let micros = 0;
230 let seconds = 0;
231 let bytes = 0;
232 for (const row of rows) {
233 const awake = Math.max(0, row.awake_seconds);
234 micros += awake * containerSecondMicros(appInstance(row.instance_type), costs);
235 micros += (Math.max(0, row.egress_bytes) / 1_000_000_000) * costs.egressGb;
236 seconds += awake;
237 bytes += Math.max(0, row.egress_bytes);
238 }
239 const hours = Math.round((seconds / 3600) * 10) / 10;
240 const detail = seconds || bytes ? `${hours.toLocaleString("en-US")} container hours and ${(Math.round(bytes / 1e7) / 100).toLocaleString("en-US")} GB sent` : null;
241 return { micros: Math.ceil(micros), seconds, detail };
242}
243
244/** What each size costs an hour awake, at most, with `marginPercent` on top: for Settings, Deployments and the docs. */
245export function containerOffer(costs: ContainerCosts, marginPercent: number, available: boolean): ContainerOffer {
246 const withMargin = (micros: number) => Math.ceil(micros * (1 + marginPercent / 100));
247 return {
248 available,
249 prices: APP_INSTANCES.map((instance) => ({ ...instance, hourlyMicros: withMargin(containerSecondMicros(instance, costs) * 3600) })),
250 egressGbMicros: withMargin(costs.egressGb),
251 sleepMinutes: CONTAINER_SLEEP_MINUTES,
252 };
253}
254
255/**
256 * The Worker every container app runs as, g1t's own. `G1tApp` holds the
257 * container: it starts it with the project's variables and secrets and
258 * `PORT`, waits up to 90 seconds for it to answer on its port, proxies to
259 * it, and stops it once no request has come for `G1T_SLEEP_MS`. Every
260 * minute it is up is metered from its start, ahead (a minute begun is a
261 * minute charged), with the bytes it sent back, and reported to g1t.
262 */
263export const APP_WORKER = `import { DurableObject } from "cloudflare:workers";
264
265const INTERNAL = new Set(${JSON.stringify(INTERNAL_BINDINGS)});
266const TICK_MS = 60000;
267const READY_MS = 90000;
268
269function page(status, title, text) {
270 return new Response("<!doctype html><meta charset=utf-8><meta name=viewport content='width=device-width, initial-scale=1'><meta name=robots content=noindex><title>" + title + "</title><body style='font:16px system-ui;background:#121214;color:#ececf1;display:grid;place-items:center;min-height:100vh;margin:0;padding:0 16px'><main style='max-width:32rem'><h1>" + title + "</h1><p style='color:#9a9aa6'>" + text + "</p></main>", { status, headers: { "content-type": "text/html; charset=utf-8", "x-robots-tag": "noindex", "cache-control": "no-store" } });
271}
272
273/** What the app answers while it is not serving: paused, down, or moved. */
274function notServing(env, request) {
275 if (env.G1T_MODE === "redirect" && env.G1T_REDIRECT) {
276 const url = new URL(request.url);
277 return new Response(null, { status: 301, headers: { location: "https://" + env.G1T_REDIRECT + url.pathname + url.search, "cache-control": "public, max-age=3600" } });
278 }
279 if (env.G1T_MODE === "paused") return page(402, "This app is paused", "The workspace it belongs to reached its usage limit on g1t. It comes back by itself once the workspace is under it again.");
280 return page(404, "This app is not up", "It was taken down. Deploying it again brings it back.");
281}
282
283const monthOf = (at) => new Date(at).toISOString().slice(0, 7);
284
285export class ${APP_CLASS} extends DurableObject {
286 constructor(ctx, env) {
287 super(ctx, env);
288 this.listening = false;
289 this.lastRequest = Date.now();
290 this.sent = 0;
291 }
292
293 async fetch(request) {
294 const env = this.env;
295 const container = this.ctx.container;
296 if (env.G1T_MODE !== "serve") {
297 if (container && container.running) await container.destroy();
298 return notServing(env, request);
299 }
300 this.lastRequest = Date.now();
301 const port = Number(env.G1T_PORT) || 8080;
302 if (!container.running) {
303 const vars = { PORT: String(port) };
304 for (const [name, value] of Object.entries(env)) if (!INTERNAL.has(name) && typeof value === "string") vars[name] = value;
305 container.start({ env: vars, enableInternet: true });
306 this.listening = false;
Merge branch 'worktree-agent-a4d0c5ee9c2f877aa'307 // How it ended, if it ends: said on the page of a request it never answered.
308 this.exited = null;
A container app that ends before it answers says why when Cloudflare does: the reason its monitor gives is logged as g1t-container-ended and shown on the app's 503 page, where only an unknown exit code was.309 this.why = null;
Merge branch 'worktree-agent-a4d0c5ee9c2f877aa'310 if (typeof container.monitor === "function") {
311 container.monitor().then(
312 () => { this.exited = "0"; },
A container app that ends before it answers says why when Cloudflare does: the reason its monitor gives is logged as g1t-container-ended and shown on the app's 503 page, where only an unknown exit code was.313 (error) => {
314 this.exited = error && error.exitCode !== undefined ? String(error.exitCode) : "unknown";
315 // Why it ended, when Cloudflare says: in the app's logs and on its page.
316 this.why = error && error.message ? String(error.message).slice(0, 300) : null;
317 console.error("g1t-container-ended", JSON.stringify({ exitCode: error?.exitCode ?? null, message: this.why }));
318 },
Merge branch 'worktree-agent-a4d0c5ee9c2f877aa'319 );
320 }
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.321 await this.tick();
322 }
323 // A backstop: g1t stops it itself once it has slept long enough.
324 await container.setInactivityTimeout((Number(env.G1T_SLEEP_MS) || 300000) + 2 * TICK_MS).catch(() => undefined);
325 if (!(await this.ready(container, port))) {
A container app that ends before it answers says why when Cloudflare does: the reason its monitor gives is logged as g1t-container-ended and shown on the app's 503 page, where only an unknown exit code was.326 const ended = (this.exited === null || this.exited === undefined ? "It is not answering on port " + port + "." : "It stopped (exit code " + this.exited + ") before it answered on port " + port + ".") + (this.why ? " Cloudflare said: " + this.why : "");
Merge branch 'worktree-agent-a4d0c5ee9c2f877aa'327 return page(503, "The app did not start", ended + " It must listen on 0.0.0.0 at the port in the PORT environment variable (the one its Dockerfile exposes, else 8080).");
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.328 }
329 let response;
330 try {
A container app is sent each request over plain HTTP, as it listens, with the visitor's scheme and host as X-Forwarded-Proto and X-Forwarded-Host: the request went on with its https:// address, the container's port was asked for TLS, and the app closed the connection (502, "It closed the connection before it answered").331 // To the container over plain HTTP, as the app listens: the visitor's
332 // https:// address would have the port asked for TLS, and the app
333 // close the connection. The path, query, method, headers and body
334 // stay as they came; the original host and scheme go along as
335 // X-Forwarded-* headers.
336 const url = new URL(request.url);
337 const headers = new Headers(request.headers);
338 headers.set("x-forwarded-proto", url.protocol.replace(":", ""));
339 headers.set("x-forwarded-host", url.host);
340 const inner = new Request("http://container" + url.pathname + url.search, {
341 method: request.method,
342 headers,
343 body: request.body,
344 redirect: "manual",
345 });
346 response = await container.getTcpPort(port).fetch(inner);
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.347 } catch (error) {
348 return page(502, "The app failed", "It closed the connection before it answered.");
349 }
350 return this.counted(response);
351 }
352
353 /** Whether the container answers on its port, waiting for it to start. */
354 async ready(container, port) {
355 if (this.listening) return true;
356 const until = Date.now() + READY_MS;
357 while (Date.now() < until) {
358 try {
359 await container.getTcpPort(port).fetch("http://container/", { method: "HEAD" });
360 this.listening = true;
361 return true;
362 } catch {}
363 if (!container.running) return false;
364 await new Promise((resolve) => setTimeout(resolve, 250));
365 }
366 return false;
367 }
368
369 /** The response, with the bytes it sends counted. */
370 counted(response) {
371 if (!response.body) return response;
372 const self = this;
373 const count = new TransformStream({
374 transform(chunk, controller) {
375 self.sent += chunk.byteLength;
376 controller.enqueue(chunk);
377 },
378 });
379 return new Response(response.body.pipeThrough(count), response);
380 }
381
382 async meter(now) {
383 let meter = await this.ctx.storage.get("meter");
384 const instance = this.env.G1T_INSTANCE;
385 if (meter && meter.instance !== instance) {
386 // A new size is counted apart, since it costs differently.
387 await this.report(meter);
388 meter = null;
389 }
390 if (!meter) meter = { epoch: crypto.randomUUID(), instance, month: monthOf(now), awakeMs: 0, egressBytes: 0, billedUntil: 0 };
391 if (meter.month !== monthOf(now)) {
392 await this.report(meter);
393 meter = { ...meter, month: monthOf(now), awakeMs: 0, egressBytes: 0 };
394 }
395 return meter;
396 }
397
398 /** Charges the next minute up (a minute begun is charged), and wakes again at its end. */
399 async tick() {
400 const now = Date.now();
401 const meter = await this.meter(now);
402 const until = now + TICK_MS;
403 meter.awakeMs += until - Math.max(meter.billedUntil, now);
404 meter.billedUntil = until;
405 meter.egressBytes += this.sent;
406 this.sent = 0;
407 await this.ctx.storage.put("meter", meter);
408 await this.ctx.storage.setAlarm(until);
409 await this.report(meter);
410 }
411
412 async alarm() {
413 const container = this.ctx.container;
414 const sleep = Number(this.env.G1T_SLEEP_MS) || 300000;
415 if (container && container.running && this.env.G1T_MODE === "serve" && Date.now() - this.lastRequest < sleep) {
416 await this.tick();
417 return;
418 }
419 // Asleep: nothing runs, and nothing more is charged.
420 if (container && container.running) await container.destroy();
421 const meter = await this.meter(Date.now());
422 meter.egressBytes += this.sent;
423 this.sent = 0;
424 await this.ctx.storage.put("meter", meter);
425 await this.report(meter);
426 }
427
428 /** Tells g1t what has run this month; a report that fails is caught up by the next. */
429 async report(meter) {
430 await fetch(this.env.G1T_METER_URL, {
431 method: "POST",
432 headers: { "content-type": "application/json" },
433 body: JSON.stringify({
434 token: this.env.G1T_METER_TOKEN,
435 epoch: meter.epoch,
436 month: meter.month,
437 awakeSeconds: Math.ceil(meter.awakeMs / 1000),
438 egressBytes: meter.egressBytes,
439 instanceType: meter.instance,
440 }),
441 }).catch(() => undefined);
442 }
443}
444
445export default {
446 async fetch(request, env, ctx) {
447 if (env.G1T_MODE !== "serve") {
448 // Its Durable Object stops its container, if it is still up.
449 ctx.waitUntil(env.APP.get(env.APP.idFromName("app")).fetch(request).catch(() => undefined));
450 return notServing(env, request);
451 }
452 return env.APP.get(env.APP.idFromName("app")).fetch(request);
453 },
454};
455`;

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