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.

173 linesCodeBlameRaw
1---
2title: Actions, runners and sandboxes
3description: How a workflow job is planned and run, on g1t's runners or yours, what walls each job in, how secrets reach a job without the job holding them, and what else runs in a sandbox.
4---
5
6import Aside from "../../../components/Aside.astro";
7import Diagram from "../../../components/Diagram.astro";
8
9Everything that runs code you did not write runs in a sandbox: workflow
10jobs, merge checks, deployments, dependency updates, backups. Three pieces
11share the work.
12
13| Piece | Is | Does |
14| ------------------ | -------------------------------------------- | ------------------------------------------------------------------------------------- |
15| `services/actions` | A Rust Worker, D1 `g1t-actions` | Reads workflows, starts runs, plans jobs, keeps logs, secrets and self-hosted runners |
16| `services/runner` | A TypeScript Worker on Cloudflare Containers | Starts a sandbox per job, and owns what leaves it |
17| `crates/runner` | The `g1t-runner` binary | Runs inside every sandbox and on every self-hosted runner, does one job, and exits |
18
19`crates/actions` holds the workflow language itself, with no I/O:
20syntax, `${{ }}` expressions, filters, matrices and cron. The Worker plans
21with it and the sandbox runs with it, so the two cannot read a workflow
22differently.
23
24## A workflow job
25
26<Diagram name="actions-job" />
27
281. **Trigger.** `actions` reads its event queue, `g1t-events-actions`, ten
29 at a time (step 1), and matches each event to the workflows in
30 `.g1t/workflows` at the commit the event is about (the default branch
31 for schedules, work items and manual runs; `services/actions/src/sync.rs`).
32 g1t never reads `.github/workflows`. The files are GitHub Actions
33 syntax, unchanged; [GitHub
34 Actions](/guides/actions/) lists what is supported.
352. **Plan.** It expands matrices, orders jobs by `needs`, and reports the
36 run as pending on the commit (steps 2 and 3).
373. **Start.** A hosted job is taken by flipping its row from `queued` to
38 `in_progress` in one statement, so two starters can never both take it.
39 `actions` asks `runner` for a sandbox (step 4), and `runner` starts a
40 Container for that job alone (step 5).
414. **Run.** The sandbox reports its steps and logs to the API, with the
42 job's own token (step 6), which passes them to `actions` (step 7). The
43 answer can tell the job to stop, which is how **Cancel** reaches a
44 running job (step 8).
455. **Finish.** The run's conclusion goes onto the commit as a status
46 (step 9), where [required checks](/architecture/work/#checks-and-statuses)
47 find it, and `workflow.completed` is published (step 10).
48
49A job that says nothing for 10 minutes is taken as lost. A cron in
50`actions` runs every minute for schedules, jobs waiting for room, and
51jobs whose runner went quiet.
52
53### Limits
54
55| | |
56| --------------------------- | ------------------------------------------------------------------------------------------------- |
57| Hosted jobs running at once | 4 per workspace always, up to 12 while the pool of 40 sandboxes keeps 8 free for other workspaces |
58| A hosted job's timeout | 60 minutes at most |
59| A self-hosted job's timeout | 24 hours at most |
60| Logs | 4 MiB a job, kept 90 days |
61| Workflows per repository | 50 |
62| Schedules | Paused after 60 days without a push |
63
64### Logs, caches and artifacts
65
66Logs are kept in D1, in chunks, deduplicated so a report sent twice is
67stored once. Cache entries and artifacts go to the R2 bucket
68`g1t-actions-cache`, uploaded through the API: cache entries under `c/`
69for 30 days, artifacts under `a/` for 91. `actions` lists them and decides
70what is kept.
71
72## Sandboxes on g1t's runners
73
74`runner` defines three Container classes, picked by `runs-on`, each a
75named Cloudflare instance type:
76
77| `runs-on` | Class | Size |
78| ---------------------------------------------------------------------------- | ---------------- | ----------------------------------------------- |
79| Any other hosted label in a private repository, and every other kind of work | `AttemptSandbox` | `standard-3`: 2 vCPU, 8 GiB memory, 16 GB disk |
80| `g1t-2core` | `Sandbox2Core` | `standard-3`: 2 vCPU, 8 GiB memory, 16 GB disk |
81| `g1t-4core`, and any other hosted label in a public repository | `Sandbox4Core` | `standard-4`: 4 vCPU, 12 GiB memory, 20 GB disk |
82
83`ubuntu-latest` follows GitHub's hosted runners: the actions service
84resolves it to `g1t-4core` when the run's repository is public (its
85event's `repository.private`), and the job is billed as that machine.
86`g1t-2core` is kept as a name of the standard machine, so workflows that
87ask for it keep working; it keeps its class and its own `max_instances`
88in `services/runner/wrangler.jsonc`. The contracts' instance table
89(`INSTANCE_TYPES`) and billing's price keeper say the same sizes, and a
90test holds them to `wrangler.jsonc`.
91
92Every sandbox starts from one image: `services/runner/base/Dockerfile`,
93which has Node, Python, Go, Rust, Java, .NET, Ruby and Docker, with the
94`g1t-runner` binary on top. Hosted runners are Linux only.
95
96<Diagram name="sandboxes" />
97
98### Egress
99
100A sandbox starts with no internet. Every HTTP and HTTPS request it makes
101goes through the runner Worker's outbound handler (`src/egress.ts`,
102`src/guard.ts`), which decides what may leave. Other protocols have no
103route out at all.
104
105### Secrets a job never holds
106
107A secret can be **brokered**: the job sees a stand-in, `g1tsec_…`, and
108the egress handler swaps in the real value only on a request to a host
109the secret allows (`src/broker.ts`). On a self-hosted runner, the job sends
110the request to `POST /actions/jobs/{job}/broker` instead, and g1t makes
111it, so the value never reaches your machine at all.
112
113## Runners of your own
114
115A [self-hosted runner](/guides/self-hosted-runners/) only ever calls out,
116over HTTPS, so it needs no open port:
117
118| Call | What it does |
119| ----------------------------- | -------------------------------------------------------------------------------------------- |
120| `POST /runners/register` | Trades a registration token, `g1trt_…`, valid for an hour, for a runner credential, `g1tr_…` |
121| `POST /runners/{id}/poll` | Waits up to 20 seconds for a job; doubles as the heartbeat |
122| `POST /runners/{id}/finished` | Says the job is over |
123| `POST /runners/{id}/remove` | Signs the runner out |
124
125The runner credential is stored only as a hash and rotates every day on a
126poll. A runner that has not polled for 90 seconds is offline; one offline
127for 14 days is removed. A job goes to a runner that has every one of its
128`runs-on` labels and whose group allows the repository, and waits up to 24
129hours for one.
130
131### Three walls
132
133On your machine, `g1t-runner` puts three walls between a job and the
134host:
135
1361. **A VM.** WSL2 on Windows, with drives, interop and the Windows `PATH`
137 turned off, vfkit on macOS, or QEMU on Linux (`deploy/sandbox`).
1382. **A Sysbox container**, the runner itself, with its own dockerd.
139 It refuses to start if the host's `docker.sock` is mounted
140 (`deploy/runner`).
1413. **A container per job**, new each time.
142
143Native Windows jobs run in Windows Sandbox instead. Each runner reports
144its isolation, `nested-container`, `vm`, `container` or `host`, and a
145`host` runner never gets a public repository's jobs. The [desktop
146app](/guides/desktop/) runs the sandboxed runner by default while it is
147open; Docker or direct execution happen only if you choose them.
148
149## Other work in sandboxes
150
151`g1t-runner` takes a `MODE`, so the same image does more than workflows:
152
153| Mode | Started by | Does |
154| ------------ | ------------------------------------- | ------------------------------------------------------------ |
155| `actions` | `actions` | A workflow job |
156| `mergecheck` | `runner`, when a pull request changes | Checks whether a pull request merges cleanly |
157| `deploy` | `deployments` | Builds an app ([Deployments](/architecture/deployments/)) |
158| `bump` | `security` | Makes a dependency update |
159| `backup` | `runner`'s sweep, every 5 minutes | Cuts a repository backup ([Git](/architecture/git/#backups)) |
160
161The merge queue needs no sandbox of its own: `work` builds each queued
162state in the repository itself ([Work](/architecture/work/#the-merge-queue)),
163and only the workflows that run on `merge_group` run in sandboxes, as
164workflow jobs.
165
166Compute is admitted through billing: a sandbox starts only after
167`billing` holds its estimated cost (`reserve`), and the hold is settled
168with the real cost when it ends (`settle`).
169
170<Aside type="caution" title="Self-hosted installations">
171 A [self-hosted g1t](/guides/self-hosting-architecture/) binds `runner` to a Worker that answers that it is off, so workflows, the merge queue and deployments
172 do not run there yet.
173</Aside>