metabase-sync 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- metabase_sync-0.1.0/.gitignore +34 -0
- metabase_sync-0.1.0/CHANGELOG.md +57 -0
- metabase_sync-0.1.0/LICENSE +21 -0
- metabase_sync-0.1.0/PKG-INFO +315 -0
- metabase_sync-0.1.0/README.md +279 -0
- metabase_sync-0.1.0/examples/README.md +34 -0
- metabase_sync-0.1.0/pyproject.toml +78 -0
- metabase_sync-0.1.0/src/metabase_sync/__init__.py +1 -0
- metabase_sync-0.1.0/src/metabase_sync/__main__.py +4 -0
- metabase_sync-0.1.0/src/metabase_sync/apply/__init__.py +30 -0
- metabase_sync-0.1.0/src/metabase_sync/apply/_cards.py +261 -0
- metabase_sync-0.1.0/src/metabase_sync/apply/_collections.py +99 -0
- metabase_sync-0.1.0/src/metabase_sync/apply/_dashboards.py +257 -0
- metabase_sync-0.1.0/src/metabase_sync/apply/_pulses.py +183 -0
- metabase_sync-0.1.0/src/metabase_sync/apply/_runner.py +249 -0
- metabase_sync-0.1.0/src/metabase_sync/apply/_shared.py +107 -0
- metabase_sync-0.1.0/src/metabase_sync/apply/_snippets.py +81 -0
- metabase_sync-0.1.0/src/metabase_sync/cli.py +354 -0
- metabase_sync-0.1.0/src/metabase_sync/client.py +222 -0
- metabase_sync-0.1.0/src/metabase_sync/diff.py +104 -0
- metabase_sync-0.1.0/src/metabase_sync/errors.py +33 -0
- metabase_sync-0.1.0/src/metabase_sync/export.py +206 -0
- metabase_sync-0.1.0/src/metabase_sync/models.py +154 -0
- metabase_sync-0.1.0/src/metabase_sync/plan.py +135 -0
- metabase_sync-0.1.0/src/metabase_sync/serialize/__init__.py +0 -0
- metabase_sync-0.1.0/src/metabase_sync/serialize/cards.py +184 -0
- metabase_sync-0.1.0/src/metabase_sync/serialize/collections.py +93 -0
- metabase_sync-0.1.0/src/metabase_sync/serialize/dashboards.py +132 -0
- metabase_sync-0.1.0/src/metabase_sync/serialize/databases.py +37 -0
- metabase_sync-0.1.0/src/metabase_sync/serialize/paths.py +106 -0
- metabase_sync-0.1.0/src/metabase_sync/serialize/pulses.py +123 -0
- metabase_sync-0.1.0/src/metabase_sync/serialize/snippets.py +91 -0
- metabase_sync-0.1.0/src/metabase_sync/serialize/version.py +56 -0
- metabase_sync-0.1.0/src/metabase_sync/serialize/yamlio.py +91 -0
- metabase_sync-0.1.0/src/metabase_sync/settings.py +35 -0
- metabase_sync-0.1.0/tests/__init__.py +0 -0
- metabase_sync-0.1.0/tests/integration/__init__.py +0 -0
- metabase_sync-0.1.0/tests/integration/conftest.py +112 -0
- metabase_sync-0.1.0/tests/integration/docker-compose.yml +38 -0
- metabase_sync-0.1.0/tests/integration/test_round_trip.py +540 -0
- metabase_sync-0.1.0/tests/test_client_retries.py +99 -0
- metabase_sync-0.1.0/tests/test_concurrency.py +70 -0
- metabase_sync-0.1.0/tests/test_legacy_query.py +202 -0
- metabase_sync-0.1.0/tests/test_paths.py +53 -0
- metabase_sync-0.1.0/tests/test_paths_resolve.py +50 -0
- metabase_sync-0.1.0/tests/test_preflight.py +108 -0
- metabase_sync-0.1.0/tests/test_recipients.py +59 -0
- metabase_sync-0.1.0/tests/test_state_version.py +48 -0
- metabase_sync-0.1.0/tests/test_version_check.py +76 -0
- metabase_sync-0.1.0/tests/test_yamlio.py +66 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
*.egg
|
|
6
|
+
.venv/
|
|
7
|
+
.python-version
|
|
8
|
+
|
|
9
|
+
# Build
|
|
10
|
+
build/
|
|
11
|
+
dist/
|
|
12
|
+
*.whl
|
|
13
|
+
*.tar.gz
|
|
14
|
+
|
|
15
|
+
# uv
|
|
16
|
+
.uv-cache/
|
|
17
|
+
|
|
18
|
+
# Pytest / coverage
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
.coverage
|
|
21
|
+
.coverage.*
|
|
22
|
+
htmlcov/
|
|
23
|
+
|
|
24
|
+
# Local state / secrets
|
|
25
|
+
.env
|
|
26
|
+
.env.local
|
|
27
|
+
state/.plan.json
|
|
28
|
+
state/.last-apply.json
|
|
29
|
+
|
|
30
|
+
# Editors
|
|
31
|
+
.vscode/
|
|
32
|
+
.idea/
|
|
33
|
+
*.swp
|
|
34
|
+
.DS_Store
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] — 2026-06-20
|
|
11
|
+
|
|
12
|
+
### Commands
|
|
13
|
+
|
|
14
|
+
- `metabase-sync export` — pull the live Metabase instance into a YAML + SQL state tree under `state/`.
|
|
15
|
+
- `metabase-sync plan` — itemised per-resource diff between the on-disk state and the live instance. Writes a human-readable report to stdout and a machine-readable `state/.plan.json` for CI consumption.
|
|
16
|
+
- `metabase-sync apply` — re-derives the diff and executes the necessary POST/PUT calls. Idempotent (a second run is a no-op). Writes `state/.last-apply.json` as an audit log.
|
|
17
|
+
- `metabase-sync index` — debug command listing remote resource counts.
|
|
18
|
+
- `metabase-sync --version` prints the package version.
|
|
19
|
+
|
|
20
|
+
### Resources covered
|
|
21
|
+
|
|
22
|
+
- Collections, native and GUI cards (including dashboard-internal cards), dashboards (tabs + dashcards), snippets (with collection routing), pulses (dashboard subscriptions, recipients matched by email), and database manifests (name + engine only — credentials never serialised).
|
|
23
|
+
- Personal collections are intentionally excluded.
|
|
24
|
+
- Out-of-scope resources (alerts, segments, legacy metrics) produce a warning on export so they aren't silently missed.
|
|
25
|
+
|
|
26
|
+
### Code-first authoring
|
|
27
|
+
|
|
28
|
+
- Dashboards and pulses reference cards by relative file path (`card_path: ../../cards/foo.sql`) and dashboards by `dashboard_path` — brand-new items can be authored without knowing any Metabase ids ahead of time. Apply allocates ids and writes the new `entity_id` back into the source file.
|
|
29
|
+
|
|
30
|
+
### Round-trip stability
|
|
31
|
+
|
|
32
|
+
- Serialises Metabase's classic `legacy_query` form for native cards and accepts either classic or MBQL5 form for GUI cards. The diff path strips volatile keys (`lib/uuid`, `info`, `lib.convert/converted?`) so MBQL5 cards don't churn across exports.
|
|
33
|
+
- SQL bodies are stored byte-faithfully (trailing whitespace, template-tag UUIDs all survive).
|
|
34
|
+
- Atomic file writes (tempfile + `os.replace`) — Ctrl+C never leaves a corrupted YAML.
|
|
35
|
+
|
|
36
|
+
### Production safety
|
|
37
|
+
|
|
38
|
+
- **HTTP retries with exponential backoff** for connection errors, 408, 429, 502, 503, 504. Configurable via `HTTP_MAX_RETRIES` (default 3) and `HTTP_RETRY_BACKOFF_S` (default 1.0). `HTTP_TIMEOUT_S` default is 120s so `result_metadata` recomputation on large cards doesn't trip.
|
|
39
|
+
- **Single PUT for dashboard updates** — metadata + tabs + dashcards in one request, so a dashboard can never be left half-applied.
|
|
40
|
+
- **`result_metadata` preserved** for metadata-only card updates so Metabase doesn't re-run the underlying query on every cosmetic edit.
|
|
41
|
+
- **Reference preflight** — apply walks every dashboard and pulse and resolves every `card_path` / `dashboard_path` against the filesystem before any HTTP call.
|
|
42
|
+
- **Optimistic-concurrency check** — plan captures `updated_at` per item; apply refuses to overwrite anything that changed since plan unless `--force` is set.
|
|
43
|
+
- **Pre-apply confirmation prompt** — `apply` re-prints the plan and asks `[y/N]` unless `--yes`, `CI=true`, or stdout is not a TTY.
|
|
44
|
+
- **Loud error on missing pulse recipients** — apply errors with the list of bad emails unless `--allow-missing-recipients` is set.
|
|
45
|
+
- **`--delete` rejected at CLI parse time** with exit code 2 (planned for a future release).
|
|
46
|
+
- **Terraform exit codes**: `plan` exits 2 when changes are detected, 0 when nothing to do, 1 on error.
|
|
47
|
+
|
|
48
|
+
### Compatibility
|
|
49
|
+
|
|
50
|
+
- Built and tested against Metabase OSS v0.62.2 (pinned for CI). A non-blocking CI job runs the integration suite against `metabase:latest` to surface upcoming breakage.
|
|
51
|
+
- Supports Python 3.11, 3.12, 3.13.
|
|
52
|
+
|
|
53
|
+
### Known limitations
|
|
54
|
+
|
|
55
|
+
- `--delete` (opt-in archival of items absent from disk) is not yet implemented.
|
|
56
|
+
- Permissions, users, and groups are out of scope.
|
|
57
|
+
- Alerts, legacy metrics, and segments are not yet supported (export warns when they exist).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 novucs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: metabase-sync
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Metabase as code: export, plan, and apply dashboards, cards, collections, snippets and pulses via the Metabase REST API.
|
|
5
|
+
Project-URL: Homepage, https://github.com/novucs/metabase-sync
|
|
6
|
+
Project-URL: Repository, https://github.com/novucs/metabase-sync
|
|
7
|
+
Project-URL: Issues, https://github.com/novucs/metabase-sync/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/novucs/metabase-sync/blob/main/CHANGELOG.md
|
|
9
|
+
Author: novucs
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: as-code,cli,dashboards,gitops,iac,metabase
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: System Administrators
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Database
|
|
23
|
+
Classifier: Topic :: Software Development :: Version Control
|
|
24
|
+
Classifier: Topic :: System :: Systems Administration
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.11
|
|
27
|
+
Requires-Dist: httpx>=0.27.0
|
|
28
|
+
Requires-Dist: pydantic-settings>=2.6.0
|
|
29
|
+
Requires-Dist: pydantic>=2.8.0
|
|
30
|
+
Requires-Dist: python-slugify>=8.0.4
|
|
31
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
32
|
+
Requires-Dist: rich>=15.0.0
|
|
33
|
+
Requires-Dist: tenacity>=9.1.4
|
|
34
|
+
Requires-Dist: typer>=0.12.5
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# metabase-sync
|
|
38
|
+
|
|
39
|
+
[](https://pypi.org/project/metabase-sync/)
|
|
40
|
+
[](https://pypi.org/project/metabase-sync/)
|
|
41
|
+
[](https://github.com/novucs/metabase-sync/actions/workflows/ci.yml)
|
|
42
|
+
[](LICENSE)
|
|
43
|
+
|
|
44
|
+
Metabase as code. `export`, `plan`, and `apply` your dashboards, cards, collections, snippets and pulses via the regular Metabase REST API. Works with Metabase **OSS** (no Enterprise license required).
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
$ metabase-sync plan
|
|
48
|
+
|
|
49
|
+
plan ← state/
|
|
50
|
+
|
|
51
|
+
collections: 0 create, 0 update, 12 skip
|
|
52
|
+
cards: 1 create, 1 update, 48 skip
|
|
53
|
+
CREATE collections/finance/cards/q4-revenue.sql Q4 Revenue — SELECT SUM(revenue) FROM finance.q4
|
|
54
|
+
UPDATE collections/finance/cards/profit-margin.sql SQL: 23 → 25 lines, +2 −0
|
|
55
|
+
dashboards: 1 create, 0 update, 8 skip
|
|
56
|
+
CREATE collections/finance/dashboards/q4-review/dashboard.yaml Q4 Review (1 dashcards)
|
|
57
|
+
|
|
58
|
+
2 create, 1 update, 68 skip
|
|
59
|
+
run `metabase-sync apply` to apply this plan.
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Why this exists
|
|
63
|
+
|
|
64
|
+
Metabase's [official serialization](https://www.metabase.com/docs/latest/installation-and-operation/serialization) and Remote Sync are gated behind Enterprise / Pro. If you self-host the open-source edition, the `/api/ee/serialization/*` endpoints return 404 and you have no first-party way to put your dashboards under version control.
|
|
65
|
+
|
|
66
|
+
This tool wraps the plain REST API and gives you the same workflow without the licence wall:
|
|
67
|
+
|
|
68
|
+
- One YAML/SQL tree per Metabase instance, suitable for git.
|
|
69
|
+
- A `plan` / `apply` loop that reads exactly like Terraform.
|
|
70
|
+
- Round-trip determinism: `export` → `apply` → `export` produces zero diff.
|
|
71
|
+
- Code-first authoring: drop a new `.sql` and a new `dashboard.yaml`, run `apply`, done. No need to know any Metabase ids ahead of time.
|
|
72
|
+
|
|
73
|
+
## Install
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
uv tool install metabase-sync # recommended: isolated install
|
|
77
|
+
# or
|
|
78
|
+
pipx install metabase-sync
|
|
79
|
+
# or
|
|
80
|
+
pip install metabase-sync
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Quickstart
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
mkdir my-metabase && cd my-metabase
|
|
87
|
+
cat > .env <<EOF
|
|
88
|
+
METABASE_URL=https://your-instance.example.com
|
|
89
|
+
METABASE_API_KEY=mb_...
|
|
90
|
+
EOF
|
|
91
|
+
metabase-sync export # creates ./state/
|
|
92
|
+
git init && git add state && git commit -m "import metabase state"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Then your day-to-day:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Edit a .sql or dashboard.yaml in your editor.
|
|
99
|
+
metabase-sync plan # see exactly what will change
|
|
100
|
+
metabase-sync apply # write changes
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Commands
|
|
104
|
+
|
|
105
|
+
| Command | Purpose |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| `metabase-sync export` | Pull the live instance into the on-disk state tree. |
|
|
108
|
+
| `metabase-sync plan` | Read-only diff. Prints a per-item report and writes `state/.plan.json`. |
|
|
109
|
+
| `metabase-sync apply` | Re-derives the diff and executes it. Idempotent. |
|
|
110
|
+
| `metabase-sync apply --only cards` | Restrict to one resource (`collections,snippets,cards,dashboards,pulses`). |
|
|
111
|
+
| `metabase-sync index` | Debug: print remote resource counts. |
|
|
112
|
+
|
|
113
|
+
Both `plan` and `apply` re-fetch the live instance, so you can't apply a stale plan against a moved instance.
|
|
114
|
+
|
|
115
|
+
## Authoring new cards and dashboards in code
|
|
116
|
+
|
|
117
|
+
You don't need to know any Metabase ids ahead of time. Create the files; apply allocates the ids and writes them back.
|
|
118
|
+
|
|
119
|
+
### A new card
|
|
120
|
+
|
|
121
|
+
`state/collections/finance/cards/q4-revenue.sql`:
|
|
122
|
+
|
|
123
|
+
```sql
|
|
124
|
+
---
|
|
125
|
+
entity_id: null
|
|
126
|
+
name: Q4 Revenue
|
|
127
|
+
description: null
|
|
128
|
+
type: question
|
|
129
|
+
display: scalar
|
|
130
|
+
database: bigquery
|
|
131
|
+
parameters: []
|
|
132
|
+
visualization_settings: {}
|
|
133
|
+
enable_embedding: false
|
|
134
|
+
embedding_params: null
|
|
135
|
+
cache_ttl: null
|
|
136
|
+
archived: false
|
|
137
|
+
template_tags: {}
|
|
138
|
+
---
|
|
139
|
+
---body---
|
|
140
|
+
SELECT SUM(revenue) FROM finance.q4
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Run `plan` to see the CREATE line, then `apply`. The `entity_id: null` becomes the server-assigned nanoid in your local file after apply.
|
|
144
|
+
|
|
145
|
+
### A new dashboard referencing a new card
|
|
146
|
+
|
|
147
|
+
`state/collections/finance/dashboards/q4-review/dashboard.yaml`:
|
|
148
|
+
|
|
149
|
+
```yaml
|
|
150
|
+
entity_id: null
|
|
151
|
+
name: Q4 Review
|
|
152
|
+
description: null
|
|
153
|
+
archived: false
|
|
154
|
+
auto_apply_filters: true
|
|
155
|
+
cache_ttl: null
|
|
156
|
+
enable_embedding: false
|
|
157
|
+
embedding_params: null
|
|
158
|
+
position: null
|
|
159
|
+
width: fixed
|
|
160
|
+
parameters: []
|
|
161
|
+
tabs: []
|
|
162
|
+
dashcards:
|
|
163
|
+
- entity_id: null
|
|
164
|
+
card_path: ../../cards/q4-revenue.sql # path relative to this dashboard dir
|
|
165
|
+
tab_position: null
|
|
166
|
+
row: 0
|
|
167
|
+
col: 0
|
|
168
|
+
size_x: 12
|
|
169
|
+
size_y: 6
|
|
170
|
+
parameter_mappings: []
|
|
171
|
+
visualization_settings: {}
|
|
172
|
+
series: []
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`card_path` is the file path from this dashboard's directory to the card's `.sql` file. Apply resolves it after creating the card, then writes the new entity_id back to your file.
|
|
176
|
+
|
|
177
|
+
## On-disk layout
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
state/
|
|
181
|
+
databases/<name>.yaml # manifest only — name + engine, NO credentials
|
|
182
|
+
snippets/<slug>.sql # YAML frontmatter + raw SQL body
|
|
183
|
+
collections/
|
|
184
|
+
<slug>/_collection.yaml
|
|
185
|
+
/<nested-slug>/_collection.yaml
|
|
186
|
+
/cards/<slug>.sql or .yaml
|
|
187
|
+
/dashboards/<slug>/dashboard.yaml
|
|
188
|
+
/cards/<slug>.sql # dashboard-internal cards
|
|
189
|
+
root/cards/... # cards directly under the root collection
|
|
190
|
+
pulses/<slug>.yaml # dashboard subscriptions
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
- Native SQL cards: `.sql` file with YAML frontmatter + raw query body.
|
|
194
|
+
- GUI / MBQL cards: `.yaml` file with the full `dataset_query` (classic or MBQL5) inlined.
|
|
195
|
+
- Dashcards: reference cards by `card_path` (relative).
|
|
196
|
+
- Pulses: reference cards by `card_path`, the target dashboard by `dashboard_path`. Recipients are stored by email.
|
|
197
|
+
- Snippets without a collection live in `state/snippets/`; snippets inside a collection live in `state/collections/<...>/snippets/`.
|
|
198
|
+
- Personal collections are filtered out.
|
|
199
|
+
|
|
200
|
+
## Configuration
|
|
201
|
+
|
|
202
|
+
| Env var | Required | Default | Notes |
|
|
203
|
+
| --- | --- | --- | --- |
|
|
204
|
+
| `METABASE_URL` | yes | — | e.g. `https://metabase.example.com` |
|
|
205
|
+
| `METABASE_API_KEY` | yes | — | Admin API key (see below) |
|
|
206
|
+
| `STATE_DIR` | no | `state/` | Relative to CWD |
|
|
207
|
+
| `HTTP_TIMEOUT_S` | no | `120` | Per HTTP request (large cards' `result_metadata` recompute can take time) |
|
|
208
|
+
| `HTTP_MAX_RETRIES` | no | `3` | Retries on 408 / 429 / 502 / 503 / 504 / connection errors |
|
|
209
|
+
| `HTTP_RETRY_BACKOFF_S` | no | `1.0` | Base delay; doubles per retry (1s, 2s, 4s) |
|
|
210
|
+
|
|
211
|
+
A `.env` file in the current working directory is read automatically.
|
|
212
|
+
|
|
213
|
+
API keys are minted from the Metabase admin UI (Settings → Admin settings → Authentication → API keys). The key needs admin permissions to fetch + write everything `export` and `apply` touch.
|
|
214
|
+
|
|
215
|
+
## Exit codes
|
|
216
|
+
|
|
217
|
+
`plan` and `apply` follow the terraform convention so CI pipelines can fan out:
|
|
218
|
+
|
|
219
|
+
| Code | Meaning |
|
|
220
|
+
| --- | --- |
|
|
221
|
+
| 0 | Success and no changes (or apply finished cleanly) |
|
|
222
|
+
| 1 | Error (HTTP failure, preflight failure, missing recipient, concurrency drift) |
|
|
223
|
+
| 2 | `plan` detected pending changes (informational; not an error) |
|
|
224
|
+
|
|
225
|
+
## CI/CD recipes
|
|
226
|
+
|
|
227
|
+
See [`examples/`](examples/) for two GitHub Actions templates:
|
|
228
|
+
|
|
229
|
+
- [`github-actions-plan-on-pr.yml`](examples/github-actions-plan-on-pr.yml) — comment the plan output on every PR that touches `state/`.
|
|
230
|
+
- [`github-actions-apply-on-merge.yml`](examples/github-actions-apply-on-merge.yml) — apply automatically on merge to `main`, gated by an environment approval.
|
|
231
|
+
|
|
232
|
+
## Round-trip guarantees
|
|
233
|
+
|
|
234
|
+
- `export` is deterministic: byte-identical output across runs.
|
|
235
|
+
- `plan` against a freshly-exported tree reports `nothing to do.`.
|
|
236
|
+
- SQL bodies are byte-faithful — trailing whitespace and template-tag UUIDs survive the round-trip.
|
|
237
|
+
|
|
238
|
+
## Caveats
|
|
239
|
+
|
|
240
|
+
Before running this on a production Metabase, you should know:
|
|
241
|
+
|
|
242
|
+
- **Apply overwrites concurrent UI edits unless you re-plan first.** `apply` runs a fresh `plan` and checks the captured `updated_at` for every item it touched. If a UI user has edited an item since you ran `plan`, apply aborts with the list and tells you to re-plan. Pass `--force` to overwrite anyway.
|
|
243
|
+
- **Dashboard contents are a full replacement.** Tabs and dashcards are PUT in one go with client-assigned negative temp ids; the server replaces existing rows and allocates new `dashcard.entity_id`s. Two simultaneous applies race; wrap CI in a concurrency group to serialise them.
|
|
244
|
+
- **Out-of-scope resources are silently NOT synced.** Alerts, segments, legacy metrics, permissions, users and groups are not part of the state tree. `export` prints a warning if it finds any so you don't discover this in production.
|
|
245
|
+
- **Personal collections are excluded.** Cards/dashboards inside a user's personal collection don't appear in the export. Dashboards in shared collections that reference a personal-collection card will fail the reference preflight at plan time, not at apply time.
|
|
246
|
+
- **`--delete` is not yet implemented.** Items removed from `state/` are not auto-archived on apply. Archive them through the UI or directly via the API. Passing `--delete` is rejected with exit code 2.
|
|
247
|
+
|
|
248
|
+
### Pre-apply backup
|
|
249
|
+
|
|
250
|
+
`apply --backup-dir <path>` re-exports the live instance to `<path>` before mutating. If apply messes something up, `metabase-sync apply --state-dir <path>` rolls forward against the backup.
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
metabase-sync apply --backup-dir /tmp/pre-apply-$(date +%Y%m%d-%H%M%S)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Troubleshooting / FAQ
|
|
257
|
+
|
|
258
|
+
**`401 Unauthorized` on the first call.** Your API key was revoked or you're pointed at the wrong instance. Run `metabase-sync diagnose` — it prints the URL and key length.
|
|
259
|
+
|
|
260
|
+
**`plan` reports updates I didn't make.** Most often a Metabase version mismatch — server-generated `lib/uuid` values on GUI cards regenerate on every UI save. The tool strips them at diff time; if you still see noise, `metabase-sync export` once to refresh and try again.
|
|
261
|
+
|
|
262
|
+
**How do I rename a card?** Edit the `name:` in the frontmatter. The `entity_id` stays the same so the rename round-trips cleanly.
|
|
263
|
+
|
|
264
|
+
**How do I move a card to another collection?** Move the file. The collection that contains the card is determined by its on-disk path; on apply the collection_id is rebound and Metabase moves it.
|
|
265
|
+
|
|
266
|
+
**How do I delete a card?** Archive it via the Metabase UI for now. The `--delete` flag is not implemented in this release. After archival, re-export and commit.
|
|
267
|
+
|
|
268
|
+
**`metabase-sync diagnose` to file a bug.** It captures everything we'd ask for in a triage thread. Paste the output into the GitHub issue template.
|
|
269
|
+
|
|
270
|
+
**My Metabase is on a really old version.** Check the [compatibility band](#compatibility). Versions older than v0.45 are explicitly refused; v0.45–v0.55 will warn but try; v0.55–v0.62 are in our tested band; newer versions warn but proceed.
|
|
271
|
+
|
|
272
|
+
## Compatibility
|
|
273
|
+
|
|
274
|
+
| Metabase | Status |
|
|
275
|
+
| --- | --- |
|
|
276
|
+
| <v0.45 | **Refused** — collection API shape too different |
|
|
277
|
+
| v0.45–v0.55 | Warns; proceed at your own risk |
|
|
278
|
+
| v0.55–v0.62 | **Tested** in CI integration matrix |
|
|
279
|
+
| >v0.62 | Warns; we want to hear about issues |
|
|
280
|
+
| `latest` | Non-blocking CI job runs against it to surface upcoming breakage |
|
|
281
|
+
|
|
282
|
+
We pin `v0.62.2` for the blocking CI job; the matrix in `.github/workflows/ci.yml` runs the same integration suite against `v0.55`, `v0.58`, `v0.60`, and `latest` non-blockingly.
|
|
283
|
+
|
|
284
|
+
Python: 3.11, 3.12, 3.13.
|
|
285
|
+
|
|
286
|
+
If something goes wrong, you can `cp -r` the backup over `state/` and re-apply to roll back.
|
|
287
|
+
|
|
288
|
+
## Security
|
|
289
|
+
|
|
290
|
+
- The API key is read from the environment (or an `.env` file). Anything that captures the environment — CI logs, shell history (`printenv`), error stack traces from third-party deps that `repr()` settings — can leak it. Tools like [sops](https://github.com/getsops/sops) + `sops exec-env encrypted.env 'metabase-sync apply'` work well.
|
|
291
|
+
- HTTPS verification follows httpx's defaults (CA bundle from `certifi`). The tool does not disable cert verification.
|
|
292
|
+
- `state/.plan.json` and `state/.last-apply.json` include full SQL bodies — they're written under `state/` and should be `.gitignore`d. The default `.gitignore` snippet:
|
|
293
|
+
```
|
|
294
|
+
state/.plan.json
|
|
295
|
+
state/.last-apply.json
|
|
296
|
+
```
|
|
297
|
+
- The tool never serialises database connection credentials (`details` is stripped from `databases/<name>.yaml`).
|
|
298
|
+
|
|
299
|
+
## Limitations
|
|
300
|
+
|
|
301
|
+
- `--delete` (opt-in archival of items absent from disk) is not yet implemented.
|
|
302
|
+
- Permissions, users, and groups are out of scope.
|
|
303
|
+
- Alerts, legacy metrics, and segments are not yet supported.
|
|
304
|
+
|
|
305
|
+
## Contributing
|
|
306
|
+
|
|
307
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for dev setup, running tests, and the release process.
|
|
308
|
+
|
|
309
|
+
## License
|
|
310
|
+
|
|
311
|
+
MIT — see [LICENSE](LICENSE).
|
|
312
|
+
|
|
313
|
+
## Disclaimer
|
|
314
|
+
|
|
315
|
+
This is an unofficial, community-built tool. Not affiliated with or endorsed by Metabase, Inc. "Metabase" is a trademark of Metabase, Inc.
|