Commit

g1t-runner 0.1.0 is released: signed binaries for five platforms at g1t.sh/downloads/runner; the Artifacts checks' results

- Runner release: the public key is the variable RUNNER_RELEASE_PUBLIC_KEY, since names starting G1T_ are reserved; the workflow hands it to the build as G1T_RUNNER_RELEASE_KEY. 0.1.0 is built, signed, verified and published (linux, macOS and Windows; x64 and arm64). - The self-hosted runners guide no longer says the runner is not available. The container image and an agent image are not published yet, and it says so where they come up. - scripts/ops: both Artifacts scripts take a global API key (CLOUDFLARE_API_KEY with CLOUDFLARE_EMAIL) as well as a token. - docs/ARTIFACTS.md: what the usage check and the fork storage test found on 2026-10-06. Cloudflare counts binding reads (read, token_create); pull looks like fetch + ls-refs + info/refs; forks are stored and billed at their source's full size, so fork retention should drop to a day; the store's early answer to a negotiating fetch.

syntaqxcommitted Parentb9a8376Browse files
7 files+93−250/7 viewed
+2−2
4848 key: runner-release-${{ hashFiles('Cargo.lock') }}
4949 - name: Build every platform
5050 env:
51− G1T_RUNNER_RELEASE_KEY: ${{ vars.G1T_RUNNER_RELEASE_KEY }}
51+ G1T_RUNNER_RELEASE_KEY: ${{ vars.RUNNER_RELEASE_PUBLIC_KEY }}
5252 RUNNER_AGENT_IMAGE: ${{ vars.RUNNER_AGENT_IMAGE }}
5353 run: node scripts/runner-release.mjs build
5454 - name: Sign
5555 env:
5656 RUNNER_RELEASE_KEY: ${{ secrets.RUNNER_RELEASE_KEY }}
57− G1T_RUNNER_RELEASE_KEY: ${{ vars.G1T_RUNNER_RELEASE_KEY }}
57+ G1T_RUNNER_RELEASE_KEY: ${{ vars.RUNNER_RELEASE_PUBLIC_KEY }}
5858 run: |
5959 node scripts/runner-release.mjs sign
6060 node scripts/runner-release.mjs verify
+8−9
99 on it, register it once, and it picks up jobs that ask for it with
1010 `runs-on: self-hosted`.
1111
12−**Not available yet:** the first release of `g1t-runner` has not been
13−published, so the downloads below and the `flagonio/g1t-runner` image do
14−not exist yet. g1t's side (**Settings → Runners**, registration tokens,
15−groups and the API) is live. This page says how the runner works once it
16−is released.
17−
1812 - **Time on your runners is $0**, on every plan, including the free one.
1913 It shows on [usage](/guides/usage-and-billing/) as self-hosted minutes.
2014 - **It only connects out.** The runner polls `api.g1t.sh` over HTTPS for
181175 your own [model provider](/guides/models/), g1t charges nothing for the
182176 run; otherwise the model is paid as usual and the machine is free.
183177 - Agent work needs an image with git, Node and the agent's CLI: register
184− the runner with `--agent-image`, which the release names, or run it with
185− `--no-docker` on a Linux machine set aside for it, where `/work` can be
186− written.
178+ the runner with `--agent-image` and an image of yours that has them, or
179+ run it with `--no-docker` on a Linux machine set aside for it, where
180+ `/work` can be written. g1t does not publish an agent image yet.
187181 - [Guardrails](/guides/guardrails/) still apply to what the agent does, but
188182 their network list cannot be enforced on your machine. The run's session
189183 says so when it starts.
232226
233227 ## Docker and Kubernetes
234228
229+**The `flagonio/g1t-runner` image is not published yet.** Until it is, run
230+the binary on the machine, or build the image from
231+[`deploy/runner/Dockerfile`](https://g1t.sh/flagon-io/g1t/blob/main/deploy/runner/Dockerfile)
232+with the Linux binary beside it.
233+
235234 The runner's image runs `register-and-run`, which registers once and then
236235 runs. Each option can also come from `G1T_RUNNER_<OPTION>` in the
237236 environment, such as `G1T_RUNNER_TOKEN` and `G1T_RUNNER_LABELS`, so a
+50−2
387387 Ratios over 1: something reaches Artifacts that g1t does not meter, such as sandboxes pushing
388388 directly with handed-out credentials.
389389 - Only days after the meters were deployed compare; before that only `git_operations` exists.
390−- Binding calls (`binding.*`) never appear among Cloudflare's event types; if Cloudflare says
391− they are billed, map them in `operation_mapping`.
390+- Binding calls do appear: Cloudflare's events include `read` and `token_create` actions (and
391+ `namespace_*`) besides the five documented ones. If Cloudflare says they are billed, map the
392+ `binding.*` meters in `operation_mapping`.
393+
394+A global API key works in place of the token: `CLOUDFLARE_API_KEY` with `CLOUDFLARE_EMAIL`
395+(both scripts).
396+
397+**Result, 2026-10-06** (31 days; g1t's meters cover only 2026-10-06, the day they shipped):
398+
399+| Cloudflare event | 31 days | 2026-10-06 | g1t's meters, 2026-10-06 |
400+| --- | --- | --- | --- |
401+| `read` | 137,225 | 107,616 | `binding.get` 85,206, `read_file` 49,846, `read_tree` 9,208, `read_blob` 4,964, `log` 2,798 |
402+| `pull` | 646 | 101 | `git.fetch` 29, `git.ls_refs` 26, `git.info_refs` 426 (+ 129 internal) |
403+| `push` | 385 | 10 | `git.receive_pack` 3 |
404+| `token_create` | 2,721 | 338 | `binding.create_token` 34 |
405+| `fork` / `create` / `delete` | 96 / 14 / 8 | 2 / 0 / 0 | |
406+| errors | 699 (688 client) | 476 client | |
407+
408+- `pull` is not one per upload-pack fetch: 101 pulls against 29 fetches on the one day both
409+ exist. Over 31 days `pull` ≈ fetch + ls-refs + info/refs (ratio 1.06), so listing refs likely
410+ counts as a pull. Not yet changed in `operation_mapping`: one day of meters is too little, and
411+ re-check after a week before setting `cost_operations` for `git.info_refs` and `git.ls_refs`.
412+- `read` is the open question that matters. If reads are billed as operations at $0.15 per
413+ 1,000, today's demo-scale traffic alone is about 3.2 million a month (~$480). Ask Cloudflare
414+ (Q1) before 2026-10-14. Either way the volume is mostly waste: every repos call opens a handle
415+ with `get` even when the answer is cached, and the object cache may not be hitting (no hit/miss
416+ meter yet). Fixes, ranked: lazy `get`; a real cache for objects named by hash (KV or an
417+ in-isolate LRU, with hit/miss meters); Actions reading workflows from the synced table instead
418+ of the store on every event; caller attribution in the meters.
419+- 476 client errors on 2026-10-06 are unexplained; the fetch fix below accounts for some (every
420+ failed negotiation was one).
392421
393422 ### R2: running and reading `scripts/ops/fork-storage-test.mjs`
394423
413442 Either way R2 retires forks; the answer decides `FORK_RETENTION_DAYS` (shared: a week is fine;
414443 copied: shorten it to 1 or 2 days and ask Cloudflare to raise the 1 TB account limit).
415444
445+**Result, 2026-10-06: forks are stored and billed as copies.** A 100 MB source took 57 s to
446+push; each of 5 forks took 4–6 s. The storage dataset (`artifactsStorageAdaptiveGroups`,
447+`max.repositorySizeBytes`) gave every fork the source's full 105,582,592 bytes: about 633 MB for
448+the six, not about 106 MB. Whatever Artifacts shares underneath, storage billing and the 1 TB
449+account limit see full copies. So:
450+
451+- `FORK_RETENTION_DAYS` should drop from 7 to 1 (not yet changed).
452+- g1t meters a workspace's storage once per repository (`stored_bytes`), so an open pull
453+ request's working copy is Cloudflare cost g1t absorbs: about $0.05 a month per 100 MB per open
454+ pull request. Small now; decide whether open working copies count toward a workspace's
455+ storage before agent pull requests reach thousands.
456+- Ask Cloudflare whether forks share objects physically, and for a higher account limit.
457+
458+Also found while testing (fixed in `git_http.rs`): the store answers a protocol v2 fetch that is
459+still negotiating, and whose `have`s it does not know, with `acknowledgments`, `NAK`, then a pack.
460+git refuses that ("expected no other sections to be sent after no 'ready'"). g1t now ends such
461+an answer after the acknowledgments with a flush, and the client negotiates again. Report it to
462+Cloudflare.
463+
416464 ### R7: making more namespaces (yours to run, when needed)
417465
418466 ```sh
+4−2
562562
563563 1. `node scripts/runner-release.mjs keygen`. Put `RUNNER_RELEASE_KEY` in the
564564 repository's secrets (production environment) and keep a copy offline;
565− put `G1T_RUNNER_RELEASE_KEY` in its variables. A build made without the
566− public key never updates itself.
565+ put the public key in its variables as `RUNNER_RELEASE_PUBLIC_KEY` (names
566+ starting `G1T_` are reserved; the workflow hands it to the build as
567+ `G1T_RUNNER_RELEASE_KEY`). A build made without the public key never
568+ updates itself.
567569 2. `npx wrangler r2 bucket create g1t-downloads`, and deploy the site so it
568570 has the `DOWNLOADS` binding.
569571 3. Set the variables `RUNNER_IMAGE` (the image's name in a public registry),
+13−0
1111 const WRANGLER = join(ROOT, "node_modules/wrangler/bin/wrangler.js");
1212 export const ACCOUNT_ID = "1e6f2cffa3f445920836e8ebe446bb58";
1313
14+/**
15+ * Headers for Cloudflare's REST and GraphQL APIs, for the ops scripts:
16+ * CLOUDFLARE_API_TOKEN as a bearer token, or else a global API key
17+ * (CLOUDFLARE_API_KEY with CLOUDFLARE_EMAIL). Null when neither is set.
18+ */
19+export function cloudflareAuth(env = process.env) {
20+ if (env.CLOUDFLARE_API_TOKEN) return { authorization: `Bearer ${env.CLOUDFLARE_API_TOKEN}` };
21+ if (env.CLOUDFLARE_API_KEY && env.CLOUDFLARE_EMAIL) {
22+ return { "x-auth-key": env.CLOUDFLARE_API_KEY, "x-auth-email": env.CLOUDFLARE_EMAIL };
23+ }
24+ return null;
25+}
26+
1427 /** What a deploy's version message starts with, followed by the commit. */
1528 export const MESSAGE_PREFIX = "g1t-deploy";
1629
+8−5
1616 // token needs D1: Read too), or with CLOUDFLARE_D1_TOKEN when that is set,
1717 // or as you are logged in (`npx wrangler login`) when neither has it.
1818
19−import { ACCOUNT_ID, exec, jsonFrom, wranglerEnv } from "../deploy/cloudflare.mjs";
19+import { ACCOUNT_ID, cloudflareAuth, exec, jsonFrom, wranglerEnv } from "../deploy/cloudflare.mjs";
2020 import { ROOT } from "../deploy/stack.mjs";
2121 import { join } from "node:path";
2222
3333 const days = Math.min(31, Math.max(1, Number(option("--days", "31")) || 31));
3434 const asJson = flag("--json");
3535
36−const token = process.env.CLOUDFLARE_API_TOKEN;
37−if (!token) {
38− console.error("Set CLOUDFLARE_API_TOKEN to a token with Account Analytics: Read on account " + ACCOUNT_ID + ".");
36+const auth = cloudflareAuth();
37+if (!auth) {
38+ console.error(
39+ "Set CLOUDFLARE_API_TOKEN to a token with Account Analytics: Read on account " + ACCOUNT_ID +
40+ ", or CLOUDFLARE_API_KEY and CLOUDFLARE_EMAIL.",
41+ );
3942 process.exit(2);
4043 }
4144
6265 }`;
6366 const response = await fetch("https://api.cloudflare.com/client/v4/graphql", {
6467 method: "POST",
65− headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
68+ headers: { ...auth, "content-type": "application/json" },
6669 body: JSON.stringify({ query, variables: { accountTag: ACCOUNT_ID, start: start.toISOString(), end: end.toISOString() } }),
6770 });
6871 const body = await response.json();
+8−5
1010 // against Cloudflare's API, never through g1t.sh.
1111 //
1212 // export CLOUDFLARE_API_TOKEN=<token: Artifacts edit, Account Analytics read>
13+// (or a global API key: CLOUDFLARE_API_KEY with CLOUDFLARE_EMAIL)
1314 // node scripts/ops/fork-storage-test.mjs run # make, fork 5x, measure for 20 min, delete
1415 // node scripts/ops/fork-storage-test.mjs run --keep # ... and keep it, to measure again tomorrow
1516 // node scripts/ops/fork-storage-test.mjs measure # read the figures again (e.g. the next day)
2829 import { tmpdir } from "node:os";
2930 import { join } from "node:path";
3031
32+import { cloudflareAuth } from "../deploy/cloudflare.mjs";
33+
3134 const ACCOUNT_ID = process.env.CLOUDFLARE_ACCOUNT_ID || "1e6f2cffa3f445920836e8ebe446bb58";
3235 const API = `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}`;
3336 const args = process.argv.slice(2);
4447 const SOURCE = "fork-test-source";
4548 const forkName = (n) => `fork-test-copy-${n}`;
4649
47−const token = process.env.CLOUDFLARE_API_TOKEN;
48−if (!token) {
49− console.error("Set CLOUDFLARE_API_TOKEN (Artifacts edit, Account Analytics read).");
50+const auth = cloudflareAuth();
51+if (!auth) {
52+ console.error("Set CLOUDFLARE_API_TOKEN (Artifacts edit, Account Analytics read), or CLOUDFLARE_API_KEY and CLOUDFLARE_EMAIL.");
5053 process.exit(2);
5154 }
5255
5356 async function api(method, path, body) {
5457 const response = await fetch(`${API}${path}`, {
5558 method,
56− headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
59+ headers: { ...auth, "content-type": "application/json" },
5760 body: body ? JSON.stringify(body) : undefined,
5861 });
5962 const json = await response.json().catch(() => ({}));
6366 async function graphql(query, variables = {}) {
6467 const response = await fetch("https://api.cloudflare.com/client/v4/graphql", {
6568 method: "POST",
66− headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
69+ headers: { ...auth, "content-type": "application/json" },
6770 body: JSON.stringify({ query, variables }),
6871 });
6972 const json = await response.json();