Skip to content
1,443 linesCodeBlameRaw
1---
2title: GitHub Actions
3description: Your GitHub Actions workflows run on g1t as they are. Rename .github to .g1t and push.
4---
5
6g1t runs GitHub Actions workflows. They are written exactly as on GitHub,
7and kept in `.g1t/workflows/` instead of `.github/workflows/`.
8
9## Moving from GitHub
10
11```sh
12git mv .github .g1t
13git commit -m "Run our workflows on g1t"
14git push g1t main
15```
16
17That is the whole move. Everything in the folder comes along: workflows,
18local actions under `.g1t/actions/` and anything else you keep there.
19Workflows that still say `uses: ./.github/actions/setup` find it under
20`.g1t/` once `.github` is gone.
21
22g1t never reads `.github`. A repository mirrored to both places can keep
23`.github` for GitHub and `.g1t` for g1t, side by side.
24
25Then add your [secrets and variables](#secrets-and-variables): GitHub never
26gives their values out, so they cannot be copied across.
27
28## What runs
29
30| On GitHub | On g1t |
31| --- | --- |
32| `on:` `push` (branches, tags, paths), `pull_request`, `pull_request_target`, `issues`, `issue_comment`, `pull_request_review`, `schedule`, `workflow_dispatch`, `workflow_run`, `merge_group`, `create`, `repository_dispatch`, `release`, `deployment`, `deployment_status` | The same, from g1t's own pushes, pull requests, issues, comments, [releases](#releases), [deployments](#deployments) and [merge queue](/guides/merge-queue/). `create` starts on each new branch or tag; `repository_dispatch` on [a dispatch event](#repository-dispatch). |
33| `jobs`, `needs`, `if`, `outputs`, `env`, `defaults`, `timeout-minutes`, `continue-on-error` | The same. |
34| `timeout-minutes` and `continue-on-error` on a step | The same, for `run:` and `uses:` steps alike. A `uses:` step's action is stopped at its limit, with every process it started; a step inside a composite action stops at its own limit or the `uses:` step's, whichever comes first. A step stopped this way fails, unless `continue-on-error` lets the job go on. |
35| `strategy.matrix` with `include` and `exclude`, `fail-fast`, `max-parallel`, a matrix from `fromJSON(needs.…)` | The same. |
36| `concurrency` with `cancel-in-progress`, for the workflow or for one job | The same: one run, or one job, of a group at a time. |
37| `permissions:` for the workflow or for one job, `read-all`, `write-all` | The same: they decide what [the job's token](#the-jobs-token) may do. |
38| `${{ }}` expressions: every operator, function and context | The same, including `hashFiles`, `success()`, `failure()`, `always()` and `cancelled()`. |
39| `run:` with `bash`, `sh`, `python` or a custom shell | The same. |
40| JavaScript actions (`uses: owner/repo@v7`, `owner/repo/path@v7`) | Fetched from that repository on g1t when g1t has it and your repository may use it, otherwise from GitHub, and run as they are, on Node 24, the runtime current actions declare. See [actions and workflows from other repositories](#actions-and-workflows-from-other-repositories). |
41| Composite actions | The same. |
42| Reusable workflows (`jobs.<id>.uses: ./.g1t/workflows/build.yml`, or `owner/repo/.g1t/workflows/build.yml@v1` in another repository) | The same: `with:` inputs, `secrets:` by name or `secrets: inherit`, `on.workflow_call` outputs, and nesting up to four deep. `.github/workflows/…` finds the workflow under `.g1t/` after the move. See [actions and workflows from other repositories](#actions-and-workflows-from-other-repositories). |
43| `actions/checkout` | Checks out from g1t, with `ref`, `fetch-depth`, `path`, `repository`, `token` and `submodules`. |
44| `GITHUB_OUTPUT`, `GITHUB_ENV`, `GITHUB_PATH`, `GITHUB_STATE`, `GITHUB_STEP_SUMMARY` | The same. Step summaries show on the run's page; see [job summaries](#job-summaries). |
45| `::error::`, `::warning::`, `::notice::`, `::group::`, `::add-mask::` | The same: errors and warnings become annotations on the run, and [masked](#masking-secrets) values stay hidden. |
46| `secrets.*`, `vars.*`, `secrets.GITHUB_TOKEN` | The same. `secrets.G1T_TOKEN` is [the job's own token](#the-jobs-token); `GITHUB_TOKEN` is its alias. |
47| `environment:` on a job | The job waits for the environment's [protection rules](#environments), then reads each key's row for that environment, as environment secrets work, and the run records a [deployment](/guides/deployments-api/#deployments-from-g1t-actions) to it. `url` gives the deployment its address; `deployment: false` reads the environment's values without making one. The name may be an expression. |
48| `actions/upload-artifact`, `actions/download-artifact`, `actions/upload-artifact/merge` | The same inputs and outputs as version 4: `retention-days`, `overwrite`, `compression-level`, `include-hidden-files`, `!` exclusions, download by `pattern` with `merge-multiple`, and from another run with `run-id` and `github-token`. Up to 5 GiB each; see [artifacts](#artifacts). |
49| `actions/cache`, `actions/cache/restore`, `actions/cache/save` | Kept per repository and branch, found by `key` or the newest under a `restore-keys` prefix. `path` takes globs and `!` exclusions. Up to 2 GiB each; see [the cache](#the-cache). |
50| Actions that cache through the toolkit, such as `actions/setup-node` with `cache: npm` or `Swatinem/rust-cache` | The same: they save to and restore from the repository's cache. See [actions built on the toolkit](#actions-built-on-the-toolkit). |
51| sccache with `SCCACHE_GHA_ENABLED` | The same: its entries go to the repository's cache. See [caching Rust builds](#caching-rust-builds). |
52| `permissions: id-token: write` | The job can ask for an OIDC token, and trade it for a cloud provider's credentials. See [OIDC tokens](#oidc-tokens). |
53| `docker build`, `push`, `run`, `login`, `compose`, Buildx | The same, with a Docker Engine of the job's own. See [Docker](#docker). |
54| `services:` | The same: each service starts before the steps, health checks are waited for, and it is reached at `localhost` on its port and by its name. |
55| `container:` | The same: every step runs inside the image. |
56| `uses: docker://image`, Docker actions (`runs.using: docker`) | The same: built from the action's Dockerfile or pulled, and run with GitHub's `/github/workspace` layout. |
57| `docker/setup-buildx-action`, `docker/build-push-action`, `docker/login-action` | The same. `setup-buildx-action` picks the job's own Engine as the builder. |
58
59The **Actions** page of a workflow says, under *How this runs on g1t*,
60anything in it that runs differently.
61
62### Not yet
63
64- **Windows and macOS on g1t's machines.** g1t's own runners are Linux; a
65 job with `runs-on: windows-latest` or `macos-latest` fails, and says so.
66 [Self-hosted runners](/guides/self-hosted-runners/) of any OS run them:
67 `runs-on: [self-hosted, windows]`.
68- **Docker's `type=gha` build cache.** Buildx skips it on g1t, and the
69 build runs without a cache. Use a registry cache instead; see
70 [caching image builds](#caching-image-builds).
71- **Actions that upload artifacts with the toolkit's artifact library
72 themselves.** The library refuses to run against any server but
73 github.com. `actions/upload-artifact`, `actions/download-artifact` and
74 `actions/upload-artifact/merge` work, because g1t runs them itself. See
75 [actions built on the toolkit](#actions-built-on-the-toolkit).
76- **`on: delete`.** Deleting a branch or tag starts no workflows yet; the
77 workflow's page says so.
78
79Why each of these is missing, and what to use instead, is on
80[What g1t can't do yet](/about/limitations/#actions-and-runners).
81
82## Schedules
83
84A workflow with `on: schedule` runs on the default branch's latest commit,
85at each time its cron lines name, in UTC:
86
87```yaml
88on:
89 schedule:
90 - cron: "0 9 * * mon" # Mondays at 09:00 UTC
91 - cron: "*/30 * * * *" # every 30 minutes
92```
93
94Each line has five fields: minute, hour, day of month, month and day of
95the week. A field takes `*`, a number, a range `1-5`, a list `1,15` and a
96step `*/10`; days take `mon` to `sun`, and months `jan` to `dec`.
97
98| Rule | What happens |
99| --- | --- |
100| Every 5 minutes at most | A schedule more frequent than every 5 minutes, such as `* * * * *` or `*/2 * * * *`, runs every 5 minutes instead, on the five-minute marks (:00, :05, :10 and so on), at each mark that ends five minutes in which it would have run. A mark outside the hours, days or months the schedule names never runs: `* 9 * * *` runs from 09:00 to 09:55. |
101| No push for 60 days | Schedules pause in a repository that has had no push for 60 days. The next push to any branch resumes them. Other events and `workflow_dispatch` still start the workflow. |
102| Actions not paid for | When a scheduled run's job could not start because the workspace's plan, spend limit or the open-source pool does not cover it, that run fails and says why, and the workflow's schedule waits an hour before it tries again. |
103| Archived repository | Schedules wait until the repository is unarchived. |
104
105## Actions and workflows from other repositories
106
107A step's `uses: owner/repo@ref` (or `owner/repo/path@ref`) and a job's
108`uses: owner/repo/.g1t/workflows/build.yml@ref` name another repository.
109g1t looks for it on g1t first:
110
111| The repository | What happens |
112| --- | --- |
113| On g1t and public | Your workflows use it, from any workspace. |
114| On g1t, private, in your workspace, with **Access** set to *Accessible from repositories in* the workspace | Your private repositories' workflows use it. A job reads it with a read-only token for that repository alone, which ends with the job. |
115| On g1t, private, and not shared that way | The step or job fails, and says why. A private repository's actions are never used by a public repository's workflows, whose logs anyone can read, nor from another workspace. |
116| Not on g1t, or private in a workspace you cannot see | An action is fetched from GitHub, as before; a reusable workflow is read from a public repository on GitHub. |
117
118`ref` is a branch, a tag or a commit. A reusable workflow may be under
119`.g1t/workflows/` or `.github/workflows/`; a `.github/workflows/` path
120also finds the file under `.g1t/workflows/` in a repository moved to
121g1t. A `./.g1t/workflows/…` call inside a workflow from another
122repository reads from that repository, at the same ref.
123
124To share a private repository's actions and workflows with the rest of its
125workspace, an admin chooses **Settings → Actions → Access → Accessible from
126repositories in** the workspace, or calls
127`PUT /repos/{owner}/{repo}/actions/permissions/access` with
128`{"access_level": "organization"}` (`none` to stop).
129
130### Secrets for a called workflow
131
132A called workflow gets only the secrets its caller passes, plus
133`G1T_TOKEN` (`GITHUB_TOKEN`):
134
135```yaml
136jobs:
137 build:
138 uses: acme/shared/.g1t/workflows/build.yml@v2
139 with:
140 node-version: 24
141 secrets:
142 npm-token: ${{ secrets.NPM_TOKEN }}
143
144 deploy:
145 uses: ./.g1t/workflows/deploy.yml
146 secrets: inherit
147```
148
149- `secrets:` with names passes each as the called workflow names it,
150 read from the caller's `secrets`, `needs`, `inputs`, `matrix`,
151 `github` and `vars`.
152- `secrets: inherit` passes every secret the caller has.
153- A job in the called workflow with its own `environment:` also reads that
154 environment's secrets, over what was passed.
155- A secret the called workflow marks `required: true` under
156 `on.workflow_call.secrets` that the caller does not pass fails the
157 calling job before anything runs.
158
159`vars` are the calling repository's, and a called workflow's jobs run
160with the calling run's `github` context: `actions/checkout` checks out the
161calling repository.
162
163## Releases
164
165Workflows with `on: release` start when a release changes, at the commit
166its tag names (`GITHUB_REF` is `refs/tags/<tag>`). Each change is one or
167more activity types, which `types:` chooses among:
168
169| Change | Activity types |
170| --- | --- |
171| A draft made | `created` |
172| A release made and published | `created`, `published`, and `released` (or `prereleased` for a prerelease) |
173| A draft published | `published`, and `released` or `prereleased` |
174| A prerelease made a full release | `edited` and `released` |
175| Made a draft again | `unpublished` |
176| Title, notes or prerelease changed | `edited`, with `github.event.changes` holding the old title and notes |
177| Deleted (the tag stays) | `deleted` |
178
179```yaml
180on:
181 release:
182 types: [published]
183```
184
185`github.event.release` has `tag_name`, `name`, `body`, `draft`,
186`prerelease`, `target_commitish`, `author` and `html_url`. A release a
187job's own token makes or changes starts no workflows.
188
189## Deployments
190
191`on: deployment` starts when a deployment is made, and
192`on: deployment_status` when one has a new status: one reported through
193the [deployments API](/guides/deployments-api/) or a
194[g1t.page](/guides/deployments/) build. The run is at the commit deployed;
195`GITHUB_REF` is the branch or tag deployed, and empty for a bare commit.
196
197```yaml
198on: deployment_status
199
200jobs:
201 smoke:
202 if: github.event.deployment_status.state == 'success'
203 runs-on: ubuntu-latest
204 steps:
205 - run: curl -fsS "${{ github.event.deployment_status.environment_url }}"
206```
207
208`github.event.deployment` has `environment`, `ref`, `sha`, `task` and
209`payload`; `github.event.deployment_status` has `state`, `environment_url`
210and `log_url`. Deployments a workflow makes, with `environment:` or with
211its job's token, start no workflows, so a workflow cannot set itself off.
212
213## The runner
214
215Jobs run in a fresh sandbox each: Debian with Node 24, Python 3, Go, Rust,
216Java 21, .NET 8, Ruby 3.3, `build-essential`, `git`, `curl`, `jq`, Docker
217(with Buildx and Compose) and passwordless `sudo`, in GitHub's layout
218(`/home/runner/work`, `RUNNER_TEMP`, `RUNNER_TOOL_CACHE`).
219`runner.os` is `Linux`. `ubuntu-latest`, `ubuntu-24.04` and other Linux
220labels all run here. A job whose `runs-on` names `self-hosted` waits for one
221of your [self-hosted runners](/guides/self-hosted-runners/) instead. Setup actions such as
222`actions/setup-node` and `actions/setup-python` install other versions as
223they do on GitHub.
224
225### Languages and their setup actions
226
227Each language below is on `PATH` from the job's first step, so a workflow
228that only runs `java`, `dotnet` or `ruby` needs no setup step. When it has
229one, the setup action finds the version that is already there and
230downloads nothing.
231
232| Language | Version | Where | Setup action |
233| --- | --- | --- | --- |
234| Java | Eclipse Temurin 21 (LTS), JDK | `JAVA_HOME` (also `JAVA_HOME_21_X64`), in `RUNNER_TOOL_CACHE` | `actions/setup-java` with `distribution: temurin` and `java-version: 21` uses it. Other versions and distributions are downloaded. |
235| .NET | SDK 8 (LTS) | `DOTNET_ROOT`, `/usr/share/dotnet` | `actions/setup-dotnet` with `dotnet-version: 8.0.x` keeps it when it is the newest 8.0 SDK, and installs other SDKs beside it. |
236| Ruby | 3.3, with Bundler | in `RUNNER_TOOL_CACHE` | `ruby/setup-ruby` with `ruby-version: '3.3'` (or a `.ruby-version` naming 3.3) uses it. |
237| Node | 24 | `/usr/local/bin` | `actions/setup-node` installs other versions. |
238| Python | 3.11 | `/usr/bin/python3` | `actions/setup-python` installs other versions. |
239| Go | 1.27 | `/usr/local/go` | `actions/setup-go` installs other versions. |
240| Rust | stable, with `rustfmt`, `clippy` and the `wasm32-unknown-unknown` target | `~/.cargo/bin` | `rustup` is there to add toolchains and targets. |
241
242```yaml
243steps:
244 - uses: actions/checkout@v5
245 - uses: actions/setup-java@v5
246 with:
247 distribution: temurin
248 java-version: 21
249 - run: ./gradlew build
250```
251
252Because the sandbox runs Debian, `ruby/setup-ruby` treats it as a
253self-hosted runner and uses only the Rubies in `RUNNER_TOOL_CACHE`. A version other than 3.3 fails at that step; install
254it in a `run` step instead (for example with `ruby-build`) or run the job
255in a `container:` with the Ruby you need, such as `ruby:3.4`.
256
257The headers that gems and .NET need to build native code (`libyaml`,
258`libffi`, `zlib`, OpenSSL, ICU) are installed too.
259
260### Calling g1t's API from a job
261
262The `gh` command is not installed: it needs a GraphQL API, and g1t's API
263is REST. Call it with `curl`, using the job's token and the API's address,
264which every job has as `GITHUB_API_URL`:
265
266```yaml
267- name: Comment on the pull request
268 env:
269 TOKEN: ${{ github.token }}
270 run: |
271 curl -fsS -X POST "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/issues/${{ github.event.number }}/comments" \
272 -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
273 -d '{"body": "Built."}'
274```
275
276The token reaches this repository and does what the job's `permissions:`
277say; see [the job's token](#the-jobs-token). The
278[API reference](/reference/api/) lists every endpoint.
279
280### Machine sizes
281
282A job runs on the standard machine unless its `runs-on` names a larger
283one:
284
285| `runs-on` | vCPUs | Memory | Disk |
286| --- | --- | --- | --- |
287| `ubuntu-latest`, or any other Linux label | 0.5 | 4 GiB | 8 GB |
288| `g1t-2core` | 2 | 8 GiB | 16 GB |
289| `g1t-4core` | 4 | 12 GiB | 20 GB |
290
291```yaml
292jobs:
293 build:
294 runs-on: g1t-4core
295```
296
297The label can come from the matrix or the run's inputs
298(`runs-on: ${{ matrix.big && 'g1t-4core' || 'ubuntu-latest' }}`). A
299larger machine costs what it costs g1t, plus the same margin as all
300sandbox time: see [usage and billing](/guides/usage-and-billing/#workflow-jobs-on-larger-machines).
301Builds that compile, such as Rust or a large TypeScript project, finish
302several times faster on one.
303
304A job on g1t's machines runs for at most 60 minutes, whatever its
305`timeout-minutes`; one on a self-hosted runner can run for up to 24 hours.
306A job stopped at its time cap fails saying so.
307
308### What a job can reach
309
310A job's network is restricted, as an agent's is (see
311[guardrails](/guides/guardrails/)): it reaches the hosts its project's
312guardrails allow, g1t itself, and what builds need, and nothing else.
313What builds need is the package registries (npm, PyPI, crates.io, the Go
314proxy, RubyGems, Packagist, NuGet, Maven and Gradle, Debian's mirrors),
315GitHub, where `uses:` actions and the setup actions' downloads come from,
316the toolchains' download sites (`nodejs.org`, `go.dev`,
317`static.rust-lang.org`), and the public container registries (Docker Hub,
318GitHub's, Quay, and `mirror.gcr.io`, the mirror of Docker Hub that a job's
319Engine asks first). A request anywhere else gets `403` with
320the reason. To reach another host, someone with the Maintain [role](/guides/access-and-roles/) or
321higher adds it to the project's
322allowed domains under **Settings → Guardrails**; a project whose guardrails
323turn the network restriction off runs its jobs with an open network.
324
325A host only workflows should reach, such as the API a deploy uploads to,
326goes in **Workflow-only domains** instead, limited to the workflows and
327environments that need it: `api.cloudflare.com | deploy.yml | production`
328lets only `deploy.yml`'s jobs with `environment: production` reach it.
329Agents never reach those hosts, and neither do runs of pull requests from
330forks. See [workflow-only domains](/guides/guardrails/#workflow-only-domains).
331
332g1t does not run cryptocurrency miners: a step that names one (`xmrig`,
333a `stratum+tcp://` pool, `--donate-level`) is not run, and a job that
334looks like it is mining is stopped. See
335[abuse and mining](/guides/guardrails/#abuse-and-mining).
336
337## Docker
338
339Each job on g1t's machines has a Docker Engine of its own, inside the
340job's sandbox. Nothing runs until the job uses it: the first `docker`
341command, or a job's `services:` or `container:`, starts it, in a second
342or two, and the log says so. It ends with the job, with every image,
343container and build cache in it. No other job, repository or workspace
344ever shares it.
345
346```yaml
347jobs:
348 test:
349 runs-on: ubuntu-latest
350 services:
351 postgres:
352 image: postgres:17
353 env:
354 POSTGRES_PASSWORD: ${{ secrets.DB_PASSWORD }}
355 ports: ["5432:5432"]
356 options: >-
357 --health-cmd pg_isready --health-interval 5s --health-retries 10
358 steps:
359 - uses: actions/checkout@v5
360 - run: docker compose up -d --wait
361 - run: npm test
362 env:
363 DATABASE_URL: postgres://postgres:${{ secrets.DB_PASSWORD }}@localhost:5432/postgres
364```
365
366### What works
367
368| | On g1t's machines |
369| --- | --- |
370| `docker build`, `buildx build`, `run`, `exec`, `pull`, `push`, `login`, `compose` | Work as they do on GitHub's runners. The Engine, Buildx and Compose are current releases. |
371| `services:` | Pulled and started before the first step, with `env`, `ports`, `volumes`, `options` and `credentials`. Services with a health check are waited for; one that turns unhealthy fails the job with its log. Each service's log is printed when the job ends. `job.services.<id>.id`, `.network` and `.ports` are set. |
372| `container:` | Every `run` step and JavaScript action runs inside the image, with its `env`, `options`, `volumes` and `credentials`. The workspace, `RUNNER_TEMP` and the tool cache are mounted at the same paths as on g1t's runner. |
373| `uses: docker://image` | Pulled and run, with `with.args` and `with.entrypoint`. |
374| Docker actions | Built from the action's Dockerfile (or pulled, for `image: docker://…`), and run with its `args`, `env` and `entrypoint`, its inputs as `INPUT_*` variables, and `pre-entrypoint` and `post-entrypoint`. |
375| `docker/setup-buildx-action` | Selects the job's own Engine as the builder (BuildKit). Its `name`, `driver`, `platforms` and `nodes` outputs are set. `driver`, `driver-opts` and `buildkitd-*` are not used, and the log says so. |
376| `docker/build-push-action` | Works, with `push`, `load`, `tags`, `labels`, `build-args`, `secrets`, `target`, `provenance` and `sbom`. |
377| `docker/login-action` | Works, for g1t's registry, Docker Hub, GitHub's registry, Cloudflare's (`registry.cloudflare.com`) and any registry the job can reach. |
378
379### Services and the network
380
381Every container a job starts shares the job's own network, the one its
382[guardrails](/guides/guardrails/) apply to. So:
383
384- **A service is at `localhost`** on its port, from steps and from other
385 containers. `ports: ["5432:5432"]` and `ports: ["5432"]` both mean
386 `localhost:5432`.
387- **A port mapped to another number** (`ports: ["6543:5432"]`, or
388 `docker run -p 8080:80`) is forwarded: `localhost:6543` reaches the
389 service's 5432. `job.services.<id>.ports` says which port to use, and
390 `docker inspect` and `docker port` report it.
391- **A service is also reached by its name**, as it is from a job
392 container on GitHub: `postgres:5432` works from steps, from the job's
393 container and from any container started later. So do the names of
394 containers and Compose services, and their network aliases.
395- **Two containers cannot listen on the same port.** A job with a
396 `redis` service and a Compose file that starts another Redis on 6379
397 gets an error from the second; give one of them another port.
398
399A container that asks for `--network none` gets none, and
400`--network container:<name>` shares that container's.
401
402### Job containers
403
404With `container:`, the steps run inside the image as its default user,
405usually `root`. A few things differ from GitHub's runner:
406
407- The workspace is at the same path as on g1t's runner
408 (`/home/runner/work/…`), not `/__w`. `github.workspace` is correct
409 either way.
410- JavaScript actions run inside the container with g1t's Node 24, which
411 needs an image with glibc and `libstdc++` (Debian, Ubuntu and most
412 language images have both). In an image without them, such as Alpine,
413 they run beside the container, on g1t's runner, with the same files,
414 and the log says so.
415- `actions/checkout`, `actions/cache` and the artifact actions run on
416 g1t's runner, with the same files.
417
418### Building and pushing images
419
420On g1t's machines, a job is signed in to g1t's container registry from
421the start, with its own `G1T_TOKEN`, so it can push to and pull from its
422repository's images without a login step; images of other repositories
423need this one added under their
424[Manage Actions access](/guides/packages/#manage-actions-access). A run that gets no secrets is
425not signed in. See [container registry](/guides/containers/#in-workflows).
426
427```yaml
428jobs:
429 image:
430 runs-on: g1t-4core
431 steps:
432 - uses: actions/checkout@v5
433 - uses: docker/setup-buildx-action@v3
434 - uses: docker/build-push-action@v6
435 with:
436 push: true
437 tags: g1t.sh/${{ github.repository }}:${{ github.sha }}
438 cache-from: type=registry,ref=g1t.sh/${{ github.repository }}:buildcache
439 cache-to: type=registry,ref=g1t.sh/${{ github.repository }}:buildcache,mode=max
440```
441
442For other registries, sign in with `docker/login-action` or
443`docker login`, as on GitHub. Docker Hub's images are pulled through its
444public mirror first, so jobs are rarely held up by Docker Hub's limits on
445anonymous pulls.
446
447#### Caching image builds
448
449The Engine starts empty in every job, so a build's layers are rebuilt
450unless the job brings a cache:
451
452- **A registry cache** (`cache-to: type=registry,ref=…,mode=max`), in g1t's
453 registry or any other, is the simplest and is shared by every branch.
454- **A local cache** (`cache-to: type=local,dest=/tmp/buildx-cache`) saved
455 and restored with `actions/cache`, within [the cache's limits](#the-cache).
456- **`type=gha`** is not used on g1t yet: Buildx skips it, and the build
457 runs without a cache.
458
459### Limits
460
461- **Machine.** Containers share the job's machine: its vCPUs, memory and
462 disk ([machine sizes](#machine-sizes)). Image builds and databases want
463 `g1t-2core` or `g1t-4core`. `--cpus` and `--memory` limit a container
464 within that.
465- **Disk.** Images take room on the job's disk. On a machine whose disk
466 cannot hold layered images, the Engine stores plain copies, which take
467 more room; the log says when it does.
468- **Linux, amd64.** Images for other platforms need QEMU's emulators,
469 which g1t's machines do not have set up; `docker/setup-qemu-action` is
470 not supported there yet.
471- **Privileged containers** (`--privileged`) run, with no more reach than
472 the job itself has: the job's sandbox is the boundary.
473
474### How Docker is kept safe
475
476- **One Engine per job.** It runs inside the job's own sandbox, a virtual
477 machine of its own, and is gone with it. No Docker socket of g1t's, or of
478 any machine, is shared with a job.
479- **The job's guardrails hold.** Containers use the job's network, so a
480 container, a build step or an image pull reaches only what the job may
481 reach. A host off the list gets `403` with the reason, as any step does.
482- **HTTPS keeps working.** In a job whose network is restricted, every
483 container and build step is given the certificate the job's HTTPS is
484 checked with, in `/dev/g1t-egress`, and `SSL_CERT_FILE`,
485 `NODE_EXTRA_CA_CERTS`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE`, `PIP_CERT`,
486 `GIT_SSL_CAINFO` and `CARGO_HTTP_CAINFO` pointing at it, unless the
487 container sets them itself. None of it is written into an image's layers.
488 Tools that keep their own list of certificates, such as Java's, need it
489 added in the build that uses them.
490- **Short-lived credentials.** The registry sign-in uses the run's own
491 token, which ends with the run; `credentials:` for a service or a job
492 container are used for that pull only.
493- **No miners.** A container whose image or command names a miner is not
494 created, as a step's script is not run.
495
496## The cache
497
498`actions/cache` keeps what a job saves for the repository's later jobs:
499
500| | |
501| --- | --- |
502| One entry | Up to 2 GiB, compressed. A larger one is not saved, and the job goes on. |
503| A repository's entries | Up to 10 GiB together. Saving past it removes the entries restored longest ago. |
504| How long | Until it has not been restored for 7 days, and at most 28 days after it was saved. |
505| Which branch | An entry belongs to the branch, tag or pull request whose run saved it. A run restores from its own, then from the branch its pull request merges into, then from the default branch. |
506| Keys | Written once on each branch: saving under a key that exists there does nothing. On each branch in turn, a restore finds its `key` exactly, else the newest entry whose key starts with one of its `restore-keys`. |
507| `path` | Files and folders; globs, `**` included; `~/` is the home folder; a line starting with `!` leaves matching paths out. The same key saved for other paths is another entry. |
508| Compression | zstd. |
509
510So a feature branch can read what `main` saved, but `main` never reads
511what a feature branch saved, and a pull request from outside the
512repository saves where nothing else ever reads it: nobody can plant an
513entry that the default branch's builds restore. Actions that cache through the
514toolkit, such as `actions/setup-node` with `cache: npm`, follow the same
515rules.
516
517```yaml
518- uses: actions/cache@v4
519 with:
520 path: |
521 ~/.cargo/registry/cache
522 target/release
523 !target/**/incremental
524 key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
525 restore-keys: cargo-${{ runner.os }}-
526```
527
528Each restore and save says on the job's log how large the entry was and
529how long it took. A workspace on the plan pays for what its caches and
530[artifacts](#artifacts) hold (`Actions cache storage` on its statement),
531at R2's price plus the margin; see
532[usage and billing](/guides/usage-and-billing/#actions-cache).
533
534## Artifacts
535
536`actions/upload-artifact` keeps files a job made with its run, for later
537jobs, other runs and people:
538
539| | |
540| --- | --- |
541| One artifact | Up to 5 GiB, zipped. |
542| A run's artifacts | Up to 10 GiB together. |
543| How long | The repository's setting: 14 days unless someone with the Maintain role changes it under **Settings → Repository → Artifacts**, from 1 to 90 days. `retention-days` asks for fewer days, never more. |
544| Names | One artifact per name in a run. Uploading a name again fails, unless the upload says `overwrite: true`, which replaces it. A name is up to 256 characters, none of `" : < > \| * ? \ /`. |
545| `path` | Files, folders and globs, `**` included; a line starting with `!` leaves matching paths out. Files and folders whose names start with `.` are left out unless `include-hidden-files: true`. |
546| Compression | `compression-level` 0 (stored) to 9; 6 unless you say. |
547| Outputs | `artifact-id` (a number), `artifact-url` (its run's page) and `artifact-digest` (the SHA-256 of its zip). |
548
549```yaml
550- uses: actions/upload-artifact@v4
551 with:
552 name: web-dist
553 path: |
554 dist/
555 !dist/**/*.map
556 retention-days: 5
557 compression-level: 9
558```
559
560`actions/download-artifact` downloads one by `name` into `path`, or every
561artifact of the run, each into a folder of its name; `pattern` picks them
562by name, and `merge-multiple: true` puts them all in one folder.
563`artifact-ids` picks them by number. With `github-token` and `run-id`, it
564downloads from another run of the same repository, such as the one a
565`workflow_run` workflow follows:
566
567```yaml
568- uses: actions/download-artifact@v4
569 with:
570 name: web-dist
571 github-token: ${{ secrets.GITHUB_TOKEN }}
572 run-id: ${{ github.event.workflow_run.id }}
573```
574
575`actions/upload-artifact/merge` downloads the run's artifacts that match
576`pattern`, uploads them as one artifact (`name`, `merged-artifacts` unless
577you say), and deletes them with `delete-merged: true`.
578
579A run's page lists its artifacts with their size and when they expire.
580Anyone who can see the run downloads them there; someone with the Write
581role can delete one before it expires.
582
583## Actions built on the toolkit
584
585Many actions save to the cache with GitHub's toolkit, `@actions/cache`,
586rather than through `actions/cache`: `actions/setup-node`,
587`actions/setup-python`, `actions/setup-go` and `actions/setup-java` with
588`cache:`, `Swatinem/rust-cache`, and others. They work on g1t as they
589are: every job gets `ACTIONS_RUNTIME_TOKEN`, `ACTIONS_CACHE_URL` and
590`ACTIONS_RESULTS_URL`, and g1t answers the toolkit's requests from the
591repository's cache.
592
593- Their entries are the repository's, under [the cache's](#the-cache)
594 limits, and are deleted the same way.
595- An entry is restored only by the same kind of save: the toolkit names a
596 version for each entry, from its paths and compression. An entry
597 `setup-node` saved is not restored by `actions/cache`, and the other way
598 round.
599- An entry the toolkit sends whole, which it does below 128 MB, is not
600 saved when it is over 100 MB, the most g1t takes in one request. The
601 step warns and the job goes on.
602- The toolkit's artifact library refuses to run against any server but
603 github.com, so an action that uploads artifacts with it directly fails
604 with its own message. `actions/upload-artifact`,
605 `actions/download-artifact` and `actions/upload-artifact/merge` work:
606 g1t runs those itself.
607
608## Caching Rust builds
609
610A checkout gives every file a new modification time, so Cargo compiles
611your workspace's own crates again even when `target/` was restored from
612the cache. [sccache](https://github.com/mozilla/sccache) caches each
613compiler call by what it compiles (the source, the flags, the toolchain
614and the dependencies), so a crate that did not change comes back from the
615cache instead. Its GitHub Actions backend works on g1t as it is: it keeps
616its entries in the repository's cache, under [the cache's](#the-cache)
617limits and branch rules.
618
6191. Install sccache, pinned to a release and checked against its checksum.
6202. Set `RUSTC_WRAPPER: sccache` and `SCCACHE_GHA_ENABLED: "true"`. The
621 job's `ACTIONS_RUNTIME_TOKEN` and `ACTIONS_CACHE_URL` are already in
622 every step's environment; no step needs to export them.
6233. Set `CARGO_INCREMENTAL: "0"`: sccache does not cache incremental
624 builds, and a fresh checkout gains nothing from them.
625
626```yaml
627name: CI
628on:
629 pull_request:
630 push:
631 branches: [main]
632
633jobs:
634 test:
635 runs-on: ubuntu-latest
636 env:
637 RUSTC_WRAPPER: sccache
638 SCCACHE_GHA_ENABLED: "true"
639 CARGO_INCREMENTAL: "0"
640 steps:
641 - uses: actions/checkout@v5
642 - name: Install sccache
643 env:
644 VERSION: v0.18.0
645 SHA256: 45f1447fbe231e3037bde351ef70677dd212216c8d62ae7ca409fecc4d6acc89
646 run: |
647 name="sccache-$VERSION-x86_64-unknown-linux-musl"
648 curl -fsSL -o "$RUNNER_TEMP/sccache.tar.gz" \
649 "https://github.com/mozilla/sccache/releases/download/$VERSION/$name.tar.gz"
650 echo "$SHA256 $RUNNER_TEMP/sccache.tar.gz" | sha256sum -c -
651 tar -xzf "$RUNNER_TEMP/sccache.tar.gz" -C "$RUNNER_TEMP"
652 install -m 0755 "$RUNNER_TEMP/$name/sccache" "$HOME/.cargo/bin/sccache"
653 - run: cargo test --workspace --locked
654 - name: sccache's hits and misses
655 if: always()
656 run: |
657 echo '```' >> "$GITHUB_STEP_SUMMARY"
658 sccache --show-stats | tee -a "$GITHUB_STEP_SUMMARY"
659 echo '```' >> "$GITHUB_STEP_SUMMARY"
660```
661
662What to expect:
663
664| | |
665| --- | --- |
666| What is cached | Every library crate (`rlib`): your workspace's crates and their dependencies. |
667| What is compiled each time | What rustc links: binaries, `cdylib` crates, proc macros, build scripts and test harnesses. |
668| Its entries | One per compiled crate, often thousands for a workspace, each a few KB to a few MB. They count toward the repository's 10 GiB like any other entry, and the ones not restored for 7 days are deleted. |
669| Branches | A pull request reads what the default branch saved. Run the workflow on pushes to the default branch too, as above, or each pull request's first run starts with an empty cache. |
670| Starting over | Set `SCCACHE_GHA_VERSION` to any new value. A new sccache release starts over by itself. |
671| If the cache cannot be reached | sccache refuses to start. Set `SCCACHE_IGNORE_SERVER_IO_ERROR: "1"` to have rustc run without it instead. |
672
673sccache adds to `actions/cache` rather than replacing it: keep caching
674`~/.cargo/registry/cache` and `target/` keyed by `Cargo.lock`, so Cargo
675does not call rustc at all for dependencies that did not change, and
676sccache answers the calls it still makes for your own crates.
677`Swatinem/rust-cache` works the same way.
678
679## OIDC tokens
680
681A job can prove which repository, branch and environment it runs for with
682a short-lived OpenID Connect token signed by g1t, and trade it for a cloud
683provider's credentials. Nothing long-lived needs to sit in a secret.
684
6851. Give the job, or the workflow, `permissions: id-token: write`. A job
686 without it gets no token, and neither does a run of a pull request from
687 outside the repository.
6882. Tell your cloud to trust g1t's issuer for your repository (below).
6893. Use the provider's own login action, which asks for the token.
690
691```yaml
692permissions:
693 id-token: write
694 contents: read
695
696jobs:
697 deploy:
698 runs-on: ubuntu-latest
699 environment: production
700 steps:
701 - uses: aws-actions/configure-aws-credentials@v4
702 with:
703 role-to-assume: arn:aws:iam::123456789012:role/acme-web-deploy
704 aws-region: us-east-1
705```
706
707The job gets `ACTIONS_ID_TOKEN_REQUEST_URL` and
708`ACTIONS_ID_TOKEN_REQUEST_TOKEN`, which `core.getIDToken()` reads. To ask
709for a token yourself, name its audience:
710
711```sh
712curl -sS -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
713 "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://deploy.example.com" | jq -r .value
714```
715
716| | |
717| --- | --- |
718| Issuer | `https://api.g1t.sh/actions/oidc` |
719| Discovery | `https://api.g1t.sh/actions/oidc/.well-known/openid-configuration` |
720| Keys | `https://api.g1t.sh/actions/oidc/.well-known/jwks`, RS256, each with its `kid` |
721| Lifetime | 5 minutes |
722| Audience | What the job asks for; `https://g1t.sh/<owner>` when it asks for none |
723
724### Claims
725
726Each token carries the claims GitHub's do, so trust policies written for
727those read g1t's the same way.
728
729| Claim | Example |
730| --- | --- |
731| `sub` | `repo:acme/web:environment:production` for a job with an `environment:`; `repo:acme/web:pull_request` for a pull request's run; otherwise `repo:acme/web:ref:refs/heads/main` (or `refs/tags/v1.2.0`) |
732| `repository`, `repository_owner` | `acme/web`, `acme` |
733| `repository_id`, `repository_owner_id` | g1t's ids for them, such as `rep_01kpw0…` |
734| `repository_visibility` | `public` or `private` |
735| `ref`, `ref_type`, `ref_protected`, `sha` | `refs/heads/main`, `branch`, `"true"`, the commit |
736| `head_ref`, `base_ref` | A pull request's branches |
737| `environment` | The job's environment, when it has one |
738| `event_name` | `push`, `pull_request`, `workflow_dispatch`, … |
739| `workflow`, `workflow_ref`, `workflow_sha` | `Deploy`, `acme/web/.g1t/workflows/deploy.yml@refs/heads/main`, the commit |
740| `job_workflow_ref`, `job_workflow_sha` | The workflow that defines the job: a called workflow's own file |
741| `run_id`, `run_number`, `run_attempt` | `run_01kq9c…`, `"12"`, `"1"` |
742| `actor`, `actor_id` | Who started the run |
743| `runner_environment` | `github-hosted` on g1t's machines, `self-hosted` on yours |
744| `iss`, `aud`, `jti`, `iat`, `nbf`, `exp` | As in any OIDC token |
745
746### AWS
747
7481. Add g1t as an identity provider, under **IAM → Identity providers**:
749 provider type **OpenID Connect**, provider URL
750 `https://api.g1t.sh/actions/oidc`, audience `sts.amazonaws.com`. Or:
751
752 ```sh
753 aws iam create-open-id-connect-provider \
754 --url https://api.g1t.sh/actions/oidc \
755 --client-id-list sts.amazonaws.com
756 ```
757
7582. Give the role a trust policy for your repository:
759
760 ```json
761 {
762 "Version": "2012-10-17",
763 "Statement": [{
764 "Effect": "Allow",
765 "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/api.g1t.sh/actions/oidc" },
766 "Action": "sts:AssumeRoleWithWebIdentity",
767 "Condition": {
768 "StringEquals": {
769 "api.g1t.sh/actions/oidc:aud": "sts.amazonaws.com",
770 "api.g1t.sh/actions/oidc:sub": "repo:acme/web:environment:production"
771 }
772 }
773 }]
774 }
775 ```
776
7773. Use `aws-actions/configure-aws-credentials@v4` with `role-to-assume`,
778 as above.
779
780### Google Cloud
781
7821. Make a workload identity pool and a provider for g1t:
783
784 ```sh
785 gcloud iam workload-identity-pools create g1t --location=global
786 gcloud iam workload-identity-pools providers create-oidc g1t \
787 --location=global --workload-identity-pool=g1t \
788 --issuer-uri=https://api.g1t.sh/actions/oidc \
789 --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository" \
790 --attribute-condition="assertion.repository_owner == 'acme'"
791 ```
792
7932. Let the repository act as a service account:
794
795 ```sh
796 gcloud iam service-accounts add-iam-policy-binding deploy@acme-prod.iam.gserviceaccount.com \
797 --role=roles/iam.workloadIdentityUser \
798 --member="principalSet://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/g1t/attribute.repository/acme/web"
799 ```
800
8013. Use `google-github-actions/auth@v2` with
802 `workload_identity_provider: projects/123456789/locations/global/workloadIdentityPools/g1t/providers/g1t`
803 and `service_account`. It asks for the provider's own name as the
804 audience, which the provider accepts unless you change its allowed
805 audiences.
806
807### Azure
808
8091. On the app registration or user-assigned managed identity, add a
810 federated credential with the scenario **Other issuer**: issuer
811 `https://api.g1t.sh/actions/oidc`, subject identifier
812 `repo:acme/web:environment:production`, audience
813 `api://AzureADTokenExchange`. Or:
814
815 ```sh
816 az ad app federated-credential create --id <application id> --parameters '{
817 "name": "g1t-acme-web-production",
818 "issuer": "https://api.g1t.sh/actions/oidc",
819 "subject": "repo:acme/web:environment:production",
820 "audiences": ["api://AzureADTokenExchange"]
821 }'
822 ```
823
8242. Give it a role on what it deploys to, as for any identity.
8253. Use `azure/login@v2` with `client-id`, `tenant-id` and
826 `subscription-id`, and no secret.
827
828A federated credential matches the subject exactly: add one per
829environment or branch that deploys.
830
831### Cloudflare
832
833Cloudflare's API takes API tokens, not OIDC tokens. Keep a token scoped
834to what the workflow deploys in a secret, available to that workflow only
835(see [workflow-only domains](/guides/guardrails/#workflow-only-domains)
836for limiting where it can be sent).
837
838A Worker of your own can trust g1t's jobs directly, by checking the token
839a job sends it against g1t's keys:
840
841```ts
842import { createRemoteJWKSet, jwtVerify } from "jose";
843
844const keys = createRemoteJWKSet(new URL("https://api.g1t.sh/actions/oidc/.well-known/jwks"));
845
846export async function fromG1tJob(request: Request): Promise<boolean> {
847 const token = request.headers.get("authorization")?.replace(/^Bearer /, "") ?? "";
848 const { payload } = await jwtVerify(token, keys, {
849 issuer: "https://api.g1t.sh/actions/oidc",
850 audience: "https://deploy.example.com",
851 });
852 return payload.sub === "repo:acme/web:environment:production";
853}
854```
855
856### npm
857
858npm's trusted publishing and provenance accept OIDC tokens only from the
859CI services npm lists, and g1t is not one of them yet. Publish with a
860granular access token in a secret instead:
861
862```yaml
863- uses: actions/setup-node@v4
864 with:
865 node-version: 24
866 registry-url: https://registry.npmjs.org
867- run: npm publish
868 env:
869 NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
870```
871
872See [What g1t can't do yet](/about/limitations/#no-npm-trusted-publishing-or-provenance).
873
874## Runs and logs
875
876Open a repository's **Actions** page, in its sidebar. Pick a workflow to
877see its runs, run it by hand if it has `workflow_dispatch`, or turn it off
878without touching its file.
879
880A run's page shows its jobs, each job's steps, and their logs as they are
881written. Groups fold, errors and warnings are marked, and secrets are
882replaced with `***`.
883
884The start of each job's log lists what its [token](#the-jobs-token) may do.
885
886### Job summaries
887
888Markdown a step appends to the file in `$GITHUB_STEP_SUMMARY` shows at
889the top of the run's page, a card per job, in the order its steps wrote
890it:
891
892```yaml
893- name: Report the tests
894 if: always()
895 run: |
896 echo "### Test results" >> "$GITHUB_STEP_SUMMARY"
897 echo "| Suite | Passed | Failed |" >> "$GITHUB_STEP_SUMMARY"
898 echo "| --- | ---: | ---: |" >> "$GITHUB_STEP_SUMMARY"
899 echo "| unit | 41 | 0 |" >> "$GITHUB_STEP_SUMMARY"
900```
901
902| What | How it works |
903| --- | --- |
904| Formatting | GitHub-flavoured Markdown: tables, task lists, alerts such as `> [!WARNING]`, code blocks, and the HTML GitHub allows. Scripts, styles and event handlers are removed. |
905| Secrets | Masked like the log, before the summary leaves the runner. |
906| Size | Up to 1 MiB a step. A larger summary is refused with an error in the step's log, as on GitHub. |
907| Steps | Up to 20 steps of a job keep a summary; later ones are dropped. |
908| Actions | A JavaScript action's `core.summary` writes to the same file, so it works unchanged. |
909
910Summaries belong to their attempt: an earlier attempt keeps its own.
911
912### Re-running
913
914When a run has finished, someone with the Write role can run it again:
915
916| Button | Runs again |
917| --- | --- |
918| **Re-run all jobs** | Every job. |
919| **Re-run failed jobs** | Jobs that did not succeed (failed, cancelled or skipped), and every job that needs one of them. |
920| The re-run button beside a job's name | That job, and every job that needs it. A job of a matrix runs again with the rest of its matrix; a job of a reusable workflow runs again with the job that calls it. |
921
922Jobs that are not run again keep how they ended, and their outputs reach
923the jobs that need them.
924
925Each re-run is a new **attempt**. The run keeps its number, `github.run_attempt`
926goes up by one, and the attempt before is kept as it ended: its jobs,
927their steps, logs and summaries. Pick one from **Attempt #** at the top of
928the page to read it. Its jobs have ids of their own, so a link to an
929earlier attempt's job keeps showing that job's log.
930
931#### Debug logging
932
933Each re-run asks whether to **Enable debug logging**. The new attempt's
934jobs then run with:
935
936| Set | Effect |
937| --- | --- |
938| `RUNNER_DEBUG=1`, and `runner.debug` is `1` | Actions that check it, such as the toolkit's `core.isDebug()`, log more. |
939| `ACTIONS_STEP_DEBUG=true` | `::debug::` lines are shown in the log. |
940| `ACTIONS_RUNNER_DEBUG=true` | Set for actions that read it. |
941
942With debug logging, each step's log also says how its `if:` read:
943`Evaluating condition for step`, the expression, and the result. Setting a
944secret or variable named `ACTIONS_STEP_DEBUG` to `true` shows `::debug::`
945lines on every run instead. The attempt picker marks attempts that ran
946with debug logging.
947
948### Cancelling
949
950**Cancel run** cancels jobs that have not started at once. A job that is
951running is stopped the way GitHub stops one:
952
9531. The step it is on gets `SIGINT`, then `SIGTERM` 7.5 seconds later, and
954 is killed 2.5 seconds after that. Signals reach the processes the step
955 started too; in a [job container](#job-containers) they reach only the
956 `docker exec` that runs the step. The step ends **cancelled**.
9572. Its remaining steps run only if they ask to: `if: always()` or
958 `if: cancelled()`. Steps without an `if:`, or with `success()` or
959 `failure()`, are skipped.
9603. Post steps (an action's `post`, saving the cache) run, as their
961 `post-if` is `always()` unless the action says otherwise.
9624. The job ends **cancelled**, whatever those steps came to.
963
964While that happens the run says **Cancelling**. A job still going 5
965minutes after it was cancelled is stopped outright. **Force cancel**
966(shown while a run is cancelling) stops every job at once, without
967waiting for its cleanup steps.
968
969A step learns of a cancellation within about 10 seconds, even when it
970prints nothing. A job on a [self-hosted runner](/guides/self-hosted-runners/)
971is stopped the same way.
972
973### Searching and downloading logs
974
975The **Search logs** box above a job's steps shows only the lines that hold
976what you type, in any case, with each match marked and every step that has
977one opened. Lines inside folded groups are searched too.
978
979| Download | Where |
980| --- | --- |
981| One job's whole log, as text | The download button beside the job's name. |
982| Every job's log of an attempt, as a zip | **Download logs** at the top of the run. The zip holds `1_<job>.txt` with each job's whole log, and a `<job>/` folder with `<step>_<step name>.txt` for each step. |
983
984A zip holds up to 24 MiB of logs; jobs past that are listed with a note
985to download them on their own. Each job keeps up to 4 MB of log.
986
987### Status badges
988
989A badge shows how a workflow's latest finished run went: **passing**,
990**failing**, **cancelled**, or **no status** before it has finished one.
991
9921. Open the repository's **Actions** page and pick the workflow.
9932. Click **Create status badge**.
9943. Choose a branch and an event, if you want them, and copy the Markdown.
995
996```markdown
997[![CI](https://g1t.sh/acme/web/actions/workflows/ci.yml/badge.svg)](https://g1t.sh/acme/web/actions?workflow=ci.yml)
998```
999
1000The address is `https://g1t.sh/{workspace}/{repo}/actions/workflows/{file}/badge.svg`,
1001where `{file}` is the workflow's file name in `.g1t/workflows/`. It takes:
1002
1003| Parameter | Shows |
1004| --- | --- |
1005| `branch` | Runs on that branch. Without it, the default branch's runs, or any branch's when the default branch has none. |
1006| `event` | Runs started by that event, such as `push` or `pull_request`. |
1007
1008A public repository's badge loads for anyone and is cached for a minute.
1009A private repository's loads only for someone who can see the repository,
1010so it does not show in a README read anywhere else.
1011
1012### Masking secrets
1013
1014Every secret's value is replaced with `***` wherever a job prints it, and
1015so is every value a step masks with `::add-mask::`. Each is masked in the
1016forms it shows up in:
1017
1018| Form | Example |
1019| --- | --- |
1020| As it is | `echo $API_KEY` |
1021| Each of its lines on its own | `cat key.pem`, which prints a private key a line at a time |
1022| Base64 | `echo -n $API_KEY \| base64`, or an `Authorization: Basic` header |
1023| JSON-escaped | A secret holding quotes or newlines printed inside JSON |
1024
1025Annotations' titles and messages, and step names, are masked the same way.
1026A job output that holds a secret in any of those forms is left out, with
1027a warning in the log, since outputs go to other jobs and to the run's page.
1028A value of one character is not masked: it would hide that character
1029everywhere.
1030
1031## Pull requests
1032
1033A pull request's workflows run on each new head: when it is opened, when
1034a commit is pushed to it, and, for one g1t makes, when g1t
1035marks it ready, which on g1t is when it first has code. Each head runs
1036each workflow once.
1037
1038They also start on the activity types `reopened` (also run by
1039default, as `opened` and `synchronize` are), `converted_to_draft`,
1040`ready_for_review`, `labeled`, `unlabeled`, `milestoned`, `demilestoned`,
1041`assigned`, `review_requested` and `closed`, and `edited` when the branch
1042a pull request merges into changes; `issue_comment` workflows on
1043`created`, `edited` (with `github.event.changes.body.from`) and `deleted`; `issues` workflows on `labeled`, `unlabeled`, `milestoned` and
1044`demilestoned` too. List them under `types:` to run on them. For
1045`labeled` and `unlabeled`, `github.event.label` names the label. A pull
1046request's `branches` filter, `github.base_ref` and
1047`pull_request.base.ref` are the branch it merges into, which is not
1048always the default branch: see
1049[pull requests into other branches](/guides/base-branches/).
1050
1051A pull request from outside the workspace may wait for approval before
1052its workflows run: see [pull requests from outside](#pull-requests-from-outside).
1053
1054`github.event.pull_request` reads as it does on GitHub. For a pull request
1055g1t made, `pull_request.user` is g1t (`login` `g1t`, `type` `Bot`), and
1056`pull_request.requested_by` names the person who asked for it; it is `null`
1057on anyone else's. `github.event.issue.requested_by` does the same for an
1058issue g1t's agent filed. `sender` is whoever caused the event.
1059
1060## Checks
1061
1062A pull request's checks are its workflows. Each workflow that runs on
1063`pull_request` runs on every pull request's head, whoever opened it, a
1064person or an agent, and its runs report a check named after the workflow:
1065a workflow with `name: CI` reports `CI`, with the status context
1066`CI / pull_request` (the workflow's name and the event). Each of its jobs
1067is a [check run](/guides/checks/) on the commit, shown as
1068`CI / test (pull_request)` beside it wherever it appears.
1069
1070- **Which checks a merge needs** is up to the [rules](/guides/rules/) of the branch it merges into,
1071 their [required status checks](/guides/pull-requests/#required-status-checks),
1072 under **Settings → Rules**. A required check that failed,
1073 is still running or has not reported holds the merge. Checks that are not
1074 required are shown on the pull request and never hold it.
1075- **In a repository that merges through the [merge queue](/guides/merge-queue/)**,
1076 workflows with `on: merge_group` run on each combined state the queue
1077 builds, on the branch `g1t-queue/<entry>`, and the state lands only if
1078 they and every required check pass on it. A workflow behind a required
1079 check needs `merge_group` in its `on:`.
1080- **A pull request g1t is working on** goes back to g1t when
1081 a check fails, with the end of each failed job's log. The agent reads the
1082 run and its logs with the same tools you have, fixes the cause, and
1083 pushes; the workflows run again. See
1084 [seeing it through](/guides/working-with-g1t/#seeing-it-through).
1085
1086```yaml
1087name: CI
1088
1089on:
1090 pull_request:
1091 push:
1092 branches: [main]
1093 merge_group:
1094```
1095
1096### Add CI
1097
1098A repository with no workflows has nothing that proves a change works, for
1099people or for agents. Its pull requests, its **Branches and merging**
1100settings and its **Actions** page say **This repository has no checks**,
1101with an **Add CI** button. Anyone who can push to the repository can use it:
1102
11031. Choose **Add CI**. g1t looks at the files at the repository's root and
1104 writes a starter workflow with a job for each stack it finds, up to
1105 three: Node (npm, pnpm, Yarn or Bun), Rust, Go, Python (pip or uv), Ruby,
1106 Java (Maven or Gradle), .NET, or Make. Each job installs, lints where
1107 the project says how, builds and tests. When it finds none, the job is a
1108 placeholder that fails until you replace its last step with your own
1109 commands.
11102. The workflow is committed as `.g1t/workflows/ci.yml` on a new branch,
1111 `add-ci`, and opened as a pull request, by you. It is named `CI` and runs
1112 on `pull_request`, on `push` to the default branch, and on `merge_group`.
11133. Change it on the pull request if the steps are not how your project
1114 builds, and merge it.
11154. Once it has run, `CI` is offered under **Require status checks to pass
1116 before merging**.
1117 Require it, so that nothing merges into the default branch unless it
1118 passes.
1119
1120## Secrets and variables
1121
1122Secrets are read as `${{ secrets.KEY }}` and config as `${{ vars.KEY }}`,
1123from the rows under **Settings → Secrets and variables** that are
1124available to Workflows. A job with `environment: production` reads each
1125key's Production row; other jobs read the rows for all environments. A
1126job with an `environment:` also makes a deployment to it; see
1127[deployments from g1t Actions](/guides/deployments-api/#deployments-from-g1t-actions). See
1128[Secrets and variables](/guides/secrets-and-variables/) for how rows,
1129environments and the workspace's rows work.
1130
1131Every job also gets `${{ secrets.G1T_TOKEN }}`, [its own token](#the-jobs-token),
1132with `GITHUB_TOKEN` as its alias. A pull request's runs get secrets only
1133when its author has the Write [role](/guides/access-and-roles/) or higher
1134on the repository, a member or an outside collaborator, or is g1t working
1135on its own. For a pull request g1t made, its author is g1t and the person
1136who asked for it is the one whose role counts. Anyone else's, such as one
1137from a fork or by someone with Read or Triage, runs without secrets and
1138with a token that can only read. See
1139[who gets secrets](/guides/secrets-and-variables/#who-gets-secrets).
1140
1141## The job's token
1142
1143Each job gets a token of its own, `${{ secrets.G1T_TOKEN }}`
1144(`${{ secrets.GITHUB_TOKEN }}` and `${{ github.token }}` are the same).
1145`actions/checkout` uses it, and so can any step that calls the
1146[API](/reference/api/) or pushes with git:
1147
1148- It reaches **this repository only**. Every other repository, even one
1149 in the same workspace, is refused. So are packages: it reaches this
1150 repository's own, and another package only once the package's admins
1151 add this repository under its
1152 [Manage Actions access](/guides/packages/#manage-actions-access).
1153- It can do **what its `permissions:` say**, and nothing more.
1154- It **stops working when the job ends**, however it ends.
1155- Everything it changes is in the [audit log](/guides/audit-log/) as that
1156 job's, under its run.
1157- What it changes **starts no workflows**: a push, a pull request, an issue
1158 or a comment made with it runs nothing, so a workflow cannot set itself
1159 off. `workflow_dispatch` and [`repository_dispatch`](#repository-dispatch)
1160 are the exceptions, for a workflow that means to start another.
1161- It **never puts g1t to work**. A comment it posts that mentions
1162 `@g1t` starts nothing, and it cannot assign an issue or a plan to g1t,
1163 queue one for it, hand it work or ask it for a review. Otherwise a
1164 workflow that asks g1t to fix a failing check would run again on g1t's
1165 push, and ask again, without end. A step that should put g1t to work
1166 uses a token of a person's own, stored as a
1167 [secret](/guides/secrets-and-variables/).
1168
1169`permissions:` goes at the top of the workflow, for every job, or on a job,
1170which then ignores the workflow's. Once either is written, every permission
1171it leaves out is `none`:
1172
1173```yaml
1174permissions:
1175 contents: read
1176
1177jobs:
1178 release:
1179 runs-on: ubuntu-latest
1180 permissions:
1181 contents: write
1182 pull-requests: write
1183 steps:
1184 - uses: actions/checkout@v5
1185 - run: ./scripts/release.sh
1186```
1187
1188| Permission | `read` lets it | `write` also lets it |
1189| --- | --- | --- |
1190| `contents` | Clone and fetch with git, read the repository | Push, publish releases |
1191| `pull-requests` | Read pull requests | Open, review, close and merge them |
1192| `issues` | Read issues | Open, edit, comment on and close them |
1193| `actions` | Read workflows, runs and logs | Run, cancel and re-run them |
1194| `checks`, `statuses` | Read statuses and check runs | Report them |
1195| `deployments`, `pages` | Read deployments | Report them |
1196| `packages` | Pull packages | Push and publish them |
1197| `security-events` | Read security alerts | Upload code scanning results, change alerts |
1198| `metadata` | Always `read` | |
1199| `id-token` | Nothing | Ask for an [OIDC token](#oidc-tokens) |
1200| `discussions`, `attestations`, `models`, `repository-projects` | Nothing on g1t | Nothing on g1t |
1201
1202`read-all` and `write-all` set every permission; `permissions: {}` sets
1203none, so the token cannot even clone a private repository. A reusable
1204workflow's jobs get no more than the job that calls it.
1205
1206There is no `workflows` permission for a job's token: it can never add,
1207change or delete a file under `.g1t/workflows/` or `.github/workflows/`,
1208even with `contents: write`. A push that does is declined, naming the file,
1209so a workflow cannot rewrite the workflows that run with its repository's
1210secrets. To change workflows from a job, push with a
1211[fine-grained token](/guides/authentication/#workflow-files) that has the
1212Workflows permission, kept as a secret.
1213
1214The job's token is the repository's workspace acting with the Write role
1215at most, never Admin: it cannot manage webhooks, secrets, deploy keys or who
1216has access, whatever it asks for.
1217
1218**Without `permissions:`** a job gets the repository's default, which
1219someone with the Admin role sets under **Settings → Actions**:
1220
1221| Repository | Default until someone chooses |
1222| --- | --- |
1223| Made before restricted tokens came in, in October 2026 | **Read and write**: every permission at `write`, as before |
1224| Made since | The workspace's default for new repositories: **Read repository contents and packages** (`contents: read`, `packages: read`) unless an owner chose otherwise |
1225
1226The workspace's owners set that default, and a **maximum**, under the
1227workspace's **Settings → Actions**: with a maximum of **Read only**, no
1228repository's default goes past `contents: read` and `packages: read`,
1229whatever it chose. Workflows that write `permissions:` get what they
1230write either way, and whatever a workflow asks for, a pull request from
1231outside the repository's writers (a fork, or someone with Read or Triage)
1232gets a token that can only read.
1233
1234**Allow g1t Actions to create and approve pull requests** is off unless a
1235repository's admin turns it on under **Settings → Actions**, and they can
1236only where the workspace's owners allow it. Until then a job's token
1237cannot open a pull request or approve one, whatever its `pull-requests`
1238permission says; it can still read, comment on, review with changes
1239requested, and merge them.
1240
1241The token can never change secrets, variables, environments' rules or
1242the Actions settings, approve runs or deployments, or reach another
1243repository.
1244
1245## Environments
1246
1247A job that names an environment with `environment:` reads that
1248environment's [secrets and variables](/guides/secrets-and-variables/#a-value-per-environment)
1249and records a [deployment](/guides/deployments-api/#deployments-from-g1t-actions)
1250to it. Give the environment protection rules, and such a job waits until
1251they let it through, and only then gets the environment's secrets:
1252
1253| Rule | What it does |
1254| --- | --- |
1255| **Required reviewers** | Up to 6 people or teams. The job waits until one of them approves it. |
1256| **Prevent self-review** | Whoever started the run cannot approve it, even as a reviewer. |
1257| **Wait timer** | Minutes the job waits once it reaches the environment, up to 43,200 (30 days). |
1258| **Deployment branches and tags** | **All branches**; **Protected branches only**, those the repository's [rules](/guides/rules/) protect, the default branch included; or **Selected branches and tags**, by pattern, such as `main`, `release/*` or `v*`. A job on any other ref fails, saying so. A pull request's run is on no branch, so it can deploy only where all branches may. |
1259| **Allow admins to bypass** | On unless you turn it off: someone with the Admin role may approve without being a reviewer, which also skips the wait timer. |
1260
1261To set them:
1262
12631. Open the repository's **Settings → Environments**. It lists every
1264 environment your workflows, secrets and deployments name.
12652. Choose one, or name a new one, and set its rules.
12663. Save. Runs that reach the environment from then on wait by them.
1267
1268A run whose jobs wait shows **Waiting for review** at the top of its page,
1269with the environments, the jobs each holds, its reviewers and when its
1270wait timer runs out. Reviewers are told in their [inbox](/guides/inbox/);
1271on the run's page they choose **Approve and deploy** or **Reject**, with
1272room for a comment. One review covers every job of the run that names the
1273environment. A rejected job fails, and so does anything that needs it. The
1274rest of the run goes on meanwhile: jobs that do not need the waiting ones
1275run.
1276
1277The environment's name may be an expression, such as
1278`environment: ${{ inputs.target }}`: it is read once the job's needs are
1279done, and its rules and secrets are that environment's. Names are matched
1280without regard to case.
1281
1282From the API, `PUT /repos/{owner}/{repo}/environments/{environment}` sets
1283the rules, with `reviewers`, `prevent_self_review`, `wait_timer`,
1284`deployment_branch_policy`, `branch_policies` and `can_admins_bypass`;
1285`GET` on the same route returns them as `protection_rules`; `DELETE`
1286removes them. `POST /repos/{owner}/{repo}/actions/runs/{id}/pending_deployments`
1287approves or rejects a run's waiting jobs:
1288
1289```sh
1290curl -X PUT https://api.g1t.sh/repos/acme/web/environments/production \
1291 -H "Authorization: Bearer $G1T_TOKEN" -H "Content-Type: application/json" \
1292 -d '{"reviewers": [{"type": "Team", "name": "deployers"}], "wait_timer": 10,
1293 "deployment_branch_policy": {"protected_branches": true, "custom_branch_policies": false}}'
1294```
1295
1296A job's own token cannot approve, reject or change any of it.
1297
1298## Pull requests from outside
1299
1300A pull request from someone outside the workspace runs code anyone could
1301have written. By the repository's **approval policy**, its runs wait as
1302**Approval required** until someone with the Write
1303[role](/guides/access-and-roles/) chooses **Approve and run** on the run's
1304page. Nothing runs before then: no job starts, and no token or secret is
1305handed out.
1306
1307| Policy, under **Settings → Actions** | Whose pull requests' runs wait |
1308| --- | --- |
1309| **First-time contributors** | Someone outside the workspace who has not had a pull request merged here yet. |
1310| **Outside contributors** (the default) | Those, and everyone outside the workspace who cannot push here: pull requests from forks, and from people with Read or Triage. |
1311| **All external contributors** | Everyone outside the workspace, [outside collaborators](/guides/access-and-roles/#outside-collaborators) with Write included. |
1312
1313Members' pull requests never wait, nor do pull requests g1t opens on its
1314own. For a pull request g1t made for someone, that person is the one whose
1315policy counts. Each new push to the pull request waits again.
1316`pull_request_target` runs, which run the default branch's workflow and
1317code, never wait.
1318
1319From the API: `POST /repos/{owner}/{repo}/actions/runs/{id}/approve`
1320approves a run, and `GET` and `PUT
1321/repos/{owner}/{repo}/actions/permissions/fork-pr-contributor-approval`
1322read and set the policy, as `approval_policy`.
1323
1324## Repository dispatch
1325
1326`POST /repos/{owner}/{repo}/dispatches` starts the default branch's
1327workflows that run `on: repository_dispatch` for its `event_type`, those
1328listing it under `types:` or listing none. `client_payload` is theirs to
1329read as `github.event.client_payload`:
1330
1331```yaml
1332on:
1333 repository_dispatch:
1334 types: [docs-published]
1335
1336jobs:
1337 announce:
1338 runs-on: ubuntu-latest
1339 steps:
1340 - run: echo "Docs ${{ github.event.client_payload.version }} are out"
1341```
1342
1343```sh
1344curl -X POST https://api.g1t.sh/repos/acme/web/dispatches \
1345 -H "Authorization: Bearer $G1T_TOKEN" -H "Content-Type: application/json" \
1346 -d '{"event_type": "docs-published", "client_payload": {"version": "2.4.0"}}'
1347```
1348
1349It needs the Write role, or a token with `code:write`; a job's own token
1350needs `contents: write`. `client_payload` is a JSON object of at most 10
1351properties and 64 KB.
1352
1353## Who may run workflows
1354
1355What you can do with a repository's workflows follows your
1356[role](/guides/access-and-roles/) on it:
1357
1358| | Needs |
1359| --- | --- |
1360| See workflows, runs and their logs | Read: on a public repository, anyone |
1361| Run a workflow by hand, cancel or re-run a run, approve a pull request's run from outside | Write |
1362| Approve or reject a job waiting for an environment | One of the environment's reviewers |
1363| Enable or disable a workflow | Maintain |
1364| The repository's secrets and variables, seeing them included; environments' rules; **Settings → Actions** | Admin |
1365
1366Jobs run in g1t's sandboxes, so they need the
1367[g1t plan](/guides/usage-and-billing/#the-g1t-plan) or
1368[the trial](/guides/usage-and-billing/#the-trial); jobs on
1369[self-hosted runners](/guides/self-hosted-runners/#billing) need neither.
1370On a public repository,
1371[g1t's open-source pool](/guides/usage-and-billing/#the-open-source-pool)
1372runs them too, after a card check, until the month's pool is spent.
1373
1374Before each job starts, g1t reserves what it may cost (its time limit at
1375the sandbox price) with billing, and settles what it really cost when it
1376ends; each job's sandbox is charged as
1377[sandbox time](/guides/usage-and-billing/#sandbox-time), from the first
1378second. A job billing refuses does not start: it is recorded as failed
1379with "Not started:" and the reason, such as "Workflows run in g1t's
1380sandboxes, which cost real money, so they need the g1t plan ($20 a month)
1381or a card check", and what to do about it. The
1382Actions page tells people with Write on a repository whose workspace
1383cannot run jobs before the first run.
1384
1385## From the API
1386
1387The routes follow the standard Actions REST shape, so existing scripts
1388usually work once they point at `https://api.g1t.sh`.
1389
1390| `workflow` action | Route |
1391| --- | --- |
1392| `list` | `GET /repos/{owner}/{repo}/actions/workflows` |
1393| `list_runs` | `GET /repos/{owner}/{repo}/actions/runs`, with `workflow`, `branch`, `event`, `pull`, `head_sha` |
1394| `get_run` | `GET /repos/{owner}/{repo}/actions/runs/{id}` |
1395| `job_logs` | `GET /repos/{owner}/{repo}/actions/jobs/{job}/logs?after=`, or `?format=text` for the whole log as plain text |
1396| `get_run` with `attempt` | `GET /repos/{owner}/{repo}/actions/runs/{id}/attempts/{attempt}` |
1397| No tool: a download | `GET /repos/{owner}/{repo}/actions/runs/{id}/logs`, or `…/attempts/{attempt}/logs`: every job's log as a zip |
1398| `dispatch` | `POST /repos/{owner}/{repo}/actions/workflows/{workflow}/dispatches` with `ref` and `inputs` |
1399| `cancel` | `POST /repos/{owner}/{repo}/actions/runs/{id}/cancel`; `…/force-cancel`, or `force`, to stop running jobs without their cleanup steps |
1400| `rerun` | `POST …/runs/{id}/rerun`, or `…/rerun-failed-jobs`; one job and those that need it with `POST /repos/{owner}/{repo}/actions/jobs/{job}/rerun`. Each takes `enable_debug_logging` (or `debug`) |
1401| `update` | `PUT …/workflows/{workflow}/enable` and `…/disable` |
1402| `approve_run` | `POST /repos/{owner}/{repo}/actions/runs/{id}/approve` |
1403| `pending_deployments` | `GET /repos/{owner}/{repo}/actions/runs/{id}/pending_deployments` |
1404| `review_deployments` | `POST /repos/{owner}/{repo}/actions/runs/{id}/pending_deployments` with `environment_names`, `state` and `comment` |
1405| `get_environment` | `GET /repos/{owner}/{repo}/environments/{environment}` |
1406| `update_environment` | `PUT /repos/{owner}/{repo}/environments/{environment}` |
1407| `delete_environment` | `DELETE /repos/{owner}/{repo}/environments/{environment}` |
1408| `get_permissions`, `set_permissions` | `GET` and `PUT /repos/{owner}/{repo}/actions/permissions/workflow`, with `default_workflow_permissions` (`read`, `write` or `inherit`) and `can_approve_pull_request_reviews` |
1409| `get_workspace_permissions`, `set_workspace_permissions` | `GET` and `PUT /workspaces/{workspace}/actions/permissions/workflow`, with `default_workflow_permissions`, `max_workflow_permissions` and `can_approve_pull_request_reviews` |
1410| `get_approval_policy`, `set_approval_policy` | `GET` and `PUT /repos/{owner}/{repo}/actions/permissions/fork-pr-contributor-approval` |
1411| `get_access`, `set_access` | `GET` and `PUT /repos/{owner}/{repo}/actions/permissions/access`, with `access_level` (`none` or `organization`) |
1412| `repository_dispatch` | `POST /repos/{owner}/{repo}/dispatches` with `event_type` and `client_payload` |
1413| `list_artifacts` | `GET /repos/{owner}/{repo}/actions/artifacts`, with `name`, `page`, `per_page` |
1414| `run_artifacts` | `GET …/actions/runs/{id}/artifacts`, with `name` |
1415| `get_artifact` | `GET …/actions/artifacts/{artifact_id}` |
1416| `download_artifact` | `GET …/actions/artifacts/{artifact_id}/zip`: a `302` to a link good for 10 minutes |
1417| `delete_artifact` | `DELETE …/actions/artifacts/{artifact_id}` |
1418| `artifact_retention`, `set_artifact_retention` | `GET` and `PUT …/actions/permissions/artifact-and-log-retention` with `days` (it sets artifacts' days only; logs are kept with their run) |
1419
1420Secrets and variables have a tool of their own, `secret`:
1421
1422| `secret` action | Route |
1423| --- | --- |
1424| `list_secrets`, `set_secret`, `delete_secret` | `GET /repos/{owner}/{repo}/actions/secrets`, `PUT` and `DELETE …/secrets/{name}` |
1425| `list_variables`, `set_variable`, `delete_variable` | `GET` and `POST /repos/{owner}/{repo}/actions/variables`, `PATCH` and `DELETE …/variables/{name}` |
1426
1427Workspace secrets and variables are under
1428`/workspaces/{workspace}/actions/secrets` and `…/variables`. The fields
1429g1t adds (environments, who reads a row, linked repositories) are in
1430[Secrets and variables](/guides/secrets-and-variables/#from-the-api).
1431
1432```sh
1433curl -X POST https://api.g1t.sh/repos/acme/web/actions/workflows/ci.yml/dispatches \
1434 -H "Authorization: Bearer $G1T_TOKEN" -H "Content-Type: application/json" \
1435 -d '{"ref": "main", "inputs": {"environment": "staging"}}'
1436```
1437
1438To save an artifact from a script, follow the redirect:
1439
1440```sh
1441curl -L -o web-dist.zip -H "Authorization: Bearer $G1T_TOKEN" \
1442 https://api.g1t.sh/repos/acme/web/actions/artifacts/4182/zip
1443```