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