Skip to content

Architecture

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.

Everything that runs code you did not write runs in a sandbox: workflow jobs, merge queue builds, deployments, dependency updates, backups. Three pieces share the work.

Piece Is Does
services/actions A Rust Worker, D1 g1t-actions Reads workflows, starts runs, plans jobs, keeps logs, secrets and self-hosted runners
services/runner A TypeScript Worker on Cloudflare Containers Starts a sandbox per job, and owns what leaves it
crates/runner The g1t-runner binary Runs inside every sandbox and on every self-hosted runner, does one job, and exits

crates/actions holds the workflow language itself, with no I/O: syntax, ${{ }} expressions, filters, matrices and cron. The Worker plans with it and the sandbox runs with it, so the two cannot read a workflow differently.

A workflow job on g1t’s runners, from the push that starts it to the status on the commitDiagram: A workflow job on g1t’s runners, from the push that starts it to the status on the commit 1. events ⇢ actions: git.push on g1t-events-actions 2. actions → actions: match workflows, plan the jobs 3. actions → work: set_commit_status pending 4. actions → runner: start_actions_job 5. runner → Sandbox: a Container for this job alone The job runs 6. Sandbox → API: POST /actions/jobs/{job}: steps, logs 7. API → actions: job_report 8. API ⇠ Sandbox: go on, or cancel - Note (runner, Sandbox): HTTP(S) leaves only through the runner’s egress handler The job ends 9. actions → work: set_commit_status success or failure 10. actions ⇢ events: workflow.completedeventsactionsservices/actionsrunnerservices/runnerSandboxg1t-runnerAPIapps/apiworkservices/workgit.push on g1t-events-actions12match workflows, plan the jobsset_commit_status pending3start_actions_job4a Container for this job alone5The job runsPOST /actions/jobs/{job}: steps, logs6job_report7go on, or cancel8HTTP(S) leaves only through the runner’s egress handlerThe job endsset_commit_status success or failure9workflow.completed10
A workflow job on g1t’s runners, from the push that starts it to the status on the commit
A workflow job on g1t’s runners, from the push that starts it to the status on the commit
A workflow job on g1t’s runners, from the push that starts it to the status on the commitDiagram: A workflow job on g1t’s runners, from the push that starts it to the status on the commit 1. events ⇢ actions: git.push on g1t-events-actions 2. actions → actions: match workflows, plan the jobs 3. actions → work: set_commit_status pending 4. actions → runner: start_actions_job 5. runner → Sandbox: a Container for this job alone The job runs 6. Sandbox → API: POST /actions/jobs/{job}: steps, logs 7. API → actions: job_report 8. API ⇠ Sandbox: go on, or cancel - Note (runner, Sandbox): HTTP(S) leaves only through the runner’s egress handler The job ends 9. actions → work: set_commit_status success or failure 10. actions ⇢ events: workflow.completedeventsactionsservices/actionsrunnerservices/runnerSandboxg1t-runnerAPIapps/apiworkservices/workgit.push on g1t-events-actions12match workflows, plan the jobsset_commit_status pending3start_actions_job4a Container for this job alone5The job runsPOST /actions/jobs/{job}: steps, logs6job_report7go on, or cancel8HTTP(S) leaves only through the runner’s egress handlerThe job endsset_commit_status success or failure9workflow.completed10
  1. Trigger. actions reads its event queue, g1t-events-actions, ten at a time (step 1), and matches each event to the workflows read from the default branch’s .g1t/workflows (or .github/workflows). The files are GitHub Actions syntax, unchanged; GitHub Actions lists what is supported.
  2. Plan. It expands matrices, orders jobs by needs, and reports the run as pending on the commit (steps 2 and 3).
  3. Start. A hosted job is taken by flipping its row from queued to in_progress in one statement, so two starters can never both take it. actions asks runner for a sandbox (step 4), and runner starts a Container for that job alone (step 5).
  4. Run. The sandbox reports its steps and logs to the API, with the job’s own token (step 6), which passes them to actions (step 7). The answer can tell the job to stop, which is how Cancel reaches a running job (step 8).
  5. Finish. The run’s conclusion goes onto the commit as a status (step 9), where required checks find it, and workflow.completed is published (step 10).

A job that says nothing for 10 minutes is taken as lost. A cron in actions runs every minute for schedules, jobs waiting for room, and jobs whose runner went quiet.

Hosted jobs running at once 4 per workspace
A hosted job’s timeout 60 minutes at most
A self-hosted job’s timeout 24 hours at most
Logs 4 MiB a job, kept 90 days
Workflows per repository 50
Schedules Paused after 60 days without a push

Logs are kept in D1, in chunks, deduplicated so a report sent twice is stored once. Cache entries and artifacts go to the R2 bucket g1t-actions-cache, uploaded through the API: cache entries under c/ for 30 days, artifacts under a/ for 91. actions lists them and decides what is kept.

runner defines three Container classes, picked by runs-on, each a named Cloudflare instance type:

runs-on Class Size
Any other hosted label, and every other kind of work AttemptSandbox standard-3: 2 vCPU, 8 GiB memory, 16 GB disk
g1t-2core Sandbox2Core standard-3: 2 vCPU, 8 GiB memory, 16 GB disk
g1t-4core Sandbox4Core standard-4: 4 vCPU, 12 GiB memory, 20 GB disk

