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
232 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 workspace cost alert compares only days that carry their value, not the days before it was kept1#!/usr/bin/env node
2// Releases of the self-hosted runner, `g1t-runner` (crates/runner): built
3// for every platform, checksummed, signed, and published to the
4// g1t-downloads R2 bucket that g1t.sh serves at /downloads/runner/
5// (apps/web/app/routes/downloads-runner.ts).
6//
7// node scripts/runner-release.mjs keygen # once: the release key
8// node scripts/runner-release.mjs build [--targets linux-x64,windows-x64]
Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it.9// node scripts/runner-release.mjs manifest # again, with the sandbox's images
The workspace cost alert compares only days that carry their value, not the days before it was kept10// node scripts/runner-release.mjs sign # latest.json + .sig
11// node scripts/runner-release.mjs verify # checks the signature
12// node scripts/runner-release.mjs publish [--dry-run]
13//
14// What a release is, at runner/<version>/ in the bucket:
15//
16// g1t-runner-linux-x64, -linux-arm64, -macos-x64, -macos-arm64,
17// -windows-x64.exe the binaries
18// SHA256SUMS `sha256 name`, one line each
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.19// manifest.json { version, published_at, files: { platform: { name, sha256 } },
Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it.20// sandbox: { runner_image, files: { "wsl-x64": { name, sha256, size }, … } } }
21// g1t-sandbox-* the sandbox's images (scripts/sandbox-image.mjs), when built
The workspace cost alert compares only days that carry their value, not the days before it was kept22//
23// and at runner/: latest.json (the newest release's manifest) and
24// latest.json.sig, its Ed25519 signature in base64. A runner trusts a
25// release only when the signature checks out against the public key built
26// into it (G1T_RUNNER_RELEASE_KEY at build time) and every file's SHA-256
27// is the manifest's (crates/runner/src/selfhosted/update.rs).
28//
29// Environment:
30// RUNNER_RELEASE_KEY the private key (keygen prints it): a g1t Actions secret
31// G1T_RUNNER_RELEASE_KEY the public key, built into the binaries
32//
33// Every target is cross-compiled with cargo-zigbuild (`cargo install
34// cargo-zigbuild`, and zig on PATH), so one Linux machine builds them all.
35
36import { spawnSync } from "node:child_process";
37import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from "node:crypto";
38import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
39import { dirname, join } from "node:path";
40import { fileURLToPath } from "node:url";
41
42const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
43export const TARGETS = {
44 "linux-x64": { triple: "x86_64-unknown-linux-musl", file: "g1t-runner-linux-x64" },
45 "linux-arm64": { triple: "aarch64-unknown-linux-musl", file: "g1t-runner-linux-arm64" },
46 "macos-x64": { triple: "x86_64-apple-darwin", file: "g1t-runner-macos-x64" },
47 "macos-arm64": { triple: "aarch64-apple-darwin", file: "g1t-runner-macos-arm64" },
48 "windows-x64": { triple: "x86_64-pc-windows-gnu", file: "g1t-runner-windows-x64.exe", exe: true },
49};
50const BUCKET = "g1t-downloads";
51
52export function version() {
53 const toml = readFileSync(join(ROOT, "crates/runner/Cargo.toml"), "utf8");
54 const found = /^version\s*=\s*"([^"]+)"/m.exec(toml);
55 if (!found) throw new Error("crates/runner/Cargo.toml has no version");
56 return found[1];
57}
58
59const outDir = (v = version()) => join(ROOT, "target", "runner-release", v);
60
61/** The 32 raw bytes of an Ed25519 public key, in base64, as the runner takes it. */
62export function rawPublicKey(publicKey) {
63 const der = publicKey.export({ type: "spki", format: "der" });
64 return der.subarray(der.length - 32).toString("base64");
65}
66
67export function keygen() {
68 const { privateKey, publicKey } = generateKeyPairSync("ed25519");
69 return {
70 private: privateKey.export({ type: "pkcs8", format: "der" }).toString("base64"),
71 public: rawPublicKey(publicKey),
72 };
73}
74
75function privateKey(text = process.env.RUNNER_RELEASE_KEY) {
76 if (!text) throw new Error("RUNNER_RELEASE_KEY is not set");
77 return createPrivateKey({ key: Buffer.from(text, "base64"), format: "der", type: "pkcs8" });
78}
79
80/** Signs `bytes`: the signature in base64. */
81export function signBytes(bytes, key) {
82 return sign(null, bytes, key).toString("base64");
83}
84
85/** Whether `signature` is the key's over `bytes`, given the raw public key in base64. */
86export function verifyBytes(bytes, signature, publicRaw) {
87 const der = Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), Buffer.from(publicRaw, "base64")]);
88 const key = createPublicKey({ key: der, format: "der", type: "spki" });
89 return verify(null, bytes, key, Buffer.from(signature, "base64"));
90}
91
92export const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
93
Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it.94/**
95 * The manifest of the files in a release folder. Its `sandbox` section, the
96 * images `g1t-runner sandbox` runs and their SHA-256, comes from
97 * sandbox.json there when scripts/sandbox-image.mjs made one, and is signed
98 * with the rest.
99 */
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.100export function manifest(dir, v) {
The workspace cost alert compares only days that carry their value, not the days before it was kept101 const files = {};
102 for (const [platform, target] of Object.entries(TARGETS)) {
103 const path = join(dir, target.file);
104 if (existsSync(path)) files[platform] = { name: target.file, sha256: sha256(readFileSync(path)) };
105 }
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.106 const m = { version: v, published_at: new Date().toISOString(), files };
Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it.107 const sandboxPath = join(dir, "sandbox.json");
108 if (existsSync(sandboxPath)) {
109 const sandbox = JSON.parse(readFileSync(sandboxPath, "utf8"));
110 for (const file of Object.values(sandbox.files ?? {})) {
111 if (!existsSync(join(dir, file.name))) throw new Error(`sandbox.json names ${file.name}, which is not in ${dir}`);
112 }
113 m.sandbox = sandbox;
114 }
115 return m;
116}
117
118/** Writes manifest.json and SHA256SUMS for what is in the folder now. */
119function writeManifest(dir, v) {
120 const m = manifest(dir, v);
121 writeFileSync(join(dir, "manifest.json"), `${JSON.stringify(m, null, 2)}\n`);
122 const all = [...Object.values(m.files), ...Object.values(m.sandbox?.files ?? {})];
123 writeFileSync(join(dir, "SHA256SUMS"), all.map((f) => `${f.sha256} ${f.name}`).join("\n") + "\n");
124 return m;
The workspace cost alert compares only days that carry their value, not the days before it was kept125}
126
127function run(command, args, options = {}) {
128 const done = spawnSync(command, args, { stdio: "inherit", cwd: ROOT, shell: process.platform === "win32", ...options });
129 if (done.status !== 0) throw new Error(`${command} ${args.join(" ")} failed`);
130}
131
132function build(targets) {
133 const v = version();
134 const dir = outDir(v);
135 mkdirSync(dir, { recursive: true });
136 if (!process.env.G1T_RUNNER_RELEASE_KEY) console.error("warning: G1T_RUNNER_RELEASE_KEY is not set; these builds will not update themselves");
137 for (const platform of targets) {
138 const target = TARGETS[platform];
139 if (!target) throw new Error(`unknown target ${platform}; one of ${Object.keys(TARGETS).join(", ")}`);
140 console.error(`building ${platform} (${target.triple})`);
141 run("rustup", ["target", "add", target.triple]);
142 run("cargo", ["zigbuild", "--release", "--locked", "--package", "g1t-runner", "--target", target.triple]);
143 const built = join(ROOT, "target", target.triple, "release", target.exe ? "g1t-runner.exe" : "g1t-runner");
144 copyFileSync(built, join(dir, target.file));
145 }
Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it.146 const m = writeManifest(dir, v);
The workspace cost alert compares only days that carry their value, not the days before it was kept147 console.log(`built ${Object.keys(m.files).length} binaries of ${v} into ${dir}`);
148}
149
150function signRelease() {
151 const v = version();
152 const dir = outDir(v);
153 const bytes = readFileSync(join(dir, "manifest.json"));
154 writeFileSync(join(dir, "latest.json"), bytes);
155 writeFileSync(join(dir, "latest.json.sig"), signBytes(bytes, privateKey()));
156 console.log(`signed ${v}`);
157}
158
159function verifyRelease() {
160 const dir = outDir();
161 const publicRaw = process.env.G1T_RUNNER_RELEASE_KEY;
162 if (!publicRaw) throw new Error("G1T_RUNNER_RELEASE_KEY is not set");
163 const ok = verifyBytes(readFileSync(join(dir, "latest.json")), readFileSync(join(dir, "latest.json.sig"), "utf8"), publicRaw);
164 if (!ok) throw new Error("latest.json's signature is not the release key's");
165 const m = JSON.parse(readFileSync(join(dir, "latest.json"), "utf8"));
Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it.166 const all = [...Object.values(m.files), ...Object.values(m.sandbox?.files ?? {})];
167 for (const file of all) {
The workspace cost alert compares only days that carry their value, not the days before it was kept168 if (sha256(readFileSync(join(dir, file.name))) !== file.sha256) throw new Error(`${file.name}'s SHA-256 is not the manifest's`);
169 }
Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it.170 console.log(`verified ${m.version}: ${all.length} files${m.sandbox ? `, the sandbox's runner image ${m.sandbox.runner_image ?? "(none)"}` : ""}`);
The workspace cost alert compares only days that carry their value, not the days before it was kept171}
172
173function publish(dryRun) {
174 const v = version();
175 const dir = outDir(v);
176 const put = (key, file, type) => {
177 const args = ["wrangler", "r2", "object", "put", `${BUCKET}/${key}`, "--file", file, "--remote", "--content-type", type];
178 if (dryRun) console.log(`would put ${key}`);
179 else run("npx", args, { cwd: join(ROOT, "apps/web") });
180 };
181 // The version's files first; latest.json last, so no runner is pointed
182 // at files that are not there yet.
183 for (const name of readdirSync(dir)) {
Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it.184 // sandbox.json is in manifest.json already.
185 if (name.startsWith("latest.json") || name === "sandbox.json") continue;
The workspace cost alert compares only days that carry their value, not the days before it was kept186 put(`runner/${v}/${name}`, join(dir, name), name.endsWith(".json") ? "application/json" : "application/octet-stream");
187 }
188 put("runner/latest.json", join(dir, "latest.json"), "application/json");
189 put("runner/latest.json.sig", join(dir, "latest.json.sig"), "text/plain");
190}
191
192if (process.argv[1]?.replaceAll("\\", "/").endsWith("scripts/runner-release.mjs")) {
193 const [command, ...rest] = process.argv.slice(2);
194 const option = (name) => {
195 const at = rest.indexOf(`--${name}`);
196 return at >= 0 ? rest[at + 1] : undefined;
197 };
198 try {
199 switch (command) {
200 case "keygen": {
201 const keys = keygen();
202 console.log(`RUNNER_RELEASE_KEY=${keys.private}\nG1T_RUNNER_RELEASE_KEY=${keys.public}`);
203 console.error("Keep RUNNER_RELEASE_KEY secret (a g1t Actions secret); G1T_RUNNER_RELEASE_KEY is public and goes into every build.");
204 break;
205 }
206 case "build":
207 build((option("targets") ?? Object.keys(TARGETS).join(",")).split(","));
208 break;
Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it.209 case "manifest": {
210 // Again, once the sandbox's images have joined the binaries.
211 const m = writeManifest(outDir(), version());
212 console.log(`manifest of ${m.version}: ${Object.keys(m.files).length} binaries, ${Object.keys(m.sandbox?.files ?? {}).length} sandbox images`);
213 break;
214 }
The workspace cost alert compares only days that carry their value, not the days before it was kept215 case "sign":
216 signRelease();
217 break;
218 case "verify":
219 verifyRelease();
220 break;
221 case "publish":
222 publish(rest.includes("--dry-run"));
223 break;
224 default:
225 console.error("usage: node scripts/runner-release.mjs keygen|build|sign|verify|publish");
226 process.exit(2);
227 }
228 } catch (error) {
229 console.error(String(error.message ?? error));
230 process.exit(1);
231 }
232}

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