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.

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