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.
How it works
Section titled “How it works”- Your branch is the truth. The
.sqlfiles 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
githubsource; 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 modelsGitHub Sync is configured per connection in Admin → GitHub (admin only).
- 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. - Configure the integration. Click Connect repository (or Edit once one is connected) to open the GitHub sync sheet: Repository as
owner/repo, branch (defaults tomain), the path inside the repo where model files live (defaults tomodels/), 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. - Run the first sync. Click Sync now and check the sync log. It reports created, updated, deleted, and unchanged counts plus the commit SHA.
Auto-sync on push
Section titled “Auto-sync on push”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.
- Generate a random secret of at least 16 characters, for example
openssl rand -hex 32. - Paste it into Webhook secret in the sheet that Edit opens on the Admin → GitHub page and save (or send it as
webhook_secretwithPUT /api/admin/github/{connectionId}). It is encrypted at rest with your server’sapp_secret_key. Leave the field empty on later saves to keep the stored one. - 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.
Model file format
Section titled “Model file format”A model is a .sql file, optionally starting with a YAML frontmatter block:
---materialization: table # view (default), table, incremental or materialized_viewtarget_database: analytics # default: defaulttable_engine: MergeTree # tables only, default: MergeTreeorder_by: (event_date, id) # MergeTree-family tables only, default: tuple()partition_by: toYYYYMM(event_date) # MergeTree-family tables onlydescription: Daily rollup of events---SELECT toDate(timestamp) AS event_date, count() AS eventsFROM eventsGROUP BY event_dateThe 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: incrementaltarget_database: analyticsincremental_strategy: delete_insertunique_key: order_idwatermark_column: last_updateorder_by: order_idtests: - 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_updateFROM raw.ordersGROUP BY order_idEach 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.
Sync semantics
Section titled “Sync semantics”| 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.
Disconnecting
Section titled “Disconnecting”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.
