# Migrate from GitHub Actions to Chainguard Checks

URL: https://chainguard-docs-preview-git-entra-scim-updates.chainguard.app/docs/chainguard/checks/migrate-from-github-actions.md
Last Modified: November 12, 2026
Tags: Chainguard Checks, Migration, GitHub

Convert GitHub Actions pull request workflows to a Chainguard Checks configuration, with a mapping of common GitHub Actions keys.


If your repository runs its pull request checks with GitHub Actions, you can move them to Chainguard Checks one workflow at a time. Chainguard Checks and GitHub Actions can run on the same pull requests, so you can compare their results before you remove a workflow.

If you'd rather keep your workflows on GitHub Actions, [Chainguard Actions](/chainguard/actions/overview/) replaces the actions they use with hardened equivalents, and Guardener's [Hardened Actions](/chainguard/guardener/github/actions-security/) feature can migrate them for you.

## How the models differ

A GitHub Actions job starts on a general-purpose runner image, checks out your code, and installs its tools with setup actions before running its commands. In Chainguard Checks, a check starts in a minimal VM with your code already in `/work`, installs the packages you list, and runs its commands. The following table summarizes the differences:

| Area | GitHub Actions | Chainguard Checks |
| ---- | -------------- | ----------------- |
| Actions | Steps can use actions with `uses:`. | There are no actions. List each tool's package and run it directly. You don't need `actions/checkout`, because `/work` already holds your repository's tracked files and history at the commit under test, but not its submodules or Git LFS content. |
| Preinstalled tools | The runner image includes many tools. | The VM includes BusyBox, GNU tar, and the packages you list. List every tool a check uses, including `git`, `bash`, and `make`. |
| Network | Jobs can reach the internet. | A check has no network access unless it lists hosts under `network.egress`, such as your package registry. |
| Shell | A `run` step uses `bash -e`, or `bash -eo pipefail` with `shell: bash`. | A `script` runs with BusyBox `sh -e`, or with `bash -e` if you list the `bash` package and set `shell: bash`. Neither sets `pipefail`, so for a step that had `shell: bash`, start its script with `set -o pipefail`. Refer to [The script key](/chainguard/checks/configuration/#the-script-key). |
| Caches | Jobs save and restore caches with actions, such as `actions/cache`. | A check starts with no cache. If the `trust` section of `.chainguard/ci/identities.yaml` on your default branch defines a cache pool, a check can reuse Go build output, and the files that Go, pip, uv, Maven, and Gradle download, from earlier runs. |
| Run length | A job can run for up to 6 hours. | A whole run can take up to two hours, and checks still running at that point fail. Split long jobs into several checks, which run in parallel. |
| Background processes | A process that a step starts in the background keeps running in the job's later steps. | When a step ends, Chainguard Checks stops every process that the step left running in the background. To keep a process, such as a database, running for a check's commands, use a [service](/chainguard/checks/variations-services-caches/#services). Refer to [How commands run](/chainguard/checks/configuration/#how-commands-run). |
| Secrets | Jobs read secrets from the `secrets` context, and get a `GITHUB_TOKEN`. | There's no `secrets` context. During the beta, a check gets no GitHub token, so it can't fetch your private GitHub repositories, such as private Go modules. A check that must authenticate to Google Cloud, AWS, Azure, or Chainguard can use a workload identity, as described in [Credentials](/chainguard/checks/configuration/#credentials). |

## Key mapping

The following table maps common GitHub Actions keys and actions to their Chainguard Checks equivalents:

| GitHub Actions | Chainguard Checks |
| -------------- | ----------------- |
| `on: pull_request` | The default. A check runs on pull requests unless you set `trigger`. |
| `on: push` to the default branch | `trigger: postsubmit` |
| `on: pull_request: paths:` | `when: changed:`. A `**` matches any number of directories only as a whole path segment, so write `**.go` as `**/*.go`. A pattern that starts with `!` has no equivalent. Refer to [Run when files change](/chainguard/checks/configuration/#run-when-files-change). |
| `on: pull_request: paths-ignore:` | No equivalent. List the paths that should run the check under `when: changed:` instead. |
| `if:` | `when: expr:` with a CEL expression |
| `workflow_dispatch`, or a job that a label starts | `when: {requested: true}`, run with a `ci/run:<check>` label |
| `continue-on-error: true`, or a job that isn't required | `advisory: true` |
| `jobs.<job_id>` | `checks.<name>` |
| `runs-on` | `resources`. A built-in class without a suffix usually runs on arm64, while `ubuntu-latest` is x86-64, so use an `-amd64` class, such as `medium-amd64`, for a job that needs x86-64. Only a class with a `platform` pins the architecture among your own classes, and if your repository uses the older `.chainguard/ci/resources.yaml` file, the built-in classes aren't available. Refer to [Resources](/chainguard/checks/configuration/#resources). |
| `steps[*].run` | `steps[*].cmd` or `steps[*].script`. A step takes only `id`, `name`, `cmd`, `script`, and `shell`, so step-level `env`, `if`, and `working-directory` are refused. |
| `steps[*].uses` | Not supported. List the tool's package and run it. |
| `actions/checkout` with `repository` | `inputs` with `git`, for a public repository on github.com. Refer to [Check out another repository](/chainguard/checks/variations-services-caches/#check-out-another-repository). |
| `actions/checkout` with `submodules` or `lfs` | No equivalent. Submodule directories are empty, and Git LFS files hold their pointer files. |
| `actions/upload-artifact` and `actions/download-artifact` | `outputs` on the check that writes the files, and `inputs` with `from` on a check in the same file that reads them. Refer to [Pass files between checks](/chainguard/checks/variations-services-caches/#pass-files-between-checks). A run keeps no files that you can download. |
| `actions/cache`, or the caching in a setup action, such as `actions/setup-go` | `environment.caches`, with a cache pool in the `trust` section of `.chainguard/ci/identities.yaml` on your default branch. Use `gobuild` and `gomod` for Go build output and downloaded modules, `python` for pip and uv, `java` for Maven and Gradle, and `cargo` for Cargo. A check that uses a cache runs as `root`, the default, or as `build`, as described in [Cache types](/chainguard/checks/variations-services-caches/#cache-types). The `node` and `directory` types have no effect during the beta. |
| `jobs.<job_id>.services` | `environment.services`. A service runs in the check's VM from packages that you list, not in a container. Refer to [Services](/chainguard/checks/variations-services-caches/#services). To run a container image instead, run Docker as a service, as described in [Run Docker](/chainguard/checks/variations-services-caches/#run-docker). |
| `env` | `vars` |
| `needs` | `needs`, for checks in the same file |
| `timeout-minutes` | `timeout`, as a duration such as `15m` |
| `strategy.matrix` | `foreach`. Chainguard Checks doesn't expand a matrix, so write one entry for each combination, and replace each `${{ matrix.<key> }}` with a variable that the entry sets in `vars`, such as `$GOARCH` in a `script`. An entry can't change the check's packages, so for a matrix of tool versions, write a separate check with its own environment for each version. Refer to [Run a check once per variation](/chainguard/checks/variations-services-caches/#run-a-check-once-per-variation). |
| `strategy.max-parallel` | `parallelism` |
| `defaults.run.working-directory` | `workdir` |
| `$GITHUB_STEP_SUMMARY` | Not set, so a command that writes to it fails. Instead, print Markdown between a `::sandbox-summary::` line and an `::end-sandbox-summary::` line, and the check's output on GitHub shows it before the log. Refer to [Add a Markdown summary](/chainguard/checks/configuration/#add-a-markdown-summary). |
| `on: schedule` | Not supported. |
| `on: merge_group` | Not supported. Chainguard Checks doesn't run on merge queues, so don't require **Chainguard Checks (presubmit)** on a branch that uses one. Refer to [Require checks before merging](/chainguard/checks/github/#require-checks-before-merging). |

Refer to the [configuration reference](/chainguard/checks/configuration/) for each Chainguard Checks key.

## Example

The following GitHub Actions workflow runs Go tests on pull requests that change Go files:

```yaml {title=".github/workflows/test.yaml"}
name: test
on:
  pull_request:
    paths: ["**/*.go", "go.mod", "go.sum"]
jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: "1.24"
      - run: go test ./...
        env:
          CGO_ENABLED: "0"
```

The equivalent `.chainguard/ci.yaml` file installs Go 1.24 as a package instead of using `actions/setup-go` and drops `actions/checkout`:

```yaml {title=".chainguard/ci.yaml"}
version: 1
environment:
  packages: [go-1.24]
checks:
  test:
    cmd: go test ./...
    vars:
      CGO_ENABLED: "0"
    timeout: 15m
    when:
      changed: ["**/*.go", "go.mod", "go.sum"]
    network:
      egress: [proxy.golang.org, sum.golang.org]
```

The `network` key lets the check download modules. The workflow doesn't list these hosts, because a GitHub Actions job can reach any host. To find the hosts a check needs, refer to [Find the hosts a check needs](#find-the-hosts-a-check-needs).

By default, `actions/setup-go` caches downloaded modules and build output between runs. This file uses no cache, so each run downloads its modules and builds from scratch. To reuse Go build output and downloaded modules between runs, add the `gobuild` and `gomod` caches, as described in [Caches](/chainguard/checks/variations-services-caches/#caches). Go still downloads any module that the cache doesn't have, so keep the `network` hosts. With the cache, `go test` still runs every test instead of reusing earlier results.

## Find the hosts a check needs {#find-the-hosts-a-check-needs}

The run page that a check's **Details** link opens lists each host the check tried to reach, and suggests an `egress` list built from the hosts it connected to. The suggestion includes only connections that the check's `egress` allowed, so to see every host a check uses, let it reach any host for one run:

1. In a pull request whose code you trust, set the check's `network.egress` to `["*"]`. With `*`, the check's commands can reach any public host by name.
2. When the check finishes, open its **Network activity** on the run page, as described in [A check can't reach a host](/chainguard/checks/troubleshooting/#a-check-cant-reach-a-host).
3. Review the hosts listed after **Suggested allowlist (review before applying)**, replace `*` with them, and push the change.

The suggestion covers only that run. It leaves out connections to an IP address that the check didn't look up by name, and hosts that the check reaches only on other paths, such as retries, fallback mirrors, or code that the run didn't exercise.

To open the run page, you need a role in your Chainguard organization, as described in [The Details link or a chainctl checks command says you don't have access](/chainguard/checks/troubleshooting/#the-details-link-or-a-chainctl-checks-command-says-you-dont-have-access). Without one, list the hosts you expect, run the check, and look in its log for connection errors.

## Switch over

To move a workflow to Chainguard Checks without a gap in coverage, follow these steps:

1. In a pull request, add the equivalent checks to `.chainguard/ci.yaml`, and keep the GitHub Actions workflow.
2. Compare the results of both on that pull request and the ones after it.
3. When the results match, add **Chainguard Checks (presubmit)** as a required status check, as described in [Require checks before merging](/chainguard/checks/github/#require-checks-before-merging).
4. Remove the GitHub Actions workflow, and remove its jobs from your required status checks.

