Skip to content

Promote checks Dev to Prod

Goal

Move quality checks from one Qualytics environment to another, with Git as the system of record. Author and review checks in Dev, commit the YAML to a Git repo, and apply them to Prod through a controlled, repeatable process. The same flow scales to Test, Staging, or any other environment.

Permissions

Step Endpoint Role Team permission
Export from Dev GET /api/quality-checks (paginated) Member Reporter on the source datastore's team
Read containers (for name resolution) GET /api/containers Member Reporter
Import to Prod (create) POST /api/quality-checks Member Author (or Drafter for Draft status)
Import to Prod (update) PUT /api/quality-checks/{id} Member Author

Use a separate token per environment

Don't reuse a Dev token in Prod. Each environment should have its own service account with the minimum permissions it needs.

Prerequisites

  • The CLI is installed and authenticated for both environments (or you swap QUALYTICS_URL and QUALYTICS_TOKEN between steps).
  • The target datastore in Prod has been synced. Container names referenced in the YAML must resolve in Prod.
  • A Git repository to hold the exported YAML (this is what makes the workflow auditable).

CLI workflow

graph LR
    Dev[(Dev datastore)] -->|export| FS[checks/ folder]
    FS -->|git add + commit| Repo[(Git repo)]
    Repo -->|git pull| CI[CI / local]
    CI -->|import --dry-run| Preview[Preview diff]
    Preview --> Apply[import without --dry-run]
    Apply --> Prod[(Prod datastore)]

1. Export from Dev

export QUALYTICS_URL=https://dev.qualytics.io
export QUALYTICS_TOKEN=$DEV_TOKEN

qualytics checks export --datastore-id 1 --output ./checks/

The output directory is one YAML file per check, organized by container:

checks/
  orders/
    isNotNull_customer_id.yaml
    satisfiesExpression_total_positive.yaml
  customers/
    isUnique_email.yaml

2. Commit and review

git add checks/
git commit -m "Update quality checks from dev"
git push

A reviewer can see exactly which checks changed in the PR diff.

3. Preview import to Prod

export QUALYTICS_URL=https://prod.qualytics.io
export QUALYTICS_TOKEN=$PROD_TOKEN

qualytics checks import --datastore-id 99 --input ./checks/ --dry-run
Loaded 15 check definitions from ./checks/
[DRY RUN] Importing to datastore 99...
          Import Summary
┏━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━┓
┃ Datastore ID ┃ Created ┃ Updated ┃ Failed ┃
┡━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━┩
│ 99           │ 10      │ 5       │ 0      │
└──────────────┴─────────┴─────────┴────────┘

4. Apply

qualytics checks import --datastore-id 99 --input ./checks/

5. Promote to multiple datastores in one shot

qualytics checks import \
    --datastore-id 99 \
    --datastore-id 100 \
    --datastore-id 101 \
    --input ./checks/

Behind the scenes

CLI step Method Path Notes
checks export (list) GET /api/quality-checks?datastore_id={id} Paginated. The CLI walks all pages.
checks export (file write) (local) One YAML per check, with _qualytics_check_uid for stable identity.
checks import (resolve containers) GET /api/containers?datastore_id={id} Maps container names to IDs in the target.
checks import (existing checks) GET /api/quality-checks?datastore_id={id} Used to match by _qualytics_check_uid.
checks import (create new) POST /api/quality-checks Per check that didn't exist in target.
checks import (update existing) PUT /api/quality-checks/{id} Per check matched on UID.

Match-by-UID is what makes the import idempotent. Re-running the same import is always safe.

Python equivalent

The promote workflow has too many moving parts (paginated export, file layout, idempotent matching) to replicate fully in a few lines. For automation that's already in Python, the simplest pattern is to subprocess the CLI:

import os
import subprocess

env = {**os.environ, "QUALYTICS_URL": "https://dev.qualytics.io",
                     "QUALYTICS_TOKEN": os.environ["DEV_TOKEN"]}
subprocess.run(
    ["qualytics", "checks", "export", "--datastore-id", "1", "--output", "./checks/"],
    env=env, check=True,
)

env = {**os.environ, "QUALYTICS_URL": "https://prod.qualytics.io",
                     "QUALYTICS_TOKEN": os.environ["PROD_TOKEN"]}
subprocess.run(
    ["qualytics", "checks", "import", "--datastore-id", "99", "--input", "./checks/", "--dry-run"],
    env=env, check=True,
)
subprocess.run(
    ["qualytics", "checks", "import", "--datastore-id", "99", "--input", "./checks/"],
    env=env, check=True,
)

If you genuinely need pure-Python, the rough sketch is: paginate GET /api/quality-checks, write per-check YAML with stable UIDs, then for each YAML in the target environment, look up by UID via GET /api/quality-checks?_qualytics_check_uid={uid} and PUT or POST accordingly.

Variations and advanced usage

Filter the export

Promote only a subset:

# Active checks for two specific containers, tagged production
qualytics checks export \
    --datastore-id 1 \
    --containers "10,20" \
    --tags "production" \
    --status Active \
    --output ./checks/

Dry-run gate in CI

Make dry-run the PR check, and the real apply the merge step. See GitHub Actions pipelines.

Promote full configuration, not just checks

If connections, datastores, computed containers, and computed fields need to move too, use config export / config import instead. See Export and import full configuration.

Roll back

Roll back is git revert on the YAML, then re-import. Because import is upsert, the previous values are restored from the file.

Troubleshooting

Symptom Likely cause Fix
Container 'X' not found in target datastore Target hasn't been synced, or table doesn't exist there yet qualytics operations sync --datastore-id 99 first.
Some checks fail to update with 403 Token has Drafter permission but check status is Active Re-import with Author permission, or change status to Draft in the YAML.
Drift after import: checks have different IDs That's expected; IDs differ per environment. UIDs are what matter. Confirm with additional_metadata._qualytics_check_uid matches in source and target YAML.
Many "Updated" entries on a fresh Prod Some checks already existed in Prod from previous experiments Decide whether to keep or replace; a clean dry-run should show this clearly.
Re-import shows changes when nothing was edited A timestamp or auto-generated field is being persisted File a bug with the sample diff; export should be deterministic.