# Gate deploys in CI

> Fail a GitHub Actions job on a held release, make a deploy wait for the verdict, and send findings to GitHub's Security tab.

Every push is already checked through the GitHub App. These steps make your CI act on the verdict. They use the Stableship CLI, which needs Node 22 or newer.

## Make a token for CI

In Stableship open **Settings**, then **Connections**, open **API tokens and connected apps**, and choose **Create token**. Give each job its own token, with only what it needs:

- Waiting for a verdict or reading findings: **Read findings and rules**.
- Starting a check: also **Start checks and verify fixes**.

Add it to the GitHub repository as a secret named `STABLESHIP_TOKEN`.

## Hold a deploy until Stableship decides

For hosts that deploy from GitHub Actions or wait for CI (Railway, Fly and others), add a job that waits for Stableship's decision on the commit, and make the deploy depend on it:

```yaml
jobs:
  gate:
    runs-on: ubuntu-latest
    timeout-minutes: 115
    steps:
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npx --yes @stableship/cli@1 wait --sha "$GITHUB_SHA"
        env:
          STABLESHIP_TOKEN: ${{ secrets.STABLESHIP_TOKEN }}
  deploy:
    needs: gate
    # your deploy
```

On Railway, you can instead turn on **Wait for CI** for the service, so Railway deploys only after the workflow's checks pass.

| Stableship says | The job | The deploy |
|---|---|---|
| Passed, warned, skipped, or a person shipped it anyway | Succeeds | Goes out |
| A problem blocks the release | Fails | Waits: fix it, or ship anyway in Stableship and run the job again |
| Stableship could not finish, and the change is low-risk | Succeeds | Goes out |
| Stableship could not finish, and the change touches sign-in, data or payments | Fails | Waits for a person |
| No check started for the commit within 20 minutes | Succeeds, with a warning | Goes out |

While it waits, the Stableship check on the commit shows what is being tested and roughly how long is left.

## Fail a job on a held release

To start a check from CI and wait for its verdict:

```yaml
- name: Stableship
  run: npx --yes @stableship/cli@1 check --ref "$GITHUB_SHA" --wait --timeout 900
  env:
    STABLESHIP_TOKEN: ${{ secrets.STABLESHIP_TOKEN }}
```

| Exit code | Meaning |
|---|---|
| 0 | Passed, passed with warnings, skipped or shipped anyway. Also 0 when Stableship itself failed: the CLI prints a warning. |
| 1 | Held: a problem blocks the release. |
| 2 | Could not run: a usage error, a missing or rejected token, an API error, or the wait ran past `--timeout`. |

To fail the step only on a held release, not when it could not run, add `|| [ $? -eq 2 ]` after the command.

The CLI finds your app from the repository's `origin`. Pass `--project <id>` (from `stableship status`) to choose one.

## Findings in GitHub's Security tab

```yaml
- run: npx --yes @stableship/cli@1 findings --sarif > stableship.sarif
  env:
    STABLESHIP_TOKEN: ${{ secrets.STABLESHIP_TOKEN }}
- uses: github/codeql-action/upload-sarif@v3
  with: { sarif_file: stableship.sarif, category: stableship }
```

The job needs `permissions: security-events: write`.

## A pre-push hook

`stableship hook install` adds a Git pre-push hook: a push to your default branch prints the open blocking problems first. With `--strict` it refuses the push while something blocks, unless the commit has a `Gate:` trailer. The hook never stops a push because Stableship could not be reached. `stableship hook uninstall` removes it.
