Skip to content

GitHub Sync

Connect a GitHub repository to a connection and CH-UI imports your models from it: .sql files in the repo become models in CH-UI, and every sync keeps them up to date. Git is the source of truth: edit in your repo, merge to your branch, and CH-UI follows.

GitHub Sync is a Pro feature. Without an active license the endpoints return 402.

  • Your branch is the truth. The .sql files under the configured path define your models.
  • Sync is one-way (pull). CH-UI fetches the branch, parses each model file, and creates, updates, or deletes models to match the repo.
  • Only git-managed models are touched. Models imported from GitHub are tracked with a github source; models you created by hand in the UI are never modified or deleted by a sync. GitHub-managed models are read-only in the model editor.
  • One commit per sync. CH-UI resolves the branch to a commit SHA once and reads the tree and every file at that SHA, so a push during a sync never mixes two commits.
  • Unchanged commits are skipped. CH-UI remembers the last commit that synced without errors and does nothing if the branch hasn’t moved. A sync with errors does not record its commit, so the next sync retries it.
edit .sql ──▶ merge to branch ──▶ push webhook (or Sync now) ──▶ CH-UI pulls and updates models

GitHub Sync is configured per connection in Admin → GitHub (admin only).

  1. Create a token. A GitHub fine-grained personal access token with read access to the repository’s contents is enough. The token is encrypted at rest with your server’s app_secret_key.
  2. Configure the integration. Click Connect repository (or Edit once one is connected) to open the GitHub sync sheet: Repository as owner/repo, branch (defaults to main), the path inside the repo where model files live (defaults to models/), the personal access token and the webhook secret, then Save. The page then shows the repository, branch and path, whether a token and webhook secret are set, the last sync, and Test connection.
  3. Run the first sync. Click Sync now and check the sync log. It reports created, updated, deleted, and unchanged counts plus the commit SHA.

To sync automatically when the branch changes, add a push webhook in your repository settings pointing at:

https://your-ch-ui-server/api/github/webhook/{connectionId}

The webhook URL for the current connection is shown on the Admin → GitHub page.

A webhook secret is required. CH-UI verifies GitHub’s X-Hub-Signature-256 header on every delivery and rejects requests that don’t match. Until a secret is saved for the connection, every webhook request is refused with 403, so a push never triggers a sync from an unauthenticated caller.

  1. Generate a random secret of at least 16 characters, for example openssl rand -hex 32.
  2. Paste it into Webhook secret in the sheet that Edit opens on the Admin → GitHub page and save (or send it as webhook_secret with PUT /api/admin/github/{connectionId}). It is encrypted at rest with your server’s app_secret_key. Leave the field empty on later saves to keep the stored one.
  3. In the GitHub webhook settings, set Content type to application/json, paste the same value into Secret, and pick Just the push event.

Payloads are capped at 5 MiB. Only one sync runs per connection at a time: a push that arrives while a sync is running is folded into a single follow-up sync, and Sync now returns 409 until the running sync finishes.

A model is a .sql file, optionally starting with a YAML frontmatter block:

---
materialization: table # view (default), table, incremental or materialized_view
target_database: analytics # default: default
table_engine: MergeTree # tables only, default: MergeTree
order_by: (event_date, id) # MergeTree-family tables only, default: tuple()
partition_by: toYYYYMM(event_date) # MergeTree-family tables only
description: Daily rollup of events
---
SELECT
toDate(timestamp) AS event_date,
count() AS events
FROM events
GROUP BY event_date

The keys read are:

Key Applies to Meaning
materialization all view (default), table, incremental or materialized_view
target_database all Database the model builds into (default default)
description all Free text
table_engine tables Engine name (default MergeTree)
engine_args tables Engine arguments without the parentheses, e.g. version for ReplacingMergeTree
order_by tables ORDER BY for MergeTree-family engines (default tuple())
partition_by tables PARTITION BY for MergeTree-family engines
incremental_strategy incremental append (default) or delete_insert
unique_key delete_insert Column or comma-separated columns
watermark_column incremental Later runs take rows above the table’s maximum of this column
tests all Data tests, see below

“Tables” means table, incremental and materialized_view. The filename (minus .sql) becomes the model name, so it must be a valid identifier (^[a-zA-Z_][a-zA-Z0-9_]*$); a file like daily-events.sql is reported as an error. An unknown materialization, a delete_insert model without unique_key, or an invalid test is an error too, and the file is skipped. Files without frontmatter are imported as views against the default database.

An incremental model with tests:

---
materialization: incremental
target_database: analytics
incremental_strategy: delete_insert
unique_key: order_id
watermark_column: last_update
order_by: order_id
tests:
- kind: unique
column: order_id
- kind: not_null
column: status
- kind: accepted_values
column: status
values: [placed, paid, refunded]
severity: warn
- kind: relationship
column: user_id
to: users # a model, a table in analytics, or database.table
field: user_id
- kind: expression
expression: amount >= 0
---
SELECT
order_id,
argMax(user_id, updated_at) AS user_id,
argMax(status, updated_at) AS status,
argMax(amount, updated_at) AS amount,
max(updated_at) AS last_update
FROM raw.orders
GROUP BY order_id

Each test takes kind (not_null, unique, accepted_values, relationship, expression), column (all kinds but expression), severity (error, the default, or warn) and its own keys: values for accepted_values, to and field for relationship, expression for expression. The file is the source of truth: a sync replaces the model’s tests with the list, in order, and a file without tests leaves the model with none. See Data tests.

Reference other models with $ref(model_name), as in the editor. See Models.

Repo state CH-UI result
New .sql file Model created (source: github)
File content or frontmatter changed Model updated
File removed Model deleted (git-managed models only)
File unparseable or invalid Reported in the sync log; an existing model for that file is kept unchanged
Repository tree too large for one GitHub response Sync fails; point the path at a smaller folder

Every sync (manual or webhook) is recorded in the sync log with its trigger, counts, commit SHA, and any per-file errors.

Remove on the Admin → GitHub page removes the integration and its token, after a confirmation. Synced models stay in CH-UI; they just stop following the repo.