g1t-2core is kept as a name of the standard machine, so workflows that ask for it keep working; it keeps its class and its own max_instances in services/runner/wrangler.jsonc. The contracts’ instance table (INSTANCE_TYPES) and billing’s price keeper say the same sizes, and a test holds them to wrangler.jsonc.

Every sandbox starts from one image: services/runner/base/Dockerfile, which has Node, Python, Go, Rust, Java, .NET, Ruby and Docker, with the g1t-runner binary on top. Hosted runners are Linux only.

Where a job runs: in a Container on g1t’s runners, or inside three walls on a machine of yoursDiagram: Where a job runs: in a Container on g1t’s runners, or inside three walls on a machine of yours - g1t’s runners: Cloudflare Containers: g1t-runner (does one job, exits); its own dockerd (when DOCKER=on); egress handler (all HTTP(S) leaves here); allowed hosts (registries, your APIs) - Your machine: three walls: job container (new for every job); its own dockerd (never the host’s docker.sock); api.g1t.sh (long poll, outbound) Arrows: - g1t-runner → egress handler - egress handler → allowed hosts: g1tsec_… → the secret - Sysbox container: g1t-runner → api.g1t.shg1t’s runners: Cloudflare Containersone sandbox per jobYour machine: three wallsVM: WSL2, vfkit or QEMUSysbox container: g1t-runnerg1t-runnerdoes one job, exitsits own dockerdwhen DOCKER=onegress handlerall HTTP(S) leaves hereallowed hostsregistries, your APIsjob containernew for every jobits own dockerdnever the host’s docker.sockapi.g1t.shlong poll, outboundg1tsec_… → the secret
Where a job runs: in a Container on g1t’s runners, or inside three walls on a machine of yours
Where a job runs: in a Container on g1t’s runners, or inside three walls on a machine of yours
Where a job runs: in a Container on g1t’s runners, or inside three walls on a machine of yoursDiagram: Where a job runs: in a Container on g1t’s runners, or inside three walls on a machine of yours - g1t’s runners: Cloudflare Containers: g1t-runner (does one job, exits); its own dockerd (when DOCKER=on); egress handler (all HTTP(S) leaves here); allowed hosts (registries, your APIs) - Your machine: three walls: job container (new for every job); its own dockerd (never the host’s docker.sock); api.g1t.sh (long poll, outbound) Arrows: - g1t-runner → egress handler - egress handler → allowed hosts: g1tsec_… → the secret - Sysbox container: g1t-runner → api.g1t.shg1t’s runners: Cloudflare Containersone sandbox per jobYour machine: three wallsVM: WSL2, vfkit or QEMUSysbox container: g1t-runnerg1t-runnerdoes one job, exitsits own dockerdwhen DOCKER=onegress handlerall HTTP(S) leaves hereallowed hostsregistries, your APIsjob containernew for every jobits own dockerdnever the host’s docker.sockapi.g1t.shlong poll, outboundg1tsec_… → the secret

A sandbox starts with no internet. Every HTTP and HTTPS request it makes goes through the runner Worker’s outbound handler (src/egress.ts, src/guard.ts), which decides what may leave. Other protocols have no route out at all.

A secret can be brokered: the job sees a stand-in, g1tsec_…, and the egress handler swaps in the real value only on a request to a host the secret allows (src/broker.ts). On a self-hosted runner, the job sends the request to POST /actions/jobs/{job}/broker instead, and g1t makes it, so the value never reaches your machine at all.

A self-hosted runner only ever calls out, over HTTPS, so it needs no open port:

Call What it does
POST /runners/register Trades a registration token, g1trt_…, valid for an hour, for a runner credential, g1tr_…
POST /runners/{id}/poll Waits up to 20 seconds for a job; doubles as the heartbeat
POST /runners/{id}/finished Says the job is over
POST /runners/{id}/remove Signs the runner out

The runner credential is stored only as a hash and rotates every day on a poll. A runner that has not polled for 90 seconds is offline; one offline for 14 days is removed. A job goes to a runner that has every one of its runs-on labels and whose group allows the repository, and waits up to 24 hours for one.

On your machine, g1t-runner puts three walls between a job and the host:

  1. A VM. WSL2 on Windows, with drives, interop and the Windows PATH turned off, vfkit on macOS, or QEMU on Linux (deploy/sandbox).
  2. A Sysbox container, the runner itself, with its own dockerd. It refuses to start if the host’s docker.sock is mounted (deploy/runner).
  3. A container per job, new each time.

Native Windows jobs run in Windows Sandbox instead. Each runner reports its isolation, nested-container, vm, container or host, and a host runner never gets a public repository’s jobs. The desktop app runs the sandboxed runner by default while it is open; Docker or direct execution happen only if you choose them.

g1t-runner takes a MODE, so the same image does more than workflows:

Mode Started by Does
actions actions A workflow job
queue runner, when the queue changes Builds a merge queue candidate
mergecheck runner, when a pull request changes Checks whether a pull request merges cleanly
deploy deployments Builds an app (Deployments)
bump security Makes a dependency update
backup runner’s sweep, every 5 minutes Cuts a repository backup (Git)

Compute is admitted through billing: a sandbox starts only after billing holds its estimated cost (reserve), and the hold is settled with the real cost when it ends (settle).