# GitHub Actions pipelines

Source: https://userguide.qualytics.io/cli/examples/github-actions-pipelines/

## Goal

Run Qualytics CLI commands automatically inside GitHub Actions: validate quality check changes on pull requests, promote checks to production on tagged releases, run nightly scans on a schedule, and detect drift between Git and the live environment. The same patterns translate to GitLab CI, Jenkins, or any other CI provider.

## Permissions

| Step | Endpoint | Role | Team permission |
| --- | --- | --- | --- |
| `checks import --dry-run` | `GET /api/quality-checks`, `GET /api/containers` | `Member` | `Reporter` (read-only validation) |
| `checks import` (apply) | `POST` / `PUT /api/quality-checks` | `Member` | `Author` |
| `config import` (apply) | `POST`/`PUT` on multiple resources | `Manager` (if creating connections), `Member` + `Editor` otherwise | `Editor` for non-connection resources |
| `operations sync/profile/scan` | `POST /api/operations/run` | `Member` | `Editor` |

> **Warning: Scope tokens per environment**
>
> Use a separate service token per environment (`DEV_QUALYTICS_TOKEN`, `PROD_QUALYTICS_TOKEN`). Never share Prod tokens with PR jobs that any contributor can trigger.

## Prerequisites

- A GitHub repository with the Qualytics YAML configuration in version control.
- GitHub Environments configured for `test`, `production`, etc., each with its own secrets.
- Service tokens for each environment, stored as GitHub Secrets.

## CLI workflow

Each subsection below is a complete workflow file. Drop them into `.github/workflows/` and adjust environment names and secrets.

### Validate on pull request, promote on release

A safe two-stage pattern: every PR runs a dry-run import to confirm the YAML is valid; merging the PR doesn't deploy anything; cutting a release deploys to Prod.

```yaml
# .github/workflows/quality-checks.yml
name: Quality Checks

on:
  pull_request:
    paths: ['checks/**']
  release:
    types: [published]

jobs:
  validate-on-pr:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - name: Install Qualytics CLI
        run: pip install qualytics-cli
      - name: Dry-run import to Test
        env:
          QUALYTICS_URL:   ${{ secrets.TEST_QUALYTICS_URL }}
          QUALYTICS_TOKEN: ${{ secrets.TEST_QUALYTICS_TOKEN }}
          CI:              true
        run: |
          qualytics checks import \
            --datastore-id ${{ vars.TEST_DATASTORE_ID }} \
            --input ./checks/ \
            --dry-run

  promote-to-prod:
    if: github.event_name == 'release'
    runs-on: ubuntu-latest
    environment: production         # required reviewers gate happens here
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: pip install qualytics-cli
      - name: Apply checks to Prod
        env:
          QUALYTICS_URL:   ${{ secrets.PROD_QUALYTICS_URL }}
          QUALYTICS_TOKEN: ${{ secrets.PROD_QUALYTICS_TOKEN }}
          CI:              true
        run: |
          qualytics checks import \
            --datastore-id ${{ vars.PROD_DATASTORE_ID }} \
            --input ./checks/
```

### Nightly quality scan

```yaml
# .github/workflows/nightly-scan.yml
name: Nightly Scan

on:
  schedule:
    - cron: '0 6 * * *'

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - run: pip install qualytics-cli
      - name: Run pipeline
        env:
          QUALYTICS_URL:   ${{ secrets.QUALYTICS_URL }}
          QUALYTICS_TOKEN: ${{ secrets.QUALYTICS_TOKEN }}
          CI:              true
        run: |
          qualytics operations sync    --datastore-id ${{ vars.DATASTORE_ID }}
          qualytics operations profile --datastore-id ${{ vars.DATASTORE_ID }}
          qualytics operations scan    --datastore-id ${{ vars.DATASTORE_ID }}
```

### Full configuration promotion (with secrets)

```yaml
# .github/workflows/promote-config.yml
name: Promote Quality Config

on:
  push:
    branches: [main]
    paths: ['qualytics-config/**']

jobs:
  promote:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: pip install qualytics-cli
      - name: Dry-run
        env:
          QUALYTICS_URL:   ${{ secrets.PROD_QUALYTICS_URL }}
          QUALYTICS_TOKEN: ${{ secrets.PROD_QUALYTICS_TOKEN }}
          DB_HOST:         ${{ secrets.PROD_DB_HOST }}
          DB_USER:         ${{ secrets.PROD_DB_USER }}
          DB_PASSWORD:     ${{ secrets.PROD_DB_PASSWORD }}
          CI:              true
        run: qualytics config import --input ./qualytics-config --dry-run
      - name: Apply
        env:
          QUALYTICS_URL:   ${{ secrets.PROD_QUALYTICS_URL }}
          QUALYTICS_TOKEN: ${{ secrets.PROD_QUALYTICS_TOKEN }}
          DB_HOST:         ${{ secrets.PROD_DB_HOST }}
          DB_USER:         ${{ secrets.PROD_DB_USER }}
          DB_PASSWORD:     ${{ secrets.PROD_DB_PASSWORD }}
          CI:              true
        run: qualytics config import --input ./qualytics-config
```

