> ## Documentation Index
> Fetch the complete documentation index at: https://docs.run.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# GitHub workflows

> Opt into run.cloud CI from .github/workflows with runs-on: runcloud

# GitHub workflows

run.cloud CI reads your repository at the commit SHA and claims jobs that
**opt in** with:

```yaml theme={null}
runs-on: runcloud
```

Also accepted: `run.cloud`, `run-cloud`, or a list that includes one of those
labels (for example `[self-hosted, runcloud]`).

Every other job is ignored by run.cloud and can keep running on GitHub Actions
as usual.

## First workflow

```yaml theme={null}
# .github/workflows/ci.yml
name: CI

on:
  workflow_dispatch:

jobs:
  test:
    runs-on: runcloud
    container: node:22
    env:
      RUNCLOUD_CPU: "1"
      RUNCLOUD_MEMORY: "2048"
      RUNCLOUD_PATHS: "src/**,package.json,package-lock.json"
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test
```

Steps run under `/bin/sh` with a portable setup, so common images such as
`node:22` (Debian `dash`) work without requiring bash.

### Why `on: workflow_dispatch`?

GitHub Actions does not know about a `runcloud` runner. If you set
`on: pull_request` without care, Actions may show a job **waiting for a
self-hosted runner** that will never start.

run.cloud does **not** use GitHub Actions to schedule work. The GitHub App
receives `pull_request` / `push` webhooks and runs opted-in jobs itself.
Using `workflow_dispatch` (or another trigger that does not auto-queue on PR)
keeps the Actions UI clean while run.cloud still executes checks on every
relevant push and PR.

Path filters for run.cloud are set with `RUNCLOUD_PATHS` (or native `paths`,
see [configuration](/ci/configuration)), not only with GitHub's `on.paths`.

## Multi-step Node job

```yaml theme={null}
name: frontend

on:
  workflow_dispatch:

jobs:
  build-and-test:
    runs-on: runcloud
    container: node:22
    env:
      RUNCLOUD_CPU: "2"
      RUNCLOUD_MEMORY: "4096"
      RUNCLOUD_PATHS: "apps/web/**,packages/**,pnpm-lock.yaml"
    steps:
      - uses: actions/checkout@v4
      - run: corepack enable
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm test
      - run: pnpm build
```

## Working directory

Use `working-directory` on a step (or native `workdir`) for monorepo packages:

```yaml theme={null}
jobs:
  go-sdk:
    runs-on: runcloud
    container: golang:1.22
    env:
      RUNCLOUD_PATHS: "run-cloud/go-sdk/**"
    steps:
      - uses: actions/checkout@v4
      - run: go test ./...
        working-directory: run-cloud/go-sdk
```

## Multiple jobs

Each job becomes its own Check Run: `run.cloud / unit`, `run.cloud / lint`.

```yaml theme={null}
name: checks

on:
  workflow_dispatch:

jobs:
  unit:
    runs-on: runcloud
    container: node:22
    env:
      RUNCLOUD_PATHS: "src/**,test/**"
    steps:
      - run: npm ci
      - run: npm test

  lint:
    runs-on: runcloud
    container: node:22
    env:
      RUNCLOUD_PATHS: "src/**,eslint.config.*"
    steps:
      - run: npm ci
      - run: npm run lint

  # Still on GitHub Actions, not claimed by run.cloud
  deploy-preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: echo "deploy preview on GitHub-hosted runners"
```

Job names must be **unique** across all workflow files and any native
[`.runcloud/ci.yml`](/ci/configuration).

## Multi-line scripts

```yaml theme={null}
steps:
  - run: |
      set -euo pipefail
      npm ci
      npm run test -- --coverage
      npm run build
```

## Path filters and skipped checks

When `RUNCLOUD_PATHS` (or job `paths`) is set and none of the changed files
match, run.cloud still posts a Check Run with conclusion **success** and title
**Skipped**. That keeps required checks from blocking unrelated PRs.

```yaml theme={null}
env:
  RUNCLOUD_PATHS: "services/api/**,go.mod,go.sum"
```

If you omit path filters, the job runs on every pull request and push that the
App receives for that repository.

## Images

Set the sandbox image with `container:` (string or `{ image: ... }`) or native
`image:`:

```yaml theme={null}
container: python:3.12-slim
# or
container:
  image: python:3.12-slim
```

The first time you use a given image, the job may take longer to become ready.
Later runs with the same image are usually faster. See
[Custom images](/compute/images).

## Branch protection

In GitHub → **Settings → Branches → Branch protection rules**, add required
status checks such as:

* `run.cloud / unit`
* `run.cloud / lint`

Exact names match the job id (or job `name:` if set) after the `run.cloud / `
prefix.

## Re-running a check

Use **Re-run** on the Check Run in the GitHub UI. run.cloud receives the
`check_run` re-request and enqueues that job again for the same SHA.

## Common mistakes

| Symptom                                     | Likely cause                                                                                               |
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| No checks at all                            | GitHub App not installed, or installation not linked in the dashboard                                      |
| Job stuck “Waiting for a runner” in Actions | GitHub Actions is scheduling the file. Use `on: workflow_dispatch` for run.cloud-only workflows            |
| `run.cloud / config` failed                 | Unsupported syntax on an opted-in job (matrix, most `uses:`, etc.). See [configuration](/ci/configuration) |
| Always skipped                              | `RUNCLOUD_PATHS` does not match any files in the PR                                                        |
| Secret not found                            | Name/group missing in the run.cloud org. See [Secrets in CI](/ci/secrets)                                  |
