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

# Sync flags in CI

> Check shipd.config.ts for drift on pull requests and push it on merge with a service account token.

## Overview

In this guide you set up two GitHub Actions workflows for the `web-app` project: one runs `shipd diff --exit-code` on pull requests that touch your flag definitions, and one runs `shipd push` when it merges to `main`. Both authenticate with a service account token (`shipd_sa_…`), not a personal key.

**Prerequisites**

* Your flag config and generated output committed (`shipd.config.ts` and TypeScript types, or Go source and generated Go accessors). See [Define flags in code](/config-as-code/define-flags).
* For TypeScript, `@shipdit/cli` in your `devDependencies`. The Go workflow pins the shared CLI to 0.5.0 (the upcoming Go-support release); update that version to match your team once it is published.
* Permission to create service accounts (organization admin or owner).

## Create a service account token

Create a service account named `github-actions`, then create a token bound to the `web-app` **project**. Do not bind it to an environment: `shipd push` makes project-wide changes, and environment-bound credentials get `403 forbidden`. Steps are in [Create a service account](/enterprise/service-accounts).

Use two tokens so pull request jobs cannot change flags:

| Token | Secret name | Scopes |
| - | - | - |
| Pull request (read-only) | `SHIPD_DIFF_TOKEN` | `flag:read`, `environment:read` |
| Main branch (write) | `SHIPD_PUSH_TOKEN` | `flag:update`, `flag:create`, `flag_state:update`, `flag_production:toggle`, `environment:read` |