### Drift detection

```yaml
# .github/workflows/drift-check.yml
name: Drift Check

on:
  schedule:
    - cron: '0 9-17 * * 1-5'
  workflow_dispatch:

jobs:
  drift:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
      - run: pip install qualytics-cli
      - name: Re-export over committed folder
        env:
          QUALYTICS_URL:   ${{ secrets.PROD_QUALYTICS_URL }}
          QUALYTICS_TOKEN: ${{ secrets.PROD_QUALYTICS_TOKEN }}
          CI:              true
        run: qualytics config export --datastore-id ${{ vars.PROD_DATASTORE_ID }} --output ./qualytics-config
      - name: Open drift PR if anything changed
        run: |
          if ! git diff --quiet -- qualytics-config/; then
            BRANCH="drift/$(date -u +%Y-%m-%dT%H%M)"
            git checkout -b "$BRANCH"
            git config user.email "drift-bot@example.com"
            git config user.name  "Qualytics Drift Bot"
            git add qualytics-config/
            git commit -m "drift: snapshot $(date -u +%Y-%m-%dT%H:%M:%SZ)"
            git push origin "$BRANCH"
            gh pr create --title "Qualytics drift detected" \
                         --body  "Drift snapshot from production. Review and decide whether to merge or revert in the UI."
          fi
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

## Behind the scenes

The CLI invocations inside each workflow hit the same API endpoints documented in the respective example pages (e.g., [Promote checks Dev to Prod](https://userguide.qualytics.io/cli/examples/promote-checks-dev-prod/), [Daily sync, profile, and scan](https://userguide.qualytics.io/cli/examples/daily-pipeline/)). GitHub Actions just provides the trigger and credential injection.

| GitHub Actions feature | Qualytics use |
| --- | --- |
| `secrets.<name>` | Env-scoped tokens, DB credentials |
| `vars.<name>` | Datastore IDs (non-sensitive) |
| `environment:` on a job | Required-reviewers gate before Prod |
| `paths:` filter | Run only on relevant file changes |
| `schedule:` trigger | Nightly scans, drift checks |

## Python equivalent

GitHub Actions itself is YAML; the actions invoke shell commands. There's no meaningful "Python equivalent" of GitHub Actions, but you can replace the `pip install qualytics-cli` + CLI invocations with direct API calls in Python if you prefer (see the Python sections of the per-scenario pages).

## Variations and advanced usage

### Required reviewers on Prod

In **Settings → Environments**, configure the `production` environment to require reviewer approval before any job that targets it can run. The job will pause until a reviewer approves.

### OIDC instead of long-lived tokens

If your Qualytics instance supports OIDC token exchange (some self-hosted deployments do), you can mint short-lived tokens from GitHub's OIDC provider rather than storing a long-lived service token. The auth call changes; everything else stays the same.

### Multiple environments in one workflow

Use a matrix and a per-environment secret prefix:

```yaml
strategy:
  matrix:
    env: [dev, staging, prod]
steps:
  - env:
      QUALYTICS_URL:   ${{ secrets[format('{0}_QUALYTICS_URL', matrix.env)] }}
      QUALYTICS_TOKEN: ${{ secrets[format('{0}_QUALYTICS_TOKEN', matrix.env)] }}
    run: qualytics datastores list
```

### Output the import summary as a PR comment

After a dry-run, post the `Import Summary` table back to the PR so reviewers see it without leaving GitHub. Use a community action like `marocchino/sticky-pull-request-comment` or write a `gh pr comment` step.

## Troubleshooting

| Symptom | Likely cause | Fix |
| --- | --- | --- |
| Workflow log shows banner output | `CI` and `QUALYTICS_NO_BANNER` not set | Add both to the env. |
| Job fails with `403 Forbidden` only on Prod | Token scoped to Dev only | Confirm the secret used matches the environment. |
| `Resource not accessible by integration` on `gh pr create` | Workflow lacks PR write permission | Add `permissions: { pull-requests: write }` to the job. |
| `pip install` is slow | Default Python action | Switch to `astral-sh/setup-uv@v3` and `uv pip install`. |
| Workflow runs but doesn't seem to do anything | `paths:` filter excluded the changed files | Confirm the path filter matches the actual change. |

## Related

- [Automation & CI/CD command reference](https://userguide.qualytics.io/cli/automation/)
- [Promote checks Dev to Prod](https://userguide.qualytics.io/cli/examples/promote-checks-dev-prod/): the manual workflow that the promote pipeline above automates.
- [Drift detection](https://userguide.qualytics.io/cli/examples/drift-detection/): the manual workflow that the drift pipeline above automates.
- [Daily sync, profile, and scan](https://userguide.qualytics.io/cli/examples/daily-pipeline/): the manual workflow that the nightly-scan pipeline above automates.

*Last updated: May 12, 2026.*
