diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..f539807
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,48 @@
+name: Bug report
+description: Something does not work as documented
+labels: ["bug"]
+body:
+ - type: textarea
+ id: what-happened
+ attributes:
+ label: What happened
+ description: What did you expect, and what happened instead?
+ validations:
+ required: true
+ - type: textarea
+ id: repro
+ attributes:
+ label: Reproduction
+ description: The command you ran and its full output. Include the migration SQL if relevant.
+ render: shell
+ validations:
+ required: true
+ - type: input
+ id: version
+ attributes:
+ label: PyClickHouseMigrator version
+ description: Output of `migrator --version`
+ placeholder: "2.1.0"
+ validations:
+ required: true
+ - type: input
+ id: clickhouse
+ attributes:
+ label: ClickHouse version
+ placeholder: "25.3"
+ validations:
+ required: true
+ - type: input
+ id: python
+ attributes:
+ label: Python version
+ placeholder: "3.13"
+ - type: dropdown
+ id: cluster
+ attributes:
+ label: Cluster mode
+ options:
+ - "No — single node"
+ - "Yes — ON CLUSTER"
+ validations:
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..4a7091a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,11 @@
+blank_issues_enabled: true
+contact_links:
+ - name: Documentation
+ url: https://maksim-burtsev.github.io/PyClickHouseMigrator/
+ about: Migration format, CI/CD, cluster mode, troubleshooting.
+ - name: Known limitations
+ url: https://maksim-burtsev.github.io/PyClickHouseMigrator/known-limitations/
+ about: Check here before filing — some behavior is intentional.
+ - name: Security vulnerability
+ url: https://github.com/Maksim-Burtsev/PyClickHouseMigrator/security/advisories/new
+ about: Report privately, not as a public issue.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..89508c4
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,31 @@
+name: Feature request
+description: Suggest a change or addition
+labels: ["enhancement"]
+body:
+ - type: textarea
+ id: problem
+ attributes:
+ label: What problem are you hitting?
+ description: Describe the situation, not the solution.
+ validations:
+ required: true
+ - type: textarea
+ id: proposal
+ attributes:
+ label: What would you like it to do?
+ validations:
+ required: true
+ - type: textarea
+ id: alternatives
+ attributes:
+ label: What are you doing today instead?
+ description: Workarounds, other tools, manual steps.
+ - type: checkboxes
+ id: scope
+ attributes:
+ label: Scope check
+ description: >
+ This tool intentionally stays small — it applies migrations, it is not a
+ schema platform. See docs/known-limitations.md.
+ options:
+ - label: I have read the known limitations and this is not one of them
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..04be1f3
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,12 @@
+## What
+
+
+
+## Checklist
+
+- [ ] `uv run ruff check . && uv run ruff format --check .`
+- [ ] `uv run mypy py_clickhouse_migrator/`
+- [ ] `uv run pytest` passes against a live ClickHouse
+- [ ] Tests added or updated for the behavior change
+- [ ] Docs / README / `llms.txt` updated if the CLI surface changed
+- [ ] `CHANGELOG.md` entry added
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1e3cbd4..45d68c7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -33,17 +33,27 @@ jobs:
run: uv run mypy py_clickhouse_migrator/
test:
+ # Keep this name stable: the master ruleset requires these exact check
+ # names. The ClickHouse version is reported by the step below instead.
name: Test (Python ${{ matrix.python-version }})
needs: lint
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
- python-version: ["3.11", "3.12", "3.13", "3.14"]
+ include:
+ - python-version: "3.11"
+ clickhouse: "24.8"
+ - python-version: "3.12"
+ clickhouse: "25.3"
+ - python-version: "3.13"
+ clickhouse: "latest"
+ - python-version: "3.14"
+ clickhouse: "latest"
services:
clickhouse:
- image: clickhouse/clickhouse-server:latest
+ image: clickhouse/clickhouse-server:${{ matrix.clickhouse }}
ports:
- 19000:9000
env:
@@ -94,14 +104,14 @@ jobs:
raise SystemExit(f"ClickHouse service did not become ready: {last_error}")
PY
- - name: Run tests
+ - name: Run tests (ClickHouse ${{ matrix.clickhouse }})
run: uv run pytest -v --cov=py_clickhouse_migrator --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v7
with:
files: coverage.xml
- flags: "python-${{ matrix.python-version }}"
+ flags: "python-${{ matrix.python-version }}-ch-${{ matrix.clickhouse }}"
token: ${{ secrets.CODECOV_TOKEN }}
test-cluster:
diff --git a/.github/workflows/demo.yml b/.github/workflows/demo.yml
new file mode 100644
index 0000000..a3a11a3
--- /dev/null
+++ b/.github/workflows/demo.yml
@@ -0,0 +1,81 @@
+name: Demo GIF
+
+on:
+ workflow_dispatch:
+ push:
+ branches: [master]
+ paths:
+ - demo/**
+ - .github/workflows/demo.yml
+
+concurrency:
+ group: demo-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: write
+
+jobs:
+ render:
+ name: Render assets/demo.gif
+ runs-on: ubuntu-latest
+
+ services:
+ clickhouse:
+ image: clickhouse/clickhouse-server:latest
+ ports:
+ - 19000:9000
+ env:
+ CLICKHOUSE_DB: test
+ CLICKHOUSE_USER: default
+ CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
+ options: >-
+ --health-cmd "clickhouse-client --query 'SELECT 1'"
+ --health-interval 2s
+ --health-timeout 5s
+ --health-retries 10
+
+ steps:
+ - uses: actions/checkout@v7
+ - uses: astral-sh/setup-uv@v10.0.1
+ with:
+ enable-cache: true
+ - uses: actions/setup-python@v7
+ with:
+ python-version: "3.14"
+
+ - name: Install migrator
+ run: |
+ uv sync --dev
+ echo "$PWD/.venv/bin" >> "$GITHUB_PATH"
+
+ - name: Wait for ClickHouse service
+ run: |
+ probe="from clickhouse_driver import Client; Client.from_url('clickhouse://default@localhost:19000/test').execute('SELECT 1')"
+ for i in $(seq 1 30); do
+ if .venv/bin/python -c "$probe" 2>/dev/null; then
+ echo "ClickHouse ready"
+ exit 0
+ fi
+ echo "Waiting for ClickHouse... ($i/30)"
+ sleep 2
+ done
+ echo "ClickHouse service did not become ready" >&2
+ exit 1
+
+ - name: Render tape
+ uses: charmbracelet/vhs-action@v2
+ with:
+ path: demo/demo.tape
+
+ - name: Commit GIF
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+ git add assets/demo.gif
+ if git diff --staged --quiet; then
+ echo "GIF unchanged"
+ else
+ git commit -m "chore: regenerate demo GIF [skip ci]"
+ git push
+ fi
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 1897315..ff720f5 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -46,10 +46,11 @@ jobs:
uses: pypa/gh-action-pypi-publish@release/v1
docker:
- name: Docker Hub
+ name: Docker Hub + GHCR
needs: publish
runs-on: ubuntu-latest
permissions:
+ contents: read
packages: write
steps:
@@ -59,6 +60,12 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
+ - name: Login to GHCR
+ uses: docker/login-action@v4
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
@@ -67,7 +74,9 @@ jobs:
id: meta
uses: docker/metadata-action@v6
with:
- images: maksimburtsev/py-clickhouse-migrator
+ images: |
+ maksimburtsev/py-clickhouse-migrator
+ ghcr.io/maksim-burtsev/pyclickhousemigrator
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
diff --git a/.gitignore b/.gitignore
index 2ff5f8f..26880da 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,3 +42,6 @@ Thumbs.db
# uv
.python-version
+
+# Local article drafts (published elsewhere, not part of the package)
+managing_clickhouse_migrations_devto*.md
diff --git a/CHANGELOG.txt b/CHANGELOG.md
similarity index 60%
rename from CHANGELOG.txt
rename to CHANGELOG.md
index f0574a8..3f7824f 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.md
@@ -1,8 +1,20 @@
-Change log
-==========
+# Changelog
-2.0.1 (02/08/2026)
--------------------
+All notable changes to this project are documented here.
+This project follows [Semantic Versioning](https://semver.org/): breaking changes only in major releases.
+
+## 2.1.0 — 2026-08-23
+
+- Published the image to GitHub Container Registry (`ghcr.io/maksim-burtsev/pyclickhousemigrator`) alongside Docker Hub, with identical tags and both architectures
+- Added ClickHouse 24.8 and 25.3 to the CI test matrix; previously only `latest` was tested
+- Fixed the `org.opencontainers.image.licenses` label in the image (the singular `license` key is not valid OCI and was ignored) and added title, description, and documentation labels
+- Added `SECURITY.md`, `CONTRIBUTING.md`, issue forms, and a pull request template
+- Renamed `LICENCE.txt` to `LICENSE` so GitHub detects the license, and `CHANGELOG.txt` to `CHANGELOG.md`
+- Added a demo GIF to the README, rendered from a VHS tape in CI
+- Documented the comparison with golang-migrate, Atlas, dbt, and Alembic
+- No CLI or migration behavior changes
+
+## 2.0.1 — 2026-08-02
- Added the hosted documentation site and linked it from README and package metadata
- Added the `py.typed` marker for typed-package discovery
@@ -11,8 +23,7 @@ Change log
- Updated CI and documentation workflows
- No CLI or migration behavior changes
-2.0.0 (26/04/2026)
--------------------
+## 2.0.0 — 2026-04-26
- SQL-first migration format: migrations are `.sql` files with `-- migrator:up`, `-- migrator:down`, and explicit `-- @stmt` blocks
- Removed the old documented Python migration workflow from user-facing documentation
@@ -25,8 +36,7 @@ Change log
- Hardened lock cluster name validation
- Full documentation refresh for README, llms.txt, llms-full.txt, and docs/*
-1.1.0 (30/03/2026)
--------------------
+## 1.1.0 — 2026-03-30
- New --send-receive-timeout option
- Docker image (Docker Hub)
@@ -35,8 +45,7 @@ Change log
- Checksum computed from SQL output instead of file content
- Removed unused termcolor dependency
-1.0.0 (22/03/2026)
--------------------
+## 1.0.0 — 2026-03-22
- Distributed locking with TTL
- Checksum validation & repair
@@ -47,17 +56,14 @@ Change log
- CLI error handling
- --version flag
-0.3 (19/03/2024)
-------------------
+## 0.3 — 2024-03-19
- Fix queries parsing
-0.2 (26/12/2023)
-------------------
+## 0.2 — 2023-12-26
- Add .env loading
-0.1 (24/12/2023)
-------------------
+## 0.1 — 2023-12-24
- First release
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..d6c9c29
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,54 @@
+# Contributing
+
+Thanks for taking the time. Small, focused pull requests are the easiest to merge.
+
+## Setup
+
+Requires [uv](https://docs.astral.sh/uv/) and Docker.
+
+```sh
+git clone https://github.com/Maksim-Burtsev/PyClickHouseMigrator.git
+cd PyClickHouseMigrator
+uv sync --dev
+```
+
+## Running tests
+
+Most tests need a live ClickHouse:
+
+```sh
+docker compose -f docker-compose.test.yml up -d --wait
+uv run pytest -v
+```
+
+Cluster tests run against a separate two-node compose file:
+
+```sh
+docker compose -f docker-compose.cluster.yml up -d --wait
+uv run pytest tests/ -m cluster -v
+```
+
+## Before opening a PR
+
+The same three checks CI runs:
+
+```sh
+uv run ruff check .
+uv run ruff format --check .
+uv run mypy py_clickhouse_migrator/
+```
+
+## Guidelines
+
+- Add a test for any behavior change — the test suite is the reason this tool is
+ safe to point at a production schema.
+- Keep the dependency set small. `click` and `clickhouse-driver` are the only
+ runtime dependencies, and that is a feature.
+- Update `docs/` and `README.md` when you change CLI behavior, plus `llms.txt` /
+ `llms-full.txt` if the change affects the documented surface.
+- Add an entry to `CHANGELOG.md` under an "Unreleased" heading.
+
+## Reporting bugs
+
+Open an issue with the migrator version (`migrator --version`), ClickHouse
+version, the migration SQL if relevant, and the full command plus output.
diff --git a/Dockerfile b/Dockerfile
index aec9b25..168dd15 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,9 +1,11 @@
FROM python:3.14-slim
LABEL maintainer="Maksim Burtsev
SQL-first ClickHouse migrations for Python teams.
+Plain .sql files, ordered applies, checksums, rollback, dry-run — no ORM, no diff engine.
+
+