Add `flag:archive` only if the job runs `shipd push --prune`. Why each permission is needed is in [Push permissions](/reference/cli#permissions).

Create a `shipd-diff` environment under **Settings → Environments**, require trusted reviewers, and store `SHIPD_DIFF_TOKEN` as an environment secret there. Review the pull request commit before approving this environment job: TypeScript and Go configs execute code, and Go tests execute pull-request code too. Restrict approval to trusted changes; do not approve untrusted fork jobs. The examples deliberately gate the whole diff job before it receives the token.

Store `SHIPD_PUSH_TOKEN` as a repository secret under **Settings → Secrets and variables → Actions** for the protected `main` branch workflow.

Both workflow examples use the file paths from [Define flags in code](/config-as-code/define-flags). Go source selects Go output by default unless `typesLanguage` overrides it.

## Check drift on pull requests

```yaml TypeScript theme={null}
# .github/workflows/flags-diff.yml
name: flags-diff
on:
  pull_request:
    paths: ["shipd.config.ts"]

jobs:
  diff:
    environment: shipd-diff
    permissions:
      contents: read
    runs-on: ubuntu-latest
    env:
      SHIPD_API_URL: https://api.useshipd.com
      SHIPD_API_KEY: ${{ secrets.SHIPD_DIFF_TOKEN }}
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm exec shipd diff --exit-code --env production
```

```yaml Go theme={null}
# .github/workflows/flags-diff.yml
name: flags-diff
on:
  pull_request:
    paths: ["**/*.go", "go.mod", "go.sum"]

jobs:
  diff:
    environment: shipd-diff
    permissions:
      contents: read
    runs-on: ubuntu-latest
    env:
      SHIPD_API_URL: https://api.useshipd.com
      SHIPD_API_KEY: ${{ secrets.SHIPD_DIFF_TOKEN }}
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - uses: actions/setup-go@v6
        with:
          go-version: "1.23"
      - run: npm install -g @shipdit/cli@0.5.0
      - name: Validate definitions and generated accessors
        run: |
          shipd types gen
          git ls-files --error-unmatch internal/flags/flags_gen.go
          git diff --exit-code -- internal/flags/flags_gen.go
          go test ./...
      - run: shipd diff --exit-code --env production
```

`SHIPD_API_URL` is required. Without it, a runner that never ran `shipd login` calls `http://localhost:8000`.

| Exit | Result | What to do |
| - | - | - |
| `0` | No drift | Nothing. |
| `2` | The pull request changes flags | Expected when you edit `shipd.config.ts`. Read the plan in the job log. Make the job non-blocking if you only want the plan as information. |
| `4` | Unlinked flags in the plan | Run `shipd diff` locally and follow its note. See [exit codes](/reference/cli#exit-codes). |
| `1` | Error | Read the `Error:` line. See [Errors](/reference/errors#cli-error-codes). |

`shipd diff` also reports flags that exist on the server but not in the file, as `- archive flag`, and counts them as drift. Add them to `shipd.config.ts` (or `shipd pull`) so the diff shows only your changes.

## Push on main

```yaml TypeScript theme={null}
# .github/workflows/flags-push.yml
name: flags-push
on:
  push:
    branches: [main]
    paths: ["shipd.config.ts"]

concurrency: shipd-push

jobs:
  push:
    runs-on: ubuntu-latest
    env:
      SHIPD_API_URL: https://api.useshipd.com
      SHIPD_API_KEY: ${{ secrets.SHIPD_PUSH_TOKEN }}
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - name: shipd push
        run: |
          set +e
          pnpm exec shipd push --ci --env production
          code=$?
          if [ "$code" -eq 5 ]; then
            echo "::warning::shipd push queued changes for approval. They are not live until approved."
            exit 0
          fi
          exit "$code"
```

```yaml Go theme={null}
# .github/workflows/flags-push.yml
name: flags-push
on:
  push:
    branches: [main]
    paths: ["**/*.go", "go.mod", "go.sum"]

concurrency: shipd-push

jobs:
  push:
    runs-on: ubuntu-latest
    env:
      SHIPD_API_URL: https://api.useshipd.com
      SHIPD_API_KEY: ${{ secrets.SHIPD_PUSH_TOKEN }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - uses: actions/setup-go@v6
        with:
          go-version: "1.23"
      - run: npm install -g @shipdit/cli@0.5.0
      - name: Validate definitions and generated accessors
        run: |
          shipd types gen
          git ls-files --error-unmatch internal/flags/flags_gen.go
          git diff --exit-code -- internal/flags/flags_gen.go
          go test ./...
      - name: shipd push
        run: |
          set +e
          shipd push --ci --env production
          code=$?
          if [ "$code" -eq 5 ]; then
            echo "::warning::shipd push queued changes for approval. They are not live until approved."
            exit 0
          fi
          exit "$code"
```

* `--ci` skips the confirmation prompt. `CI=true`, which GitHub Actions sets, does the same. Without either, push is cancelled in a non-interactive job.
* `concurrency` stops two merges from pushing at once and conflicting with each other.
* Pushing to `production` needs `flag_production:toggle` even for a `default`-only change.

## Go projects

Use the same tokens, permission scopes, and exit codes. Install Go 1.23+ and Node.js 22.12+ on the runner. The CLI reads `shipd.config.go` directly and selects Go output by default unless `typesLanguage` overrides it. The workflows watch all Go source so edits to imported local helper packages also trigger them. Include any non-Go files those helpers read in the path filters, and adjust the output checks if you override `typesLanguage` or `typesOut`.

```bash theme={null}
shipd types gen
git ls-files --error-unmatch internal/flags/flags_gen.go
git diff --exit-code -- internal/flags/flags_gen.go
shipd diff --exit-code
```

`git ls-files` fails if the generated output is not tracked. The first diff checks that committed accessors match the Go definitions; the CLI diff compares definitions with the server.

## Handle exit codes 3 and 5

| Exit | Meaning | What to do |
| - | - | - |
| `3` | Conflict. A flag changed on the server after the plan, a change for the flag is already waiting for approval, or an approval policy blocks a definition change. Nothing was applied. | Let the job fail. On your machine, `shipd pull`, re-apply your edits, and open a new pull request. See [Why did shipd push return a conflict?](/config-as-code/conflicts) |
| `5` | Some changes are queued for approval and not live. Other changes in the push applied. | The workflow above passes with a warning. A reviewer approves the request in the dashboard. See [Approvals](/operations/approvals). |

Do not add `--force` to CI. It overwrites changes made in the dashboard, and it cannot bypass approvals.

The full exit code list is in the [CLI reference](/reference/cli#exit-codes).

## Next steps

* [Push, pull, and diff](/config-as-code/sync)
* [Service account tokens](/reference/service-account-tokens)
* [GitHub Actions example](/examples/github-actions)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.