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
53 linesCodeBlameRaw
1import { env } from "cloudflare:workers";
2
3import type { Route } from "./+types/downloads-runner";
4
5/**
6 * Releases: `g1t.sh/downloads/<tool>/<version>/<file>`, served from the
7 * g1t-downloads R2 bucket. `runner` is the self-hosted runner
8 * (scripts/runner-release.mjs), `cli` the g1t CLI (scripts/cli-release.mjs),
9 * `desktop` the desktop app (scripts/desktop-release.mjs). `latest/<file>`
10 * is the newest release's, as its `latest.json` names it; the runner's
11 * `latest.json` and `latest.json.sig` are what runners check before
12 * updating, and the desktop app's `latest.json` names each platform's
13 * installer with its signature, as the app's updater reads it.
14 */
15const TOOLS = new Set(["runner", "cli", "desktop"]);
16const TYPES: Record<string, string> = {
17 json: "application/json",
18 sig: "text/plain; charset=utf-8",
19 txt: "text/plain; charset=utf-8",
20};
21
22async function object(key: string): Promise<R2ObjectBody | null> {
23 const bucket = env.DOWNLOADS;
24 return bucket ? bucket.get(key) : null;
25}
26
27export async function loader({ params }: Route.LoaderArgs) {
28 const tool = params.tool ?? "";
29 const path = (params["*"] ?? "").replace(/^\/+/, "");
30 if (!TOOLS.has(tool) || !path || path.includes("..") || !/^[A-Za-z0-9._/-]+$/.test(path)) {
31 throw new Response("Not found\n", { status: 404 });
32 }
33 let key = `${tool}/${path}`;
34 // `latest/<file>`: the file of the newest release.
35 if (path.startsWith("latest/")) {
36 const manifest = await object(`${tool}/latest.json`);
37 if (!manifest) throw new Response("No release yet\n", { status: 404 });
38 const { version } = (await manifest.json()) as { version: string };
39 key = `${tool}/${version}/${path.slice("latest/".length)}`;
40 }
41 const found = await object(key);
42 if (!found) throw new Response("Not found\n", { status: 404 });
43 const name = key.split("/").pop() ?? "g1t-runner";
44 const extension = name.includes(".") ? name.split(".").pop()! : "";
45 const headers = new Headers({
46 "content-type": TYPES[extension] ?? "application/octet-stream",
47 etag: found.httpEtag,
48 // A version's files never change; what `latest` points to does.
49 "cache-control": path.startsWith("latest") ? "public, max-age=300" : "public, max-age=31536000, immutable",
50 });
51 if (!TYPES[extension]) headers.set("content-disposition", `attachment; filename="${name}"`);
52 return new Response(found.body, { headers });
53}