| 1 | --- |
| 2 | title: Actions, runners and sandboxes |
| 3 | description: 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 | |
| 6 | import Aside from '../../../components/Aside.astro'; |
| 7 | import Diagram from '../../../components/Diagram.astro'; |
| 8 | |
| 9 | Everything that runs code you did not write runs in a sandbox: workflow |
| 10 | jobs, merge queue builds, deployments, dependency updates, backups. Three |
| 11 | pieces 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: |
| 20 | syntax, `${{ }}` expressions, filters, matrices and cron. The Worker plans |
| 21 | with it and the sandbox runs with it, so the two cannot read a workflow |
| 22 | differently. |
| 23 | |
| 24 | ## A workflow job |
| 25 | |
| 26 | <Diagram name="actions-job" /> |
| 27 | |
| 28 | 1. **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. |
| 33 | 2. **Plan.** It expands matrices, orders jobs by `needs`, and reports the |
| 34 | run as pending on the commit (steps 2 and 3). |
| 35 | 3. **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). |
| 39 | 4. **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). |
| 43 | 5. **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 | |
| 47 | A 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 |
| 49 | jobs 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 | |
| 64 | Logs are kept in D1, in chunks, deduplicated so a report sent twice is |
| 65 | stored once. Cache entries and artifacts go to the R2 bucket |
| 66 | `g1t-actions-cache`, uploaded through the API: cache entries under `c/` |
| 67 | for 30 days, artifacts under `a/` for 91. `actions` lists them and decides |
| 68 | what is kept. |
| 69 | |
| 70 | ## Sandboxes on g1t's runners |
| 71 | |
| 72 | `runner` defines three Container classes, picked by `runs-on`, each a |
| 73 | named 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 |
| 82 | ask for it keep working; it keeps its class and its own `max_instances` |
| 83 | in `services/runner/wrangler.jsonc`. The contracts' instance table |
| 84 | (`INSTANCE_TYPES`) and billing's price keeper say the same sizes, and a |
| 85 | test holds them to `wrangler.jsonc`. |
| 86 | |
| 87 | Every sandbox starts from one image: `services/runner/base/Dockerfile`, |
| 88 | which 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 | |
| 95 | A sandbox starts with no internet. Every HTTP and HTTPS request it makes |
| 96 | goes through the runner Worker's outbound handler (`src/egress.ts`, |
| 97 | `src/guard.ts`), which decides what may leave. Other protocols have no |
| 98 | route out at all. |
| 99 | |
| 100 | ### Secrets a job never holds |
| 101 | |
| 102 | A secret can be **brokered**: the job sees a stand-in, `g1tsec_…`, and |
| 103 | the egress handler swaps in the real value only on a request to a host |
| 104 | the secret allows (`src/broker.ts`). On a self-hosted runner, the job sends |
| 105 | the request to `POST /actions/jobs/{job}/broker` instead, and g1t makes |
| 106 | it, so the value never reaches your machine at all. |
| 107 | |
| 108 | ## Runners of your own |
| 109 | |
| 110 | A [self-hosted runner](/guides/self-hosted-runners/) only ever calls out, |
| 111 | over 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 | |
| 120 | The runner credential is stored only as a hash and rotates every day on a |
| 121 | poll. A runner that has not polled for 90 seconds is offline; one offline |
| 122 | for 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 |
| 124 | hours for one. |
| 125 | |
| 126 | ### Three walls |
| 127 | |
| 128 | On your machine, `g1t-runner` puts three walls between a job and the |
| 129 | host: |
| 130 | |
| 131 | 1. **A VM.** WSL2 on Windows, with drives, interop and the Windows `PATH` |
| 132 | turned off, vfkit on macOS, or QEMU on Linux (`deploy/sandbox`). |
| 133 | 2. **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`). |
| 136 | 3. **A container per job**, new each time. |
| 137 | |
| 138 | Native Windows jobs run in Windows Sandbox instead. Each runner reports |
| 139 | its isolation, `nested-container`, `vm`, `container` or `host`, and a |
| 140 | `host` runner never gets a public repository's jobs. The [desktop |
| 141 | app](/guides/desktop/) runs the sandboxed runner by default while it is |
| 142 | open; 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 | |
| 157 | Compute is admitted through billing: a sandbox starts only after |
| 158 | `billing` holds its estimated cost (`reserve`), and the hold is settled |
| 159 | with 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> |