Compare commits

...

468 Commits

Author SHA1 Message Date
Chris Truter
2076d909d9 Run the tests please 2026-03-16 16:23:48 +02:00
Chris Truter
b2c2816d86 Switch branch of python-runner-container 2026-03-16 16:12:41 +02:00
Nicola Mometto
d8104c911d untgz: use resolveIn to prevent zip slip (#70921) 2026-03-16 12:49:46 +00:00
Nicola Mometto
5d5930ab64 Track OSS transform usage in Snowplow (#70925)
move snowplow transform metrics to OSS, as transforms are now available on OSS
2026-03-16 12:47:28 +00:00
github-automation-metabase
2738ec81b7 Reduce number of tests kept in memory for embedding SDK component tests (#70928)
* Add memory management settings to SDK component test Cypress config

Add experimentalMemoryManagement: true and numTestsKeptInMemory: 0 to
the embedding SDK component test config. The config spreads defaultConfig
which lacks these settings, causing Cypress to retain 50 DOM snapshots
per test (the default). This leads to Chrome renderer OOM crashes during
content-translations.cy.spec.tsx.

- experimentalMemoryManagement: true triggers gc() between tests
- numTestsKeptInMemory: 0 prevents DOM snapshot heap accumulation

* Use numTestsKeptInMemory: 1 to match main CI config convention

Change from 0 to 1 per review feedback. This matches the value used in
the main e2e config (config.js:190) and keeps the last snapshot available
for debugging while still preventing OOM from the default of 50.
2026-03-16 12:35:32 +00:00
Nicola Mometto
cd77eb5140 Skip running transform jobs if there are no transforms to run (#70926)
* fix: don't run transform jobs if there are no tranforms to run

* chore: add test
2026-03-16 12:34:46 +00:00
Kevin Kelleher
2a4046736a Fix pivot table drill through when binning a datetime by day (#70680) 2026-03-16 07:29:47 -04:00
Ngoc Khuat
660c1ddfe6 Fix flaky scoring-test/mine-test on H2 (#70852) 2026-03-16 17:48:53 +07:00
Ngoc Khuat
6845e78a5d Fix flaky schema-change integration test on Redshift (#70863)
Fix flaky python-transform-schema-change-integration-test on Redshift

Replace fixed Thread/sleep with polling for correct field metadata.
The transform pipeline sets run status to :succeeded before
complete-execution! (which runs the metadata sync) finishes. On
Redshift, the sync takes longer due to network latency, so the
2-second sleep was insufficient — the QP would query with stale
Field records (still referencing the old "age" column).
2026-03-16 17:48:27 +07:00
github-automation-metabase
eea9a750d6 Add 403 error handling to rerun-workflows.yml (#70916)
Handle 403 errors in rerun-workflows.yml

Wrap the reRunWorkflowFailedJobs API call in a try/catch that
gracefully handles HTTP 403 errors. This prevents false-positive CI
failures when the target run is cancelled, a PR run, or otherwise
ineligible for retry.

Also adds missing await to the API call so errors are properly caught.
2026-03-16 08:42:34 +01:00
metabase-bot[bot]
123a4f966d [chore] Update E2E test timings with recent data (#70913)
Update E2E test timings with averaged data

Collected timing data from 7 days of test runs.
All specs with timing data were included (simple averages).

🤖 Generated automatically by update-e2e-timings workflow

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: GitHub Actions <actions@github.com>
2026-03-16 07:02:38 +01:00
Denis Berezin
659d0a37b1 Add side-panel to Tables and Metrics in the Library (#69673)
* Temp

* Add full implementation

* Add e2e tests

* Remove excess image file

* Temp

* Address design issues

* Fix design review comments

* One more fix

* Fix e2e tests

* Remove empty source gotcha

* Address review issues

* Revert graphy changes
2026-03-14 02:22:45 +03:00
Eric Normand
59a52b2631 Validate FK target and dimension remap fields belong to the same database (#70879)
- Adds same-database validation for `fk_target_field_id` in `PUT /api/field/:id` (resolves existing TODO)
- Adds existence and same-database validation for `human_readable_field_id` in `POST /api/field/:id/dimension`
- Returns 400 if the target field belongs to a different database than the source field
2026-03-13 22:22:20 +00:00
bryan
a83196324b Fix MySQL 8.4 app-db test failures in dependencies tests (#70897) 2026-03-13 16:11:42 -06:00
Robert Kosara
9ab2bfbae8 Update HAR file creation instructions to include sensitive data (#70394)
Updated the documentation to clarify that the HAR file contains sensitive data and revised the instructions for saving the HAR file accordingly. Also added a screenshot of the settings in Chrome.

---------

Co-authored-by: Rodrigo López Dato <rodrigo@metabase.com>
2026-03-13 14:28:16 -07:00
Eric Normand
c261122999 Fix broken deps not surfaced for missing fields in :fields (GHY-3157) (#70865)
- The `:soft?` marking in `find-bad-refs-with-source` was too broad — it marked **all** bad refs in `:fields` clauses as soft, even when the field was completely missing from the source (not just inactive).
- Added `(not (:active column true))` guard so only **inactive** fields in `:fields` are marked `:soft?`. Missing/unresolvable fields remain hard errors and are properly surfaced by dependency analysis.
- This preserves the GHY-3044 fix (inactive fields the QP can drop are still soft) while fixing GHY-3157 (genuinely missing fields are reported).
2026-03-13 21:03:54 +00:00
Denis Berezin
0b65ccce3a UXW-3177 batch dashcard queries by 5 (#70542)
* UXW-3177 batch dashcard queries by 5

* Add unit tests

* Remove fetch limit for HTTP2+
2026-03-13 23:44:45 +03:00
Nathan Voxland
bb2adc5f46 Changed repro-bot label to ".Run Repro Bot" (#70890) 2026-03-13 15:37:09 -05:00
Ben Grabow
c5dcdc3238 Temporarily disable app db version checks (#70884)
* Temporarily disable app db version checks

* Comment out unused code

* Entirely rip out unused code
2026-03-13 12:47:04 -07:00
Aleksandr Lesnenko
5537f0f258 Add cypress claude setup (#70715)
e2e skills
2026-03-13 15:06:50 -04:00
Riley Thompson
bfdd8a0b23 Handle clickhouse dbname when it has spaces in it (#70822)
* take first name

* Add test

* fix test

* Add comment

* fix typos

Fix typo in comment regarding legacy `dbname` handling.
2026-03-13 18:47:35 +00:00
Riley Thompson
3c05a30b8e Enable old clickhouse tests (#70756)
* enable clickhouse tests

* enable more tests

* fix kondo and test
2026-03-13 18:22:20 +00:00
Alex Yarosh
106a707fcc docs: it's writable not writeable (#70877)
* it's writable

* no md in redirect dummy
2026-03-13 14:14:52 -04:00
Nathan Voxland
21c8136e7e Repro Bot Enabling (#70823) 2026-03-13 13:10:22 -05:00
John Swanson
7319599ceb Fix token check errors on startup (#70789)
During startup, the database isn't migrated yet, so we can't check the
token hash.

If the database hasn't been started yet, skip checking the token hash in
the DB and just delegate to the lower-level checker that actually hits
the store.
2026-03-13 08:46:20 -08:00
Nathan Voxland
13bc693357 Can't use regex filter with \b on non-ascii names (#70838) 2026-03-13 11:36:45 -05:00
Nick Fitzpatrick
2b11a3bed2 Update react-grid-layout and react-resizeable (#70760)
* Update react-grid-layout and react-resizeable

* adding e2e test

* e2e adjustment
2026-03-13 13:20:12 -03:00
Nick Fitzpatrick
c910ca3fd3 measure table names in tree in v1 data model page (#70657) 2026-03-13 12:23:45 -03:00
bryan
d8b40292d1 Disable blocking dependency checks on save (#70819) 2026-03-13 15:18:49 +00:00
Nick Fitzpatrick
b773f68f23 update html2canvas-pro (#70728)
* update html2canvas

* Update Loki Snapshots

---------

Co-authored-by: Metabase Automation <github-automation@metabase.com>
2026-03-13 12:13:20 -03:00
Edward Paget
647861b624 test: agent-specific test runner (#70821)
* test: agent-specific test runner

Adds an "agent" test runner that filters the output of running `clojure
-X:...:test` commands to make it more relevant for agents preventing
them from needing to use tail or grep to find information and reducing
context pollution.

Also implements randomized ports for test web-servers allowing multiple
agents to run tests in parallel (or to run multiple commands in
parallel). This removes the directive to use the ./bin/mage-based test
runner, to direct agents to use the agent-specific runner.

* fixup! missing import

* add the test agent script
2026-03-13 10:00:49 -05:00
lbrdnk
e741639713 Error source for native entities (#70021)
* Update schema with unknown error source

* Adjust validate-native-query

* Update tests

* Extend error source to multi table queries

* Update test

* Sql-tools providing more functions

* Card references

* Update tests

* require

* Unrelated fix

* repl reloading

* Tests

* Use placeholders instead of dummy subqueries

* Comment

* Placeholders

* Modules (temporary)

* potemkin

* Address circular deps extend sql-tools interface

* Sort modules

* Fix params

* Adjust test

* Placeholder

* More tests

* Cleanup

* whitespace

* Remove explicit unknown for now

* typo

* Update tests

* Break the circular dep more standard way

* Add comment

* whitespace
2026-03-13 16:00:06 +01:00
Anton Kostenko
5765b0df15 Add OpenTelemetry Tracing module (#69295)
* Add OpenTelemetry Tracing module

---------

Co-authored-by: dan sutton <dan@dpsutton.com>
Co-authored-by: Chris Truter <crisptrutski@users.noreply.github.com>
2026-03-13 10:48:13 -04:00
Nicola Mometto
40d00b8e7f add missing tags to enterprise settings (#70846)
fix: add missing tags to enterprise settings
2026-03-13 13:07:48 +00:00
github-automation-metabase
58cde65c4e Auto-update documentation for master (#70840)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-03-13 09:05:50 -04:00
Sébastien
5995621da8 Fixed color flickering for sdk question titles (#70842) 2026-03-13 13:52:48 +01:00
Sébastien
70d4a78914 Fixed issue when hiding SDK banner (#70727) 2026-03-13 13:52:30 +01:00
Fraser
cf6e1c032a Add module metrics (#70841) 2026-03-13 06:49:54 -06:00
Riley Thompson
e70f22e0eb Upgrade snowflake jdbc to 4.0.1 (#70505)
* upgrade snowflake jdbc to 4.0.1

* remove unnecessary exclusions

* replace internal parse

* fix eastwood

* address comments

* handle edge case
2026-03-13 08:43:15 -04:00
Riley Thompson
4fd189509f Ignore unknown settings in clickhouse (#70799)
* move additional options to clickhouse settings

* add test

* add test description

* add prefix and keep in subname

* just ignore unknown settings

* fix test
2026-03-13 12:39:37 +00:00
Ngoc Khuat
f7fd7fe5c5 Add workflow_dispatch action to reset cloud datasets (#70744) 2026-03-13 19:06:30 +07:00
Nicolò Pretto
a0e28bc775 fix: use the correct setting for enabling metabot in the embed wizard (#70820) 2026-03-13 09:54:13 +01:00
Aleksandr Lesnenko
a5b4f10c10 Metrics viewer fixes (#70655)
* fix conflicting filters on the same dimension

* og pr review feedback

* fix label
2026-03-12 22:54:34 +00:00
bryan
57502d5ef1 Fix cloud driver tests skipped when deps.edn changes [DEV-1672] (#70833) 2026-03-12 15:24:41 -07:00
bryan
b2f981e967 Fix flaky sync-materialized-views-test with auto-retry (#70750) 2026-03-12 15:50:48 -06:00
Fraser
fb8f72b7bd Setup boundaries linter (#70768) 2026-03-13 10:14:20 +13:00
Riley Thompson
502b66528c Wrap sqlserver boolean expressions in a case statement (#67136)
* wrap in case

* wrap case in cast, cleanup

* comments

* rename

* add test

* move comparison operators to lib.schema.filter

* Add boolean ops

* add and, or, not. rename comparison to predicate

* clean up tests
2026-03-12 17:05:01 -04:00
Jeff Bruemmer
2838b6501c docs - clickhouse server setting prefix (#70827)
clickhouse server setting prefix
2026-03-12 14:01:06 -07:00
Nemanja Glumac
7603550836 Revert "Replace circleci Docker images with the official DB images (#70773) (#70813)
This reverts commit 9ad40ab1c3.
2026-03-12 20:56:55 +01:00
Nick Fitzpatrick
ba8074bb87 New password generation for users (#70646)
* New password generation for users

* update to use window.crypto
2026-03-12 16:31:12 -03:00
John Swanson
90097715d2 dev env + start (mage -johnswanson-dev-env) (#70741)
Babashka task that starts up an isolated dev environment per worktree. Expands on @edpaget's excellent work, with interactive fzf prompts for edition, token, appdb, and warehouse services. Optional `--run` flag to launch backend + frontend in the background with PID tracking. Persistent config in `.dev-env-state.edn` for reuse across invocations.
2026-03-12 08:58:06 -08:00
Stas Gavrylov
12839276af GDGT-1893: Generate .d.css files to support CSS Variables autocomplete (#68723)
* CssVarsDeclarationPlugin: Add plugin and documentation to generate .d.css files at build step

* CssVarsDeclarationPlugin: Add recommended .vscode settings to support CSS vars autocomplete

* CssVarsDeclarationPlugin: Fix nested unions parsing

* CssVarsDeclarationPlugin: Add unit tests

* Update vscode settings for css vars to include Mantine
2026-03-12 18:24:54 +02:00
Nemanja Glumac
2ef536a0fe Cross-version follow up (#70800)
See:
https://github.com/metabase/metabase/pull/70063#discussion_r2924663893
2026-03-12 16:53:57 +01:00
Nemanja Glumac
9ad40ab1c3 Replace circleci Docker images with the official DB images (#70773) 2026-03-12 15:21:32 +01:00
Mahatthana (Kelvin) Nomsawadi
6351e5bc0e Fix SDK subscription not working under StrictMode (#70663)
* Fix can't click on the time dropdown on subscription sidebar in StrictMode

* Add tests

* update e2e test

* Fix unit tests

* improve code clarity

* Fix E2E tests

* Simplify code

* Make default value consistent

* Make the new Select button have dynamic size

* Fix unit tests

* Fix E2E tests
2026-03-12 14:09:28 +00:00
Nemanja Glumac
18d8dadf1b Cross-version testing infrastructure - Stage 1 (#70063)
Adds health-check-only cross-version migration tests using Docker Compose
with PostgreSQL. Tests verify that Metabase can upgrade/downgrade between
versions by checking the `/api/health` endpoint.

Usage:
```
./cross-version/test.sh --source v1.58.6 --target v1.58.7
```

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-12 14:36:47 +01:00
Chris Truter
d5f606e8f2 Upgrade toucan2 to 1.0.571 (#70771) 2026-03-12 14:00:09 +01:00
Alex Yarosh
acbf476d8b docs: maybe not deprecating (#70790)
Revise guidance on using Transforms over models

Updated recommendation for using Transforms instead of models.
2026-03-12 08:56:37 -04:00
Nick Fitzpatrick
21af2827f2 homepage redirect in useLayoutEffect (#70658) 2026-03-12 09:22:45 -03:00
Mahatthana (Kelvin) Nomsawadi
04b141b975 Fix flakes because we used JS instead of CSS (#70718) 2026-03-12 12:09:36 +00:00
Sébastien
ff9d9acdac Embedding upsells now open data studio modal in cloud (#70565) 2026-03-12 12:47:49 +01:00
Nemanja Glumac
d7848ee998 Bump eastwood timeout to 30 minutes (#70770) 2026-03-12 11:31:06 +01:00
Chris Truter
47c037a3f8 Use lib for MBQL construction in Workspace tests (#70532) 2026-03-12 09:38:01 +00:00
Chris Truter
ab1ccbf8ec Forward port migration moves from 58 -> 59 (#70765) 2026-03-12 09:30:07 +00:00
Chris Truter
8662aaaa87 Re-enable schema that was giving trouble (#70708) 2026-03-12 08:27:07 +00:00
Chris Truter
81b10ad959 Delete changesets orphaned by move from 59 -> 60 (#70719) 2026-03-12 08:25:10 +00:00
bryan
0a67d24d79 Fix dataset-already-loaded?-test flake on Redshift (#70748) 2026-03-11 16:20:36 -06:00
Romeo Van Snick
258305b914 Fix flakes (#70296)
* Fix flake

* Wait for unreferenced entities

* Wait for unreferenced entities

* Fix helper

* Fix test
2026-03-12 08:58:46 +11:00
Ben Grabow
21b67b2892 Show read-only mode banner to all users (#70529) 2026-03-11 16:22:01 -05:00
Nathan Voxland
3dcbfef5c5 Support MB_MONITOR_PERFORMANCE to write as files to a directory (#69582) 2026-03-11 15:41:56 -05:00
Ben Grabow
1ccf800dee Update minimum App DB versions (#70446)
* Upgrade mariadb images to 10.4.0

* Update job names to match the new version

* Advance minimum mariadb version to 10.4.6 to exclude unstable early versions of 10.4

* Don't specify patch version

* Upgrade images to MariaDB 10.6

* Upgrade CI actions to use pg14

* WIP: start updating MySQL 8.0 -> 8.4

* Advance minimum required app db versions

* Revert "Upgrade CI actions to use pg14"

This reverts commit 82f1aec823.

* Revert "WIP: start updating MySQL 8.0 -> 8.4"

This reverts commit c1c78d4298.

* Revert "Upgrade images to MariaDB 10.6"

This reverts commit f99d3c4f04.

* Revert just about all the changes to run CI

* Use newer MariaDB in semantic search tests

* Use newer mysql in semantic search tests

* Use newer postgres in semantic search tests

* Update app db version

* Update postgres version

* Trigger mysql driver tests

* Update unit tests to expect the new required versions

* Update mariadb and mysql used in drivers tests

* Fix expected supported mysql versions

* Remove dirt

* DRY the tests

* Dirt to trigger some CI tests

* Dirt to trigger some CI tests

* Remove dirt and update failing CI job

* Remove dirt

* Update another missed CI definition
2026-03-11 13:58:35 -05:00
da
d4d83d2f8e Upgrade Jetty to 12.1.7 (#70731)
CVE fix
2026-03-11 18:29:58 +00:00
Riley Thompson
3e655a0bb2 Get default port from nested connection properties (#70713)
* search for port in group fields

* add test

* update test

* simplify test

* fix sqlite
2026-03-11 18:06:17 +00:00
github-automation-metabase
0409c4a20b Add retry + checksum to Clojure install, remove continue-on-error (#70700)
* Add retry + checksum to Clojure install, remove continue-on-error

Remove continue-on-error: true from the Install Clojure CLI step so failures
surface immediately instead of silently propagating to downstream steps.

Add retry logic (3 attempts with curl --retry and --fail) and a post-install
clojure --version gate to handle transient download failures from
download.clojure.org.

* Add explicit error message when all Clojure install retries fail
2026-03-11 17:14:53 +00:00
Nicolò Pretto
b51c0c9efa fix typecheck broken on parameters.unit.spec.ts (#70725) 2026-03-11 15:53:12 +00:00
Nicola Mometto
51f8fb9af1 fix expected status of bigquery tests after #62351 (#70710) 2026-03-11 15:20:44 +00:00
metamben
bfb0b42b42 Get rid of category and id parameter types (#70196)
* Get rid of `category` and `id` parameter types

Closes QUE2-326
2026-03-11 17:40:42 +03:00
Chris Truter
09544d950d Log the migrations blocking the downgrade (#70701) 2026-03-11 13:19:18 +00:00
github-automation-metabase
03dd34ec4a Auto-update documentation for master (#70689)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-03-11 08:51:41 -04:00
Kamil Mielnik
d08e561420 GDGT-1793 Visualization Typescript Conversions - lib/ (#70249)
* Convert frontend/src/metabase/visualizations/lib/timeseries.js to TypeScript
* Convert frontend/src/metabase/visualizations/lib/timeseries.unit.spec.js to TypeScript
* Convert frontend/src/metabase/visualizations/lib/errors.js to TypeScript
* Convert frontend/src/metabase/visualizations/lib/mapping.js to TypeScript
* Convert frontend/src/metabase/visualizations/lib/warnings.js to TypeScript
* Convert frontend/src/metabase/visualizations/lib/trends.js to TypeScript
* Convert frontend/src/metabase/visualizations/lib/numeric.js to TypeScript
* Convert frontend/src/metabase/visualizations/lib/table.js to TypeScript
* Convert frontend/src/metabase/visualizations/lib/table.unit.spec.js to TypeScript
* Convert frontend/src/metabase/visualizations/lib/action.js to TypeScript
* Convert frontend/src/metabase/visualizations/lib/renderer_utils.js to TypeScript
* Convert frontend/src/metabase/visualizations/lib/renderer_utils.unit.spec.js to TypeScript
* Convert frontend/src/metabase/visualizations/lib/table_format.js to TypeScript
* Convert frontend/src/metabase/visualizations/lib/table_format.unit.spec.js to TypeScript
* Convert frontend/src/metabase/visualizations/lib/utils.js to TypeScript
2026-03-11 12:00:12 +00:00
mendral-app[bot]
214c38b3f2 Add retry loop to fetch-artifact action's download step (#70513)
Add retry loop to fetch-artifact download step

Wrap the downloadArtifact API call with a retry loop (3 attempts,
exponential backoff at 15s/30s) to handle transient GitHub API 500
errors that have been causing e2e and health-check job failures.

Co-authored-by: mendral-app[bot] <233154221+mendral-app[bot]@users.noreply.github.com>
2026-03-11 11:36:20 +01:00
mendral-app[bot]
52ae8a14db Fix prompt shell expansion in codespell workflow (#70290)
Fix codespell workflow: use prompt_file instead of shell expansion in YAML

The prompt field contained $(cat codespell_output.txt) which YAML passes
as a literal string — shell expansion does not occur in YAML values.
This caused Claude Code to receive the literal text instead of the actual
codespell output, resulting in exit code 1 on every nightly run.

Fix by writing the prompt to a file in a dedicated build step, then
passing it via the prompt_file input which claude-code-action supports.

Co-authored-by: mendral-app[bot] <233154221+mendral-app[bot]@users.noreply.github.com>
2026-03-11 11:23:16 +01:00
Chris Truter
e153a3290b Rename migrations that didn't make it to v59 (#70673) 2026-03-11 09:54:35 +00:00
Chris Truter
2830b3c94a Agent API for Workspaces (#70496) 2026-03-11 09:43:37 +00:00
Nicolò Pretto
2fa8abc247 Fix nested button DOM nesting in StepperWithCards (#70667) 2026-03-11 10:19:07 +01:00
Nemanja Glumac
be2a29d138 Remove devex from clj-kondo code owners (#70693) 2026-03-11 10:18:43 +01:00
Alexander Polyankin
ab089eb84d Fix CSS wrapping in replace sources (#70688) 2026-03-11 03:09:58 +00:00
Fraser
b40d76f5b4 Extract shared upsell components into common (#70619)
* Extract shared upsell components into common

 Moved to common/components/upsells/:
  - components/ subdir — the building blocks: UpsellBanner, UpsellBigCard, UpsellCta, UpsellGem, UpsellPill, UpsellWrapper, UpgradeModal, analytics, use-upsell-link
  - constants.ts — shared URLs and constants
  - 4 composed upsell pages used outside admin: UpsellEmbedHomepage, UpsellMetabaseBanner, UpsellStorage, UpsellUsageAnalytics
  - UpsellCard (merged in from its old location at common/components/UpsellCard/)

  Kept in admin/upsells/:
  - 13 admin-only upsell pages (UpsellBetterSupport, UpsellCacheConfig, UpsellCloud, UpsellDevInstances, UpsellEmailWhitelabel, UpsellHosting, UpsellPerformanceTools, UpsellPermissions, UpsellSSO, UpsellSdkLink,
  UpsellSemanticSearch, UpsellUploads, UpsellWhitelabel)
  - These now import primitives from metabase/common/components/upsells/ instead of ./components

  Updated ~30 import paths across the codebase. Reduced shared/common → feature/admin violations from 12 to 2.
2026-03-11 11:41:28 +13:00
Nathan Voxland
b12dfa4fd9 Per-PR liquibase changelogs (#70263) 2026-03-10 16:58:26 -05:00
John Swanson
5e2e510811 Handle Data Analyst group name conflict (#70594)
There are 30 instances on stats with groups named "Data Analysts". We
need to handle this case.

First, we modify the migration:

- set `validCheckSum` to `ANY` to handle the case where this migration
already ran

- skip if there's already a `data-analyst` magic group

- insert a group named "Data Analysts", if one doesn't already exist

- otherwise name it "Metabase Data Analysts" (there are 0 groups on
with this name in cloud, theoretically it's possible but someone would
have to have BOTH `Metabase Data Analysts` and `Data Analysts` for this
to be an issue).

Similarly, we create a "Data Analysts" group on startup in OSS if one
doesn't already exist. This group is an invisible placeholder, that will
become visible if the instance ever upgrades. Adopt similar logic here:
if the existing group for data analysts is called "Metabase Data
Analysts", rename it to "Metabase Data Analysts (converted)" and create
the new placeholder group as "Metabase Data Analysts".
2026-03-10 14:02:47 -07:00
Edward Paget
c6a2880b31 Add mage dev-env task for isolated worktree dev environments (#70602)
* Add worktree dev environment tooling

Add `mage dev-env` task for isolated worktree dev environments with
Docker containers and deterministic per-worktree ports. Includes workmux
layout with nREPL dev server, frontend build, and nvim panes. Adds a
custom Claude Code Docker image with Clojure tooling via mise, and
a bin/claude-dangerous helper script.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Move dev-env mage task to personal -edpaget-dev-env task

Move the worktree dev environment tooling from the shared mage task
namespace (mage.dev-env) to a personal task namespace (mage.edpaget.dev-env),
remove the sample .workmux.example.yaml, and update .workmux.yaml hooks
to use the new command name.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 16:01:59 -05:00
Jeff Bruemmer
5cd8e165fa docs - notes on timeouts (#70468)
notes on timeouts
2026-03-10 16:52:19 -04:00
Alexander Polyankin
05c26a4363 Clean up CODEOWNERS (#70666) 2026-03-10 14:32:41 -04:00
William
8ceeb1fce4 source replacement (#69257)
* fixes

* test parser changes

* schema test

* fix linter

* implicit join error

* fe tests

* fix linter

* test fixes

* more tests

* tests

* upgrade field refs for dashboardcard_series and dashboards in transitive deps

- Add dashboard-upgrade-field-refs! for Phase 1b: upgrades parameter_mappings
  on dashboards found in transitive deps (cards are upgraded first in Phase 1)
- Fix dashboard-card-update-field-refs! to find dashcards via
  DashboardCardSeries, not just by primary card_id
- Split dashboard-ids out of transitive in runner, pass separately to run-swap*
- Add tests for series card parameter mapping swap and dedup

* fixup some tests, add promise to progress instance

useful for test so we don't have to loop on the db row

```clojure
     (reify replacement.protocols/IRunnerProgress
       (set-total! [_ total] ...)
       (advance! [this] ...)
       (advance! [this n]
         (let [c    (swap! completed* + n)
               t    @total*
               prev (- c n)
               crossed-boundary? (or (= c t)
                                     (not= (quot prev progress-batch-size)
                                           (quot c progress-batch-size)))]
           (when crossed-boundary?
             (let [progress (if (pos? t) (double (/ c t)) 0.0)]
               (update-progress! run-id progress)
               (when (replacement.protocols/canceled? this)
                 (log/infof "Replacement run %d was canceled" run-id)
                 (when on-complete
                   (deliver on-complete :run/cancelled))  ;; <-----------
                 (throw (ex-info "Run canceled" {:run-id run-id})))))))
       (canceled? [_] ...)
       (start-run! [_] ...)
       (succeed-run! [_]
         (log/infof "Replacement run %d succeeded." run-id)
         (succeed-run! run-id)
         (when on-complete
           (deliver on-complete :run/success)))  ;; <--------
       (fail-run! [_ throwable]
         (log/errorf throwable "Replacement run %d failed" run-id)
         (fail-run! run-id (ex-message throwable))
         (when on-complete
           (deliver on-complete :run/fail))))     :<-----
```

and in tests we can do like so:

```clojure
(deftest advance!-progress-write-boundaries-test
  (testing "advance! writes progress at batch boundaries and on final item"
    (mt/with-premium-features #{:dependencies}
      (mt/with-model-cleanup [:model/ReplacementRun]
        (let [writes (atom [])
              done?  (promise)]
          (with-redefs [replacement-run/update-progress!
                        (fn [_run-id progress]
                          (swap! writes conj progress))]
            (let [record (replacement-run/create-run! :card 1 :card 2 (mt/user->id :rasta))
                  progress (replacement-run/run-row->progress record done?)]
              (execute/execute-async!
               (fn [progress]
                 (replacement.protocols/set-total! progress 120)
                 ;; advance one-at-a-time for 120 items
                 (dotimes [_ 120]
                   (replacement.protocols/advance! progress)))
               progress)
              ;; wait for virtual thread
              (is (= :run/success (u/deref-with-timeout done? 500)))  ;; <------------
              ;; Writes at items 50, 100, 120 (final) → progress 50/120, 100/120, 120/120
              (is (= [(double (/ 50 120)) (double (/ 100 120)) 1.0]
                     @writes))
              (is (=? {:is_active nil :progress 1.0 :status :succeeded}
                      (t2/select-one :model/ReplacementRun :id (:id record)))))))))))
```

* fixes

* add new flag to resolution

* fix

* fix sandboxing

* fix

* wip be

* wip  be

* wip be

* wip

* wip be

* wip be

* missing join-alias

* wip be

* more field ref cases

* wip be

* wip be

* wip be

* remove healing

* implicit join second level

* swap refactoring

* both loops process all

[GHY-3182: swap all dependencies](https://linear.app/metabase/issue/GHY-3182/swap-all-dependencies)

* field refs

* fix types

* fix ref types

* short curcuit when swapping

* base-type test

* fix order by

* test

* Viz settings in cards

* handle field->expression conversion

* base-type fix

* unique fix

* deduplicate test

* only upgrade/swap when well-formed query

`(-> card :dataset_query :stages seq)`

degenerate cases are `{}` and `{:lib/type :lib/query}`

* remove tests with broken schema

* test that :field -> :expression works in parameters, too

* viz_settings!

* legacy->pMBQL

* preserve native queries metadata

* fix typo in a comment

* Tests for field_refs.clj

* swap function order

* remove transitive card ids, add usages tests

we don't need to keep a set of all card ids. it's huge, and we're gonna
upgrade cards either way. So no need to not upgrade card references

* Fix upgrade-field-ref-to-name crash on :aggregation refs

Viz-settings locations (pivot_table.column_split etc.) can contain
[:aggregation 0] refs, not just [:field ...]. Wrap ->pMBQL in
with-aggregation-list so it can resolve legacy index-based aggregation
refs to UUID-based pMBQL. Add try/catch as safety net for other
unexpected ref types.

Fixes 13 cards failing with "no aggregation at index 0" error.

* fix expressions

* add a test

* fix the test

* add a proper test

* Skip non-field refs in upgrade-field-ref-to-name

Per Alexander's feedback: since we only resolve :field refs downstream,
skip :aggregation/:expression refs at the gate instead of setting up
aggregation context for ->pMBQL.

* remove log call in source-swap

* add schemas

* add schemas

* repro test

* guard against missing tables

* more fixes

* wip tests

* add tests for resolution with missing metadata

* Mutation testing improvements

* Some more tests

* revertme: adds comilation tracker for statsdb swap+upgrade analysis

* add checks for wrong queries

* fixes

* test fixes

* fix

* remove old code

* temp test fixes

* fix viz

* fixes

* One more tests.

* Singular mutation testing run

* tests

* fix tests

* remove not needed column checks

* fix tests

* run-swap throws with old-source new-source and check-result

* fix dependencies check test

```clojure
source-swap-test=> (clojure.test/run-test swap-native-card-source!-updates-dependencies-test)

Testing metabase-enterprise.replacement.source-swap-test

Ran 1 tests containing 1 assertions.
0 failures, 0 errors.
{:test 1, :pass 1, :fail 0, :error 0, :type :summary}
```

So an interesting bit:
we must update dependencies regardless. It's important that the dep
graph is updated. Imagine if Card A is swapped for Card B. All cards
that use Card A will be listed as deps of Card A even if they don't
reference it and don't require changes. We are still swapping the source
so we need to update the graph.

We don't have a real api for dependencies. So to ensure the cards are in
the graph we call `(events/publish-event! :event/card-create {:object
card :user-id (mt/user->id :rasta)})` and when swapping we prefer to use
`(models.dependency/swap-dependency! :measure entity-id old-source
new-source)`. But for dashboards we'll just do the full recompute
accomplished with

```clojure
(events/publish-event! :event/dashboard-update
                       {:object (t2/select-one :model/Dashboard :id dashboard-id)})
```

* working field-refs tests

* Fix logic

* Strong logic (but is it possible?)

* fix ::entity-id schema to include all entity types

* refactor

* fixes

* fixes

* fixes

* fixes

* fixes

* wip

* fix

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* wip be

* wip be

* wip be

* remove unused

* wip be

* remove invalid queries

* wip fixes

* wip walk function

* Measure and segment tests

* fixes

* aliases

* fixes

* fix tests

* make source_swap accept the preloaded entity

* renaming

* unify sources

* database id fixes

* fix table id check

* deps fixes

* reflection warning

* fixes

* fixes

* fixes

* fix team

* fix CI

* check for admin

* disable plugin

* Disable api endpoints in prod

* wip tests

* fire events; namespace doc

```clojure
clj -X:dev:test:ee:ee-dev :only metabase-enterprise.replacement.source-swap-test
Ran 29 tests in 29.144 seconds
219 assertions, 0 failures, 0 errors.
{:test 29, :pass 219, :fail 0, :error 0, :type :summary, :duration 29144.393083, :single-threaded 29}
Ran 0 tests in parallel, 29 single-threaded.
Finding and running tests took 40.1 s.
```

All tests passing

Fire event only when things change, swap sources before. That's faster
and must happen regardless, change event only on change.

Namespace docstring indicating why

* fix constraint error if dependency already exists:

```
custom-swap=> (source-swap/swap-source! [:card 27452] [:card 6759] [:table 52728])
Execution error (PSQLException) at org.postgresql.core.v3.QueryExecutorImpl/receiveErrorResponse (QueryExecutorImpl.java:2736).
ERROR: duplicate key value violates unique constraint "idx_unique_dependency"
  Detail: Key (from_entity_type, from_entity_id, to_entity_type, to_entity_id)=(card, 27452, table, 52728) already exists.
```

it seems to have already have a dependency. So now we just remove
dependency if the one we are adding already exists, otherwise we swap.

swap card A: dep on B -> dep on C
if it already depends on C, just delete the dep on B
otherwise swap where card A dep B -> dep C

* simplify check

* upgrade parameter value_source_config

* field refs

* tests

* viz tests

* parameters

* tests

* fixes

* tests

* check sources fixes

* fix tests

* tests

* wip tests

* fixes

* wip tests

* wip tests

* wip tests

* wip cleanup

* cleanup

* move test

* lint

* fix

* fix

* fixes

* fixes

* fixes

* fix

* fixes

* fix

* fix

* fixes

* fix

* FE cache fixes

* fix checks

* fix

* polish: remove tracing code, update docstring, add :pending to ::run-status enum

* Add test for table->card in check-column-mappings

* Remove unneeded argument.

* Remove unused function.

* Test that all api endpoints are gated behind dependencies feature flag

* use api/*current-user-id* instead of (:id @api/*current-user*) in source swap

Avoids unnecessarily forcing the *current-user* delay (which loads the
full User record from the DB) when only the integer ID is needed.

* stop swapping card_id in parameters

* throw on partial swap failures so the run is marked as failed

Previously run-swap* returned {:failures [...]} which execute-async!
ignored, marking the run as succeeded. Now it throws with a descriptive
message (count + first 10 failures) that gets persisted in the run's
message column.

* Check that all routes are protected behind superuser

* Add test for runner.run-swap

* FE checks

* Remove source-error schema (unused)

* Reuse lib-be.schema.source-swap/column-error schema instead of remaking it

* Kondo error

* move code to lib-be

* Better mutation testing

* fixes

* More tests for field-refs

* Better test

* More source swap tests.

* suppress dependency event handlers during source replacement

Source replacement manages dependencies itself via swap-dependency!, so
the event-driven dependency recalculation, stale-marking, and metadata
propagation triggered by entity update events is redundant work.

Adds *suppress-dependency-events?* dynamic var, checked by all dependency
event handlers. Bound to true in run-swap* phase 2.

* Fewer deps! Yay!

* Revert "suppress dependency event handlers during source replacement"

This reverts commit 03625e9c26.

* Clearer code

* update 2 docstrings

* publish dashboard-update event only when a dashcard changed

* simplify implementation

* simplify

* bulk mbql replacement

* Revert "bulk mbql replacement"

This reverts commit cf12636c42.

* simplify

* fix reseting selection when the graph changes

* fix

* add source_swap module

* fix lint

* fix comment

* fix

* fixes

* fix module order

* fix module order

* fix order

---------

Co-authored-by: Alexander Polyankin <alexander.polyankin@metabase.com>
Co-authored-by: Bryan Maass <bryan.maass@gmail.com>
Co-authored-by: dan sutton <dan@dpsutton.com>
Co-authored-by: Eric Normand <ericwnormand@gmail.com>
2026-03-10 14:25:01 -04:00
Fraser
49a3037d2a Move range-for-value into value (#70622) 2026-03-11 07:14:27 +13:00
Fraser
fa959aaab4 Remove deprecated getCurrentUser selector (#70558) 2026-03-11 07:13:06 +13:00
Kevin Kelleher
eadbd4b00d Fix broken pie chart colors for some strings in static-viz (#70640) 2026-03-10 13:53:45 -04:00
bryan
1da8e5751a Run Mage Tests on all PRs and fix module count (#70656) 2026-03-10 16:22:11 +00:00
Alex Yarosh
d47651a2cf docs: clarify transform perms (#70650)
clarify perms

Clarified the permissions required for viewing and executing transforms in Metabase.
2026-03-10 11:31:50 -04:00
Sébastien
92001b00de Fixed extraneous item in filter popover in embeds (#70634) 2026-03-10 16:17:24 +01:00
Sébastien
186c9dca65 Fix missing translation in guest embed filters (#70583) 2026-03-10 16:15:29 +01:00
Edward Paget
42aebbf137 Add configurable CSV field separator setting (#69827)
* Add configurable CSV field separator setting

- Adds csv-field-separator setting for configuring CSV export delimiter
- Supports comma (default), semicolon, tab, and other delimiters
- Useful for European locales where semicolon is standard
- Configurable via MB_CSV_FIELD_SEPARATOR environment variable
- Includes tests for different separator types and quoting behavior

Fixes #10256

* Split CSV separator tests into smaller deftests and clean up comments

Break the monolithic csv-field-separator-test into four focused tests:
- csv-field-separator-default-test
- csv-field-separator-semicolon-test
- csv-field-separator-tab-test
- csv-separator-quoting-test (already separate, just removed extra nesting)

Remove the "Changed from hardcoded" comment in csv.clj and the unused
metabase.test.util require.

* fixup! eastwood and serialization baseline

* fixup! whitespace

---------

Co-authored-by: kevin raymond <kevray39@gmail.com>
2026-03-10 09:55:03 -05:00
Eric Normand
df7d834e2e [Mutation Testing] Kill mutations in metabase.lib.field/find-visible-column-for-ref (#69320)
### Stats
- **Surviving mutations before:** 6
- **Tests added:** 2
- **Mutations killed by this PR:** 4
- **Mutations remaining:** 2

### Mutations killed
- Replace -1 with 0
- Replace field-ref with nil
- Replace :aggregation with :aggregation__
- Replace lib.metadata.calculation/returned-columns with nil

### Mutations not killed (with rationale)
- Replace some? with nil — Schema-only mutation — changing the Malli schema annotation does not affect runtime behavior
- Replace query with nil (in find-matching-column) — lib.equality/find-matching-column does not use the query arg for field ref matching in these test cases
2026-03-10 14:48:44 +00:00
Maksym Yakubych
bc7ffe094f feat: fix this query feature not using inline sql edits (#70172) 2026-03-10 11:06:04 -03:00
Ryan Laurie
205205174c Bump backport reminder threshhold to 25 hours (#70636)
bump backport reminder threshhold
2026-03-10 13:16:45 +00:00
John Swanson
05567456f2 Use virtualized table for data perms (#70343) 2026-03-10 09:01:10 -04:00
Kevin Kelleher
9bfb9ed7a4 Use lowest cardinality dimension as default for boxplot (#70569) 2026-03-10 08:49:24 -04:00
Kevin Kelleher
b161e19442 Fix handling of object columns in pie charts (#68095) 2026-03-10 07:22:13 -04:00
Uladzimir Havenchyk
c28beea344 chore: convert ObjectDetail viz to typescript (#70425)
* chore: convert ObjectDetail viz to typescript

* add column_title to viz definition type
2026-03-10 14:00:07 +03:00
Mahatthana (Kelvin) Nomsawadi
b22ab2010d Improve modular embedding analytics (#70449)
* Add v3 for modular embedding analytics

* Add implementation

* Improve test readability

* Improve code format

* Make the code autocomplete-able to prevent human-error

* Add missing default value tests

* Format

* Ensure we assert all properties

* fix "id_new" and "id_new_native" tracking

* Update the implementation so it count 0 like before

* Fix logic

* Fix snowplow schema lint

* Remove too much linear ticket number references
2026-03-10 10:54:58 +00:00
Jakub Chodorowicz
6f4d71c279 Gdgt 1801 Leaflet components (#70299)
* make LeafletMap generic component

* convert leaflet grid heatmap

* use filter guard to make sure we're dealing with L.Rectangle

* remove  param which doesn't exist

* add guard helper for data set column

* create LeafletMapPoint variadic generic type

# Conflicts:
#	frontend/src/metabase/visualizations/components/LeafletMap.tsx

* extact sync map from props methods

* migrate LeafletMarkerPinMap to TS

* don't force assert existence of pin dependencies

* use 3 tuple point everywhere in LeafletMarkerPinMap

* use ClickObject type

* migrate LeafletTilePinMap to TS

* convert LeafletChoropleth to TS

* remove type assertions in LeafletMarkerPinMap

* remove TileLayerWithInternals casting

* improve tile layer typings

* add casting comment

* improve embedding context types

* improve LeafletGridHeatMap types

* don't hardcode default scale for d3 scale linear

* call zoom control also when prevProps are missing

* improve HoveredObject, use HoveredObject in LeafletGridHeatMap

* remove unneeded nullish coalescence

* simplify getTileUrl by using just parameters as arguments

* remove unneeded assertion

* remove unneeded this param
2026-03-10 10:04:38 +01:00
Nicolò Pretto
c8d5e94ffa test(eajs): handleLink should prevent default behavior when returning {handled:true} (#70592)
test(eajs): handleLink should  prevent default behavior when returning {handled:true}
2026-03-10 09:43:31 +01:00
Ngoc Khuat
22a1f275f0 Fix computed transform tables being editable (#70441) 2026-03-10 14:54:23 +07:00
mendral-app[bot]
ce8660f161 Use env vars for embedding workflow (#70614)
fix: prevent script injection via PR title in embedding-sdk workflow

Move user-controlled PR context values (title, number, URL) and secrets
from direct GitHub Actions expression interpolation into env vars.
This prevents a crafted PR title from breaking out of the shell string
and executing arbitrary commands on the runner.

Co-authored-by: mendral-app[bot] <233154221+mendral-app[bot]@users.noreply.github.com>
2026-03-10 06:51:16 +04:00
Denis Berezin
dcf260f4c9 Fix broken e2e test for #68998 (#70585) 2026-03-10 03:24:04 +03:00
Aleksandr Lesnenko
4897452a08 Metrics Explorer (#68961)
Co-authored-by: Edward Paget <ed.paget@gmail.com>
Co-authored-by: Alexander Polyankin <alexander.polyankin@metabase.com>
Co-authored-by: John Swanson <john.swanson@metabase.com>
Co-authored-by: Nick Fitzpatrick <nick@metabase.com>
Co-authored-by: Edward Paget <edpaget@users.noreply.github.com>
Co-authored-by: Kevin Kelleher <kevin.kelleher@metabase.com>
Co-authored-by: Maz Ameli <maz@metabase.com>
2026-03-09 20:18:15 -04:00
dpsutton
c87ad18e21 quarantined driver notifications (#70612)
* quarantined driver notifications

* temp on push to bot-testing channel

* prettier

* more compact

* add back a bit of emphasis

* remove push edge and restore channel
2026-03-09 18:14:56 -05:00
Fraser
78a8c59d71 Move expandable string to where it's used (#70559) 2026-03-10 12:08:58 +13:00
Fraser
336c2a8b1a Convert OnClickOutsideWrapper to TypeScript (DEV-1496) (#69418)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 11:50:54 +13:00
Rodrigo López Dato
567bbd68bb Clarify that SSO provisioning also allows reactivating accounts (#67524)
* Clarify that SSO provisioning also allows reactivating accounts

* Update wording in docs

* ditto LDAP

* Use radio buttons instead of a toggle

* lint/wording

* lint

* Revert changes to autogenerated docs

* Fix eslint rule + improve wording

* Make OIDC provisioning setting consistent with other providers

* Add detailed descriptions only to env var docs, not UI

* Fix E2E test

* fix: update ESLint configuration file path from .eslintrc.js to eslint.config.mjs

* run lint when we change component tests specs

* fixing linting

* ignore linting issue in embedding test

---------

Co-authored-by: Nicolò Pretto <info@npretto.com>
Co-authored-by: Nick Fitzpatrick <nickfitz.582@gmail.com>
2026-03-09 18:09:00 -03:00
Braden Shepherdson
ba6895665f [MBQL lib] Ignore inherited-temporal-unit on bucketed columns (#70512)
Fixes QUE2-394.

This issue was caused by losing the thread of a column between stages in
the case where it has a different `:inherited-temporal-unit` than its
proper `:temporal-unit`.

`lib.equality/=` was checking the `:temporal-unit`s, which match, and
then returning `false` because only one column had
`:inherited-temporal-unit` for a different, finer unit. That shouldn't
fail the match, since the `:inherited-temporal-unit` is obsolete when
the column has just been bucketed differently.
2026-03-09 21:02:10 +00:00
Ben Grabow
09c04d91bb Remove minimally useful semantic search log messages (#70524) 2026-03-09 21:01:38 +00:00
Jeff Bruemmer
8977542307 docs - remote sync limitations (#70598)
* branch limitations

* edits

* link

* Apply suggestions from code review

Co-authored-by: Alex Yarosh <alexandra@metabase.com>

---------

Co-authored-by: Alex Yarosh <alexandra@metabase.com>
2026-03-09 20:51:35 +00:00
Ben Grabow
ec678a17fc Uxw 1070/fail early if unsupported appdb version is detected (#70402) 2026-03-09 15:33:16 -05:00
Chris Truter
6eda44fc53 Fix reflection warnings in tests + add linter (#70551) 2026-03-09 13:29:04 -06:00
metamben
4219500b7a Preserve expression names when fixing bad field refs (#70530)
Closes QUE2-397
2026-03-09 21:48:51 +03:00
Will Bryant
2f2ab17ebb Fix race condition in FormattingWidget initialization (#69777) (#69778)
Although there was an early return if isLoading is true, the useState had already run, and set the state to the DEFAULT_FORMATTING_SETTINGS. This would then not get replaced when isLoading became false.

Instead, use the same idiom as SessionTimeoutSetting - install a useEffect hook that depends on the setting value and applies that setting value to the local state.
2026-03-09 11:43:20 -07:00
Maksym Yakubych
89c1712267 fix: Metabot not receiving transform errors from transforms (#70462) 2026-03-09 15:54:48 +00:00
Nicolò Pretto
ba03f8d368 remove ThemeProvider from sdk component test (#70568) 2026-03-09 16:51:29 +01:00
Edward Paget
6d84d80c06 fix: allow dashboard caching in read-only mode (#70511)
* fix: allow dashboard caching in read-only mode

* fix: split out cache permissions from can-write?

We were denying changing caching policy for dashboards on read-only
remote-synced instances because we set can-write? to false for
dashboards when we are in read-only mode.

we also need to create a new boolean flag we can return to the frontend
to indicate that caching is still allowed even when editing a dashboard
is otherwise disabled.

* fixup! PR feedback
2026-03-09 10:37:16 -05:00
Denis Berezin
b130d0d812 Pivot Table shows a blank card when rendered on a dashboard (#70385) 2026-03-09 18:18:37 +03:00
Riley Thompson
caafceff99 Upgrade clickhouse to 0.9.7 with pinned dependencies (#70448)
* upgrade clickhouse jdbc to 0.9.7

* clean up todos

* update deps
2026-03-09 11:12:36 -04:00
Jakub Chodorowicz
5fe95dbbae fix self-heal openapi job and update openapi.json (#70499) 2026-03-09 15:42:44 +01:00
Nicolò Pretto
65d9029a55 fix: update ESLint configuration file path for eslint config (#70507)
* fix: update ESLint configuration file path from .eslintrc.js to eslint.config.mjs

* run lint when we change component tests specs
2026-03-09 13:32:17 +00:00
github-automation-metabase
e4ec44c4af Auto-update documentation for master (#70560)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-03-09 09:28:12 -04:00
Uladzimir Havenchyk
0e9fd4c48c add transform-basic feature flag for transforms (#70442)
* add another feature flag for transforms

* fix linting error from master

* add debug info

* enable-transforms-basic -> enable-basic-transforms

* remove logs
2026-03-09 15:08:51 +03:00
Phoomparin Mano
cd32f3b89c Integration branch for embedding onboarding improvements (#69456)
* Add "configure data permissions and enable tenants" skeleton for onboarding (#68895)

* use onboarding stepper

* add TODO for EMB-1266

* add e2e test for skeleton

* use gap=sm for steppers

* align with design

* Add completion checks for data segregation and tenants (#68890)

* add completion information for segregation and tenants

* add data segregation checks

* add data segregation config test

* add the combined data-permissions-and-enable-tenants step

* separate main and sub checklist

* explain the purely ui step

* add schema for checklist response

* update checklist completion states

* add test cases

* address review on test permutations and using is_active for tests

* remove stale configure data permissions unit test

* remove stale test on user strategy card

* Add enable tenants and create shared collection button (#69131)

Add enable tenants and create shared collection button (#69131)

* Add data segregation strategy selection step for embedding onboarding (#69127)

Add data segregation strategy selection step for embedding onboarding (#69127)

* Lock steps 2 to 5 in data permissions setup checklist (#69135)

Lock steps 2 to 5 in data permissions setup checklist (#69135)

* Add link to documentation for database routing in the onboarding step (#69138)

Add link to documentation for database routing in the onboarding step (#69138)

* Add tenant creation and tenant summary step for tenants onboarding (#69454)

Add tenant creation and tenant summary step for tenants onboarding (#69454)

use index as key instead

update failing unit test

refactor: use slugify instead

add assertion on configure data permissions step should be done

cleanup summary components

adjust font size

use related setting card for summary

refactor: cleanup card components

add e2e test verifying tenant errors are shown

add e2e test verifying tenants are created

fix: prevent going to previous incomplete step

fix: skip locked steps

fix: consider locked steps when auto-advancing

fix: failing e2e by enabling tenants via ui

refactor: cleanup tenants import

add fallback logic for tenants listing

refactor: move onboarding step to tenants plugin

scaffold tenant summary step

scaffold tenant creation step

use paper from mantine-ui instead

scaffold tenant creation step

* Onboarding stepper should navigate to next steps correctly even when all steps are done (#69653)

Onboarding stepper should navigate to next steps correctly even when all steps are done (#69653)

cleanup: remove redundant test

fix: cleanup broken tests

remove inapplicable test case

apply onboarding stepper fixes

* Add row and column level security step for onboarding (#69622)

fix failing e2e tests

wait for permission graphs to update before checking error toasts

add test assertion on hiding selected tables

hide tables that are selected by other cards

add unit test for building permissions graph

address review on type-safe sandbox policy

extract error and success toasts into UI consumer

add clarification on why we assert GTAP

extract MiniPicker hard-coded margins to the data picker call site

address review on using design system units and rem

address review on "All tenant users" permission update hook

Add row and column level security step for onboarding

revert all changes to OnboardingStepper

update the permission graph as well

refactor: renamings

lock step "Select data to make available" if strategy not chosen

cleanup: remove button

use metabase/ui for select style

refactor: extract table column card

simplify trigger styles

revert MiniPicker change

undo test name change

refactor: simplify stepper

update e2e test assertions

add step completion assertion to e2e tests

refactor: extract to step components

allow going to next step

fix onboarding stepper re-rendering

chore: cleanup sandboxing creation test

add e2e tests

add sandbox on clicking next

clicking next on rls selector should advance to next step

refactor: rename to selections and onSelectionsChange

display options on the bottom

position dropdown directly below the trigger

fix: trigger height

fix: do not hide title on RLS step

do not collapse current step when all steps are complete

make it clearer what is goToNextIncompleteStep

simplify logic for going to next incomplete step

add data selector for row and column security

* Create a multi-select database selector component (#69635)

Create a multi-select database selector component (#69635)

chore: remove fluff tests

allow disabling rightSection with null

add data fetching to DatabaseMultiSelect

use multi autocomplete

add storybook story and unit test for disabled options

allow disabling database options

update multi select tests

chore: formatting

cleanup dropdown style

show placeholder always

apply brand focus color

remove inline-end

limit combo box width

update styles to match figma design

update option icon

scaffold database multi select

* Add connection impersonation onboarding step (#69644)

Add connection impersonation onboarding step (#69644)

add permission graph and impersonation assertion

add e2e test

use useUpdateTenantGroupPermissions for permission updates

add connection impersonation creation hook

add edge case for database does not support impersonation

disable dbs without connection impersonation

scaffold ui for connection impersonation

* Add JWT authentication onboarding for embedding hub (#69823)

test: e2e fail due to lock icons found in related settings section

replace e2e test on jwt completion

update e2e tests

scaffold e2e tests

refactor: cleanup sso stepper component

add help url and complete step button

scaffold "Test that JWT authentication is working correctly" step

scaffold "Add a new endpoint to your app" step

add ui to configure identity provider

scaffold sso setup

* Mark get embed snippet step as done when guest embed is published (#69889)

* mark guest embedding onboarding test step as done

* add e2e test for guest embed

* simplify e2e test

* fix invalid imports due to merge conflicts

* fix guest embed snippet copy incorrectly marking production embed step as done

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Embed wizard should default to newly created dashboards (#69907)

Embed wizard should default to newly created dashboards (#69907)

scaffold defaulting to newly created dashboards

add a comment on why newly created dashboards are prioritized

chore: update inline comments

de-duplicate recent items at call site

refactor: cleanup implementation

add created_at time filter

address review feedback on variable naming

address review by going through actual x-ray flow

remove invalid unit test

update duplicated and failing tests

fix failing tests

fix failing select-embed-experience e2e test

fix failing select-embed-entity e2e test

* Remove create models step from embedding onboarding checklist (#70256)

remove create models step

* Show tenant_identifier autocompletion from column values in RLS (#69829)

Show tenant_identifier autocompletion from column values in RLS (#69829)

scaffold basic tenant_identifier autocompletion

make placeholder dynamic to tenant identifier input

fix: update broken types

document why we use ad-hoc query

refactor: small cleanup

cleanup docstring

chore: update hook tests

add e2e tests for autocompletion

explicitly pass tenant attributes

simplify CreateTenantInput

Suppress ad-hoc query errors in tenant identifier autocomplete

Autocomplete suggestions are best-effort during onboarding. Query
failures (permissions, timeout, etc.) should not surface global
error UI noise.



Make attributes optional in CreateTenantInput to match API contract

The backend marks attributes as optional; the frontend type was
overly strict and forced callers to always send attributes: {}.



Derive selectedFieldIds from sandboxedTables, not tableColumnSelections

Previously, field IDs were extracted from all tableColumnSelections,
including entries skipped because the table failed to fetch. Now we
only include IDs from policies that were actually applied, deduplicated.



Use stable placeholder and aria-label in TenantIdentifierInput

The old dynamic placeholder (first suggestion value) looked pre-selected
to users. Now prefix with "e.g." using c().t for localization context,
falling back to "e.g. acme-corp" when no suggestions are loaded. Also
adds aria-label for accessible and stable test targeting.



Update E2E tests to use "e.g. 1" placeholder for tenant_identifier input

The placeholder is now prefixed with "e.g." to clarify it's an example.
This also implicitly validates that autocomplete suggestions were fetched
successfully, since the placeholder is dynamic when data is available.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Tenant creation should differ the data isolation field by strategy (#70258)

Tenant creation should differ the data isolation field by strategy (#70258)

scaffold implementation on tenant isolation fields

refactor: use ts-pattern for isolation field config and label lookups

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

update checklist endpoint to return checklist separate from isolation strategy field

refactor: use dataIsolationFieldValue field for creating tenants

fix: use activeStrategy for reload-aware strategy picker and step content

On page reload, selectedStrategy is null but activeStrategy is populated
from the backend. Switch the data segregation picker value, select-data
step lock condition, title hide flag, and match expression to use
activeStrategy so the correct state is reflected on reload.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

tweak tenant name styling

always require data isolation strategy field value to create tenants

update embedding hub checklist backend test

add e2e tests for reload and isolation strategy

fix: remove hard-coded string in tenant identifier input

refactor: move DataSegregationStrategy type to embedding-hub types module

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

refactor: remove single-letter variables

* Show segregation strategy specific data permissions in tenant summary (#70268)

Show segregation strategy specific data permissions in tenant summary (#70268)

show segregation strategy specific summary

test: add unit tests for getDataPermissionsDescription

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

fix: add selectedFieldIds to TenantsSummaryOnboardingStep plugin interface

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

add e2e test assertions and test cases

simplify row-level security summary logic

fix rls imports

update data permission description span

add bold text to description summaries

fix failing e2e tests

fix starter test failing

remove focused test

update comment on the rls e2e test

fix: use singular 'table' for single-table RLS data permissions description

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

add a comment on why RLS needs the same column

use conjunct method for string formatting

simplify textFromReactNode

simplify getDataPermissionsDescription unit test

chore: code formatting

remove route title

fix: failing test due to placeholder update

remove the starter test as it does not apply

fix: reset hiddenParameters when switching from guest to SSO embed mode for dashboards

When recentlyCreatedDashboards pre-populates the dashboardId, the resource loads
in guest mode and initializes hiddenParameters with all parameters as disabled.
Switching to SSO now resets hiddenParameters to [] for dashboard embeds, matching
the existing behavior for question embeds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

fix ignore_error flag

fix: incorrect test assertion for database selector

fix: incorrect placeholder assertion

* Tenant users are unable to query database after onboarding flow (#70476)

Tenant users are unable to query database after onboarding flow (#70476)

fix(embedding): set create-queries permission for RLS tenant users

Tenant users with RLS could view dashboards but were unable to query
the database because the permissions graph only set `view-data` without
`create-queries`. Add `"create-queries": "query-builder"` at the
database level for both sandboxed and impersonated configurations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

fix: update unit tests on embed settings

fix failing tests due to parameters reset

* Polish tenant creation and SSO for embedding onboarding (#70477)

Polish tenant creation and SSO for embedding onboarding (#70477)

polish permissions setup and create tenants onboarding

update copy for tenant identifier

* fix(type): add rlsColumnName type to plugin defs

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 19:02:34 +07:00
Fraser
6d79bbc173 Stop using 2am in tests to avoid DST issues (#70562)
* Stop using 2am in tests to avoid DST issues

* Scope to table cell data
2026-03-09 07:41:57 +00:00
Eric Normand
7c8125d4bb Fix typo in self-dependency removal GHY-3198 (#70535)
There was a typo in a transducer where the cat and remove were swapped. The cat was meant to combine collections (`#{x})` and the remove was looking for `x`, but since it happened in the wrong order, it never removed them. This led to self-dependent entities, and therefore cycles which caused infinite loops.
2026-03-07 21:58:17 +00:00
Chris Truter
54439261a4 Fix case sensitiviy issues with workspace query execution (#69350) 2026-03-07 07:35:50 +00:00
Maz Ameli
18706c2456 Switch component improvements (#70520)
* fix toggle alignment

* reduce space above description

* change usage guidelines to recommend sm

* Update Loki Snapshots

---------

Co-authored-by: Metabase Automation <github-automation@metabase.com>
2026-03-06 22:57:08 +00:00
Chris Truter
3d5b673c9a Support template tags in Workspace transforms (#69561) 2026-03-06 21:21:37 +00:00
Chris Truter
0e1325ba1e Implement scoped authorization to limit API surface (#70372) 2026-03-06 23:01:36 +02:00
Jacob Joseph
9c801dfbd9 Fix numbered list for row and column security section (#70525) 2026-03-06 20:31:04 +00:00
Nick Fitzpatrick
c27eee3f51 ignore linting issue in embedding test (#70514) 2026-03-06 16:13:58 -04:00
Maksym Yakubych
1cfc71fed7 fix: NLQ profile incorrectly used for SQL editor requests (#70436)
* fix: NLQ profile incorrectly used for SQL editor requests

* fix: tests
2026-03-06 12:55:40 -06:00
Nathan Voxland
619f2b2be6 Add repro-bot github workflow (#70435) 2026-03-06 11:02:20 -06:00
Nick Fitzpatrick
64978428b8 fixing linting (#70504) 2026-03-06 16:55:14 +00:00
dpsutton
58d2cd37b6 bump c3p0 to 0.12.0 (#70494) 2026-03-06 09:36:34 -06:00
Nicolò Pretto
6901847ed2 remove noisy logs from sdk component tests (#70416)
* should fix clj files imported from sdk component tests

* reduce noise when not finding the sdk package

* remove mantine from tests that don't need it

* eslint rule
2026-03-06 16:32:49 +01:00
Nicolò Pretto
10286754ee feat(sdk): speed up the embedding sdk loading time: do auth flow in parallel with bundle loading and split the bundle in chunks (#70015)
* test ci

* fix

* fix e2e

* make linters happy?

* more ci fixes

* more ci fixes

* maybe fix flake

* more ci things?

* more things including a test that we do fallback to old auth

* remove un-needed changes

* more cleaning

* make tests more stable

* clean up debug comments

* remove ! that was not needed

* fix broken build

* cleanup

* more cleaning up

* more cleanup

* clean up consts

* more cleaning up and saml bail out tests + logic

* revert uneeded change in copy js plugin

* better comments and renamed chunk auth function

* merge test files, fix double auth requests done due to missing redux dispatch

* fix typo in backend comment

* fixed version of renaming param to useLegacyMonolithicBundle

* remove cy wait

* remove wait + fix matcher on request for dev mode to not match map file

* clean up tests

* remove empty lines from gitignore

* remove unrelated gitignore change

* r emove imports

* cleanup tests from redundant tests

* remove other un-needed code

* comments

* add date to a TODO as suggested by Kelvin

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* rename checkForAuthConfig -> checkForAuthConfigAndStartAuth

* startAuth -> startJwtAuth

* use new URL to construct baseUrl

* use shared auth  logic

* use the real version as we can actually

* add comment on numTestsKeptInMemory usage

* add test about path traversal

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-06 14:51:25 +00:00
Nicola Mometto
8f1606adc5 Allow loading of 58 serialized tables in 59+ (#70486)
* fix: allow importing 58 serialized data-layer values

* add test
2026-03-06 14:13:16 +00:00
Ngoc Khuat
5b6e3071ac Fix duplicate alerts/subscriptions firing in multi-pod clusters (#70474)
* Fix duplicate alerts/subscriptions firing in multi-pod clusters

Set acquireTriggersWithinLock=true to force pessimistic locking during
trigger acquisition. Without this, Quartz uses an optimistic UPDATE that
is vulnerable to an ABA race condition where multiple cluster nodes can
acquire and fire the same trigger simultaneously.

* Add *warn-on-reflection* to custom_migrations/util

Fixes clj-kondo warning triggered by Java interop (System/setProperty,
System/clearProperty) added in the acquireTriggersWithinLock fix.
2026-03-06 10:35:46 -03:00
Edward Paget
76e96af834 fix: only skip deleting commonpy not pushing it (#70439)
* fix: only skip deleting commonpy not pushing it

* fixup! kondos
2026-03-06 07:16:39 -06:00
Sébastien
92017bab3f fixed table width in guest embed (#70418)
* fixed table width in guest embed

* fixed test
2026-03-06 14:10:57 +01:00
Ngoc Khuat
c86caa092e Unifying source tables transform format (#70298) 2026-03-06 18:48:41 +07:00
Jakub Chodorowicz
29590b8e09 Fix TS failure - duplicated selectors (#70480)
remove duplicated selectors
2026-03-06 10:07:41 +00:00
Ngoc Khuat
e822288497 GDGT-1872: Change transform metering to tier-based keys (#70366) 2026-03-06 05:46:42 +00:00
Noah Moss
a6c90cc7ff Filter out inactive tables for metabot (#70440)
* Filter out inactive tables for metabot

* fix another spot
2026-03-05 23:02:01 +00:00
Riley Thompson
1571e58188 Data migration to fix clickhouse upload dbs with missing schema (#70363)
* testing

* add migration

* add test

* reorder test

* improve migration

* improve test

* add comment explanation

* fmt
2026-03-05 17:05:33 -05:00
Fraser
ae1e0d6311 Convert admin/people/selectors to TypeScript (#69431)
* Convert `admin/people/selectors` to TypeScript

Part of the Accelerate TypeScript Migration project.

Closes DEV-1509

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add people property to AdminState type

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:57:15 +13:00
Fraser
3f547462c9 Convert linked-filters to ts (#70045) 2026-03-06 10:57:01 +13:00
Jeff Bruemmer
05aaad4f3a README - updates (#70460) 2026-03-05 16:02:15 -05:00
Ben Grabow
92ec0df7b1 Ignore some generated files (#70454) 2026-03-05 14:56:22 -06:00
metamben
35b542b651 Fix column-metadata->field-ref-source-alias-test (#70458) 2026-03-05 20:21:57 +00:00
Cam Saul
ab7e4712b1 Omit pivot-grouping from window function OVER PARTITION BY and ORDER BY expressions (#70273)
* Experimental fix for #70088

* cljfmt

* Document the new key

* Cleanup
2026-03-05 10:31:07 -08:00
Maksym Yakubych
47471f9518 fix: metabot returns null for pasted question references (#70388) 2026-03-05 12:23:16 -06:00
Eric Normand
86868639b4 test: mutation tests for column-metadata->field-ref (#69342)
* [Mutation Testing] Add tests for metabase.lib.field/fieldable-columns

* test: mutation tests for column-metadata->field-ref source-alias propagation

Tests the MEGA HACK source-alias-to-join-alias propagation in column-metadata->field-ref:
- Verifies source-alias becomes :join-alias for table-default columns
- Verifies inherited columns do not propagate source-alias
- Verifies inherited columns do not get non-inherited propagated keys

Kills 6 more mutations (51/60 total, 9 unkillable due to schema constraints).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 12:23:21 -05:00
Eric Normand
c61c93e825 test: mutation tests for add/remove field operations (#69329)
* [Mutation Testing] Add tests for metabase.lib.field/fieldable-columns

* test: mutation tests for add-field, add-field-to-join, remove-field, remove-field-from-join

Adds tests that kill surviving mutations:
- add-field: expression column with explicit :fields (kills :source/expressions mutation)
- add-field-to-join: join with :none fields (kills 5 mutations)
- remove-field: expression column removal (kills :source/expressions mutation)
- remove-field-from-join: join with :none fields (kills 4 mutations)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 10:44:10 -06:00
Filipe Silva
6dfde80586 rfct: remove superfluous comments (#69099)
Fix https://github.com/metabase/metabase/pull/68478#discussion_r2760627013
2026-03-05 15:48:59 +00:00
Nemanja Glumac
009911d60a Fix incomplete URL substring sanitization in LeafletMap (#70428)
The previous check used `.includes("openstreetmap.org")` which could be
bypassed by malicious domains like `openstreetmap.org.evil.com` or
`fake-openstreetmap.org`.

This fix validates that the hostname is either exactly `openstreetmap.org`
or a legitimate subdomain ending with `.openstreetmap.org`.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-05 14:40:14 +00:00
Braden Shepherdson
237bbe2919 [MBQL lib] Unexport lib.options namespace (#70276)
Fixes QUE2-351.

I added a new exported function `lib/ignore-case`, since some drivers,
test, and Metabot were using `lib.options/update-options` to set
`:case-sensitive false` on `:contains` et al clauses.

`lib.options` is only used by friends now (mostly QP).
2026-03-05 14:28:44 +00:00
Oleksandr Solovyov
c8b1767e66 Semantic search speedup (#70317)
* do not put embeddings in metadata json for semantic search
* semantic search: filter personal_owner_id right in the database
* simplify batch-update code
2026-03-05 15:18:13 +02:00
Uladzimir Havenchyk
18e327042f chore: drop unnecessary react imports (#70415) 2026-03-05 16:00:30 +03:00
Alberto Perdomo
f02d26aa14 EMB-1329: update embed wizard terms copy and add link (#69856)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Phoomparin Mano <poom@metabase.com>
Co-authored-by: Nicolò Pretto <info@npretto.com>

closes #69302
Closes linear.app/metabase/issue/EMB-1329/embedding-tooltip-directs-you-to-read-license-text-linked-above-no
2026-03-05 11:58:06 +00:00
Timofey Kachalov
8056c1835a fix(sdk): Fix the empty container detection in SDK question layouts (#70389) 2026-03-05 15:40:13 +04:00
Kevin Kelleher
04055423f0 Admin TS conversions (#69825) 2026-03-05 06:37:33 -05:00
Uladzimir Havenchyk
4e7d009a53 chore: convert Text visualization to typescript (#70364)
* chore: convert Text visualization to typescript

* fixup
2026-03-05 10:28:55 +03:00
Romeo Van Snick
a41303a180 Wrap buttons and title on smaller screens (#67681)
* Wrap title and buttons earlier when nav bar is open

* Wrap title in header at wider breakpoint

* Left align ad-hoc questions too

* Add gap when question title collapses

* Increase breakpoint

* Fix padding on ViewSection

* Use container query for query builder breakpoints

* Make padding more consistent with other headers on small screens

* Tweak question action buttons on small screens

* Add separator for actions

* Fix padding on filter panel

* Fix padding on back button

* Fix padding to match header

* Break sooner

* Do not break subheader as soon

* Fix test

* Remove reliance on query-builder from FilterPanel component

* Remove spurious margin

* Fix gap between header items

* Move container to header and footer
2026-03-05 15:02:09 +11:00
Fraser
8474a8f7f5 Convert metabase-lib utils to ts (#70044) 2026-03-05 16:24:06 +13:00
Anderson
2b61cce77b Don't call trial check EE endpoint in OSS for Remote Sync upsell (#70349) 2026-03-04 21:16:03 -05:00
Riley Thompson
0a0625d163 Revert "Upgrade clickhouse jdbc driver to 0.9.7" (#70403)
Revert "Upgrade clickhouse jdbc driver to 0.9.7 (#70387)"

This reverts commit 3a69ad8f49.
2026-03-04 16:50:23 -08:00
Jeff Bruemmer
bb42762372 docs - keys to data studio (#70405)
* keys to data studio

* link
2026-03-04 18:04:50 -05:00
Alexander Polyankin
420c4e27e0 Add writable-connection feature flag (#70287) 2026-03-04 15:54:40 -07:00
Romeo Van Snick
7d8f3849bf Polish transform template tag frontend (#70095)
* Rename confusingly named setParameterAttribute

* Remove unnecessary check

* Add options to disable parameter settings

* Disable parameters settings in transform sidebar

* Add e2e test for parameter field settings

* Rename disableParamaterSettings to parametersAreUserVisible

* Make parametersAreUserVisible configurable all the way up to QueryEditor

* Update copy when parameters aren't visible to user

* Fix test
2026-03-04 13:37:06 -08:00
Denis Berezin
def46a7c0c Update jspdf to fix security issues (#70393) 2026-03-04 23:28:10 +03:00
Riley Thompson
3a69ad8f49 Upgrade clickhouse jdbc driver to 0.9.7 (#70387)
* upgrade clickhouse jdbc to 0.9.7

* clean up todos
2026-03-04 19:22:08 +00:00
metamben
3a62030c09 Generate different column group names for joins to the same table (#70119)
* Generate different column group names for joins to the same table

Closes QUE2-180
2026-03-04 18:52:17 +00:00
metamben
8a93fb4fcd Make excluded-datetime return integers for days too (#69915)
* Make excluded-datetime return integers for days too

Closes QUE2-254
2026-03-04 18:24:04 +00:00
Egor Grushin
483117981c DEV-1599 DEV-1598 fix flakiness (#70166) 2026-03-04 17:51:33 +00:00
Nicola Mometto
88f48ce996 Restrict notification schema and allowed template values (#70261)
* fix: restrict channel renderer to hbs files in the template path

* restricted api schemas

* explicit validation

* chore: tests

* fix: test
2026-03-04 15:23:35 +00:00
Rodrigo López Dato
7eb9d27107 Conceal JWT signing secret by default (#70329) 2026-03-04 11:50:29 -03:00
Nemanja Glumac
c5b8eef4d9 Add cache restore keys for M2 and Node modules (#70370) 2026-03-04 14:17:36 +00:00
Chris Truter
9c44b33887 Add malli typing for transform execution methods (#70250) 2026-03-04 13:23:12 +00:00
Nicola Mometto
3b030bcd58 fix test after out-of-order merge (#70368) 2026-03-04 13:01:13 +00:00
Nicola Mometto
4b9c2e7720 support multiple join conditions in join-analysis lens (#70247)
* fix: count on the condition rather than trying to extract field, generalized for multiple join conditions

* chore: tests

* fix: inline
2026-03-04 11:59:29 +00:00
Chris Truter
57586f2427 Extract non-appdb module out of transforms (attempt #3) (#70245) 2026-03-04 09:50:35 +00:00
Stas Gavrylov
d813be6b09 GDGT-1755: Migrate AdHocQuestionLoader, Breadcrumbs, QuestionLoader, QuestionResultLoader, SavedQuestionLoader to TS (#70106)
* Migrate AdHocQuestionLoader component to TS

* Migrate Breadcrumbs to TS

* Migrate QuestionLoader, QuestionResultLoader, SavedQuestionLoader to TS

* GDGT-1755: Add type guards for Breadcrumbs

* GDGT-1755: Restore comments

* GDGT-1755: Address review comments

* GDGT-1755: Fix tests

* GDGT-1755: Rename type
2026-03-04 11:30:20 +02:00
Chris Truter
ccd3e61ff0 Fix clickhouse transform flake, hopefully (#70316) 2026-03-04 08:49:54 +00:00
Ben Grabow
5e647df5be Add bgrabow (#70347) 2026-03-03 22:18:36 +00:00
Braden Shepherdson
04a5c90dab [MBQL lib] Move lib.util/join-strings-with-conjunction to util.i18n (#70270)
Fixes QUE2-371.

This function has nothing to do with lib, and only depends on
`clojure.string`.
2026-03-03 22:13:40 +00:00
Nathan Voxland
bbc00a8677 Don't fail on unknown settings when calling set-many (#70282) 2026-03-03 15:57:10 -06:00
Nemanja Glumac
a43d62a505 Disable scheduled cloverage reports (#70313) 2026-03-03 22:39:11 +01:00
Ben Grabow
693ac2a9ce Make a mage task to generate resolve-as XML from clj-kondo lint-as data (#70342) 2026-03-03 15:33:17 -06:00
Alex Yarosh
d4a888c2ec docs: a lot of updates (#70176)
* oops

* links

* more links

* Update docs/configuring-metabase/appearance.md

* Apply suggestions from code review

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>

* Apply suggestions from code review

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>

---------

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>
2026-03-03 20:39:03 +00:00
Alex Yarosh
73a53fd72d docs: update default target branch for docs autogen (#70334)
Update default target branch to release-x.59.x
2026-03-03 20:04:50 +00:00
Abed Habli
7a88dfba69 Docs V59: Adding profile icon for non-admin nav paths (#69969)
adding profile icon for non-admin nav paths
2026-03-03 14:30:32 -05:00
Alex Yarosh
b6b44f3cd6 docs: transform screenshots (#70225)
* transform updates

* more transform

* Optimized 11 images, saved 1015.9 KB (45.3% smaller)

* screenshot typo

---------

Co-authored-by: Metabase Automation <github-automation@metabase.com>
2026-03-03 14:19:42 -05:00
Jeff Bruemmer
e586b78513 docs - embedded Metabot (#70275) 2026-03-03 18:44:55 +00:00
John Swanson
330fb2727e SSO users can reset password if provider disabled (#70219)
Previously, password-reset-disabled? blocked password reset for all
SSO users unconditionally (except Google when SSO was globally
disabled). After a license downgrade, EE SSO users were locked out —
they couldn't use SSO (no license) and couldn't reset their password
either.

Introduce sso-source-enabled? which checks whether a user's specific
SSO provider is currently active, using setting/get to respect the
:feature guards on EE settings. password-reset-disabled? now delegates
to this function, so password reset is only blocked when the provider
is actually usable.
2026-03-03 10:42:00 -08:00
Maksym Yakubych
ea89a862ec feat: improve mention menu in metabot (#69650) 2026-03-03 17:33:32 +00:00
Alex Yarosh
0e6915287b docs: start redirect (#70323)
start redirect
2026-03-03 12:25:40 -05:00
Jakub Chodorowicz
d0ffa5115d Gdgt 1801 visualization typescript conversions leafletmap.tsx (#70248) 2026-03-03 17:26:53 +01:00
dpsutton
86dcec8883 Set team.json and assign modules: (#70309)
team:

```javascript
    {
      "name": "Graphy",
      "label": ".Team/Graphy",
      "members": [
        "ranquild",    // alex p
        "romeovs",     // romeo
        "ericnormand", // eric
        "galdre",      // timothy
        "dpsutton",    // dan
        "escherize",   // bryan
        "bgrabow"      // ben
      ]
    }
```

Modules:
- analyze
- graph
- sync
- enterprise/dependencies
2026-03-03 09:45:28 -06:00
Rodrigo López Dato
b2c736e4ca Missing space + log4j capitalization (#70271)
* Missing space + log4j capitalization

* Don't make changes in autogenerated MD files
2026-03-03 11:56:20 -03:00
Dan Stone
4a3e155ac9 Reenable bigquery incremental test (#70252)
Was toggled off individually due to issues with write consistency:
https://github.com/metabase/metabase/pull/69753
2026-03-03 14:44:16 +00:00
Braden Shepherdson
0f687a4a6b [MBQL lib] Prefer calling exported versions of several lib.util fns (#70269)
Fixes QUE2-352.

- `lib.util/strip-id` is newly exported with the more self-documenting
  name `lib/display-name-without-id`.
- `lib.util/clause?` and `clause-of-type?` were already exported
- `lib.util/source-card-id` (and `source-table-id`) are exported as
  `lib/primary-source-card-id` (and likewise for tables).

This isn't all the escaped usages of `lib.util` functions, but other PRs
will handle those.
2026-03-03 09:25:08 -05:00
Jakub Chodorowicz
5274ca8dcb Remove obsolete LeafletHeatMap and leaflet.heat (#70301) 2026-03-03 15:18:29 +01:00
github-automation-metabase
e0123c6246 Auto-update documentation for master (#70289)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-03-03 09:03:48 -05:00
Anderson
4301a00c1b Add snowplow events when creating measures and segments (#70218)
* Add snowplow events when creating measures

* Track also from the table measures page

* Add tracking events when creating segments

* Fix e2e
2026-03-03 13:47:37 +00:00
Kamil Mielnik
7dc0929cc1 GDGT-1834 Visualization Typescript Conversions - lib/settings/ (#69893) 2026-03-03 19:39:50 +07:00
Nicolò Pretto
cf40c24b36 set fail-fast to false in sdk host/sample apps tests (#70260)
disable fail-fast on sdk tests
2026-03-03 09:20:37 +01:00
Uladzimir Havenchyk
5c4c8753d7 chore: convert viz tests and helpers to typescript (#70059)
* chore: convert viz tests to typescript

* convert more tests

* more tests

* more tests

* more tests

* use global helpers

* more tests

* more tests

* convert settings definition

* more files to typescript

* more files to typescript

* fixup

* fixup

* Update v59 sample Examples visualizations and dashboard. (#70075)

Adds Box Plot viz and a Scalar viz with conditional colors to the Example dashboard.
The resources/sample-content.edn was regenerated from the live instance content.

* Pin patched libraries to their patched version (#70143)

* address review

* add a guard

---------

Co-authored-by: Katya Voropaieva <110612633+KorowkaMu@users.noreply.github.com>
Co-authored-by: Nemanja Glumac <31325167+nemanjaglumac@users.noreply.github.com>
2026-03-03 11:14:29 +03:00
Anderson
d6540d2d62 Add snowplow events when creating a metric (#69990)
* Add snowplow events when creating a metric

* Fix tests in data-studio-metrics.cy.spec.ts

* Fix prettier
2026-03-03 01:09:31 -05:00
Nathan Voxland
d7f7d8285f Use the search cluster lock for both init indexing and re-indexing (#70272) 2026-03-02 18:07:55 -06:00
Jeff Bruemmer
cd68964a13 docs - fix summary formatting in users-roles-privileges.md (#70277)
Fix summary formatting in users-roles-privileges.md
2026-03-02 21:49:38 +00:00
Cam Saul
5e35a33837 Track whether column is :lib/from-model? (#70128)
* Track whether column is `:lib/from-model?`

* Additional fix 🔧

* Fix GH issue number

* (Try an evil hacc)

* Explain the HACC
2026-03-02 12:56:49 -08:00
Eric Normand
1734750c21 [Mutation Testing] Kill mutations in metabase.lib.field/fieldable-columns (#69321)
[Mutation Testing] Add tests for metabase.lib.field/fieldable-columns
2026-03-02 13:56:24 -06:00
Fraser
bb7604b53e Convert hoc-utils to ts (#70046) 2026-03-03 08:53:39 +13:00
Fraser
ea43ff2816 Convert Legend components to TypeScript (DEV-1502) (#69427)
* Convert Legend components to TypeScript (DEV-1502)

Convert Legend, LegendItem, LegendItemDot, and LegendLayout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 08:53:24 +13:00
Alex Yarosh
e8c624b479 docs: update plans for writeable connection (#70264)
* 😑

* remove blockquotes

* Update docs/databases/users-roles-privileges.md

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>

---------

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>
2026-03-02 19:06:42 +00:00
metamben
97d10fee1c Preserve metadata edits for summary columns (#70235)
* Preserve metadata edits for summary columns

Closes QUE2-17
2026-03-02 21:50:22 +03:00
Stas Gavrylov
5c6250fe4d GDGT-1859: Fix enabling Advanced Transforms (#70183)
* GDGT-1859: Fix Python transforms not enabled after adding to the trial

* GDGT-1859: Add page refresh after enabling Advanced Transforms

* GDGT-1859: Update unit tests
2026-03-02 13:42:27 -05:00
Nicolò Pretto
21a620c6e8 fix stress tests for sdk component tests (#70253) 2026-03-02 17:30:51 +01:00
Braden Shepherdson
1e3b836a7b [MBQL lib] Improve the docstrings for lib.computed (#70207)
The high-level docs in this namespace were lacking, and I got asked for a tour of it, so I improved the docs as well.
2026-03-02 10:25:52 -05:00
Dan Stone
a6ae573321 Turn on snowflake/bq tests for transforms (#70199)
They seem to be working ok https://github.com/metabase/metabase/actions/runs/22575305288
2026-03-02 14:21:42 +00:00
Egor Grushin
14740891cd GDGT-1785 Load cards in batches in the Transform Inspector (#70129) 2026-03-02 11:54:30 +00:00
Uladzimir Havenchyk
908ca1a1d6 chore: split into components and hooks ClickMappings (#70097)
* chore: convert dashcard related redux components to typescript

* fixup

* address review

* another review iteration

* add a missing import

* chore: split into components and hooks ClickMappings

* fix imports

* delete old file

* make hook code human readable
2026-03-02 12:11:53 +03:00
Oleksandr Solovyov
94309cd070 Add toggle to enable/disable the Metabot (#70111) 2026-03-02 10:38:59 +02:00
Romeo Van Snick
d54e6b64e2 Convert parameters to typescript (#70137)
* Convert parameters to TypeScript

* Fix types

* Fix types in normalizeValue

* Fix types in dashboard tests

* Fix more types

* Fix test

* Remove duplicate check

* Naming
2026-03-02 17:07:10 +11:00
metabase-bot[bot]
763d5e1777 [chore] Update E2E test timings with recent data (#70236)
Update E2E test timings with averaged data

Collected timing data from 7 days of test runs.
All specs with timing data were included (simple averages).

🤖 Generated automatically by update-e2e-timings workflow

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: GitHub Actions <actions@github.com>
2026-03-02 12:16:14 +13:00
Fraser
b746306e12 Convert VisualizationError utils to TypeScript (#69062)
* Convert VisualizationError utils to TypeScript

Closes DEV-1407

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-02 11:53:39 +13:00
Fraser
f0c90980ff Convert DefaultParametersSection to TypeScript (#69075)
* Convert DefaultParametersSection to TypeScript

Closes DEV-1420

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-02 11:53:21 +13:00
Egor Grushin
e53f226b40 GDGT-1852 fix ui issues in Data Summary tab in Transform Inspector (#70150)
* GDGT-1852 fix ui issues
2026-03-01 22:48:32 +00:00
Romeo Van Snick
40cbf2ffb5 Avoid updating url when navigating to query builder (#68982)
* Use Question.serializeCardForUrl for consistency

* Convert metabase/lib/card to typescript

* Remove broken createCard helper

* Fix types

* Remove unused parameter

* Remove entityId from cards

* Remove createCard tests

* Avoid renaming Card to CardObject for no reason

* Also allow serializing UnsavedCard

* Make options optional

* Simplify serializeCardForUrl

* Make serializeCardForUrl implementation match the Question.serializeCardForUrl implementation

* Use serializeCardForUrl from metabase/lib/card

* Use serializeCardForUrl from metabase/lib/card

* Use stable stringify for url hashes

* Remove unnecessary _serializeForUrl helper

* Correctly ignore possibly infinite type instantiations

* Fix unit tests

* Allow passing parameterValues

* Fix comparison

* Coerce values to undefined

* Update unit tests

* Remove duplicate key

* Make cards optional
2026-03-02 08:38:55 +11:00
Braden Shepherdson
55a0afbd2a [MBQL lib] Stop using ID-based refs for fields from joined cards (#70116)
Fixes QUE2-383.

Uses `:lib/card-id` in metadata to check if a column came from a card at any point.
If so, it should use a name-based `:field` ref rather than an ID-based one.
This makes the refs to joined columns consistent with those to main source columns,
based on whether the source is a table or card.
2026-02-28 20:10:19 +00:00
Robert Kosara
f759d9f473 Docs: Tenant attributes and collections improvements (#70179)
* Tenant attributes and collections improvements

First round of edits to add the ability to create tenant attributes using SSO.

* Revise tenant-specific access for non-admin internal users

* Edits based on Alberto's comments

* Move tenant claims info to claims section, link to it from attributes

* Apply suggestion from @alexyarosh

Co-authored-by: Alex Yarosh <alexandra@metabase.com>

* Moved tenant attribute logic and simplified sentence

* Fix a sentence, improve code example, and clean up permissions bullet points

---------

Co-authored-by: Alex Yarosh <alexandra@metabase.com>
2026-02-27 13:46:20 -08:00
Uladzimir Havenchyk
39951a570a chore: make entity copy modal generic (#70018)
* chore: convert dashcard related redux components to typescript

* fixup

* address review

* another review iteration

* chore: make entity copy modal generic

* chore: convert dashcard related redux components to typescript

* fixup

* address review

* another review iteration

* add a missing import

* type cleanup

* address review
2026-02-28 00:07:56 +03:00
Alex Yarosh
6b369ef818 docs: writeable connection (#70210)
* writeable connection

* Optimized 1 images, saved 89.0 KB (46.9% smaller)

* routing notes

* typos

---------

Co-authored-by: Metabase Automation <github-automation@metabase.com>
2026-02-27 14:33:37 -05:00
Nemanja Glumac
f3c352d920 Increase viewport to reduce "extract column" flakes (#70206) 2026-02-27 20:06:00 +01:00
Anderson
9c0576cc5c Add snowplow event for opening Data Studio (#69880)
* Add snowplow event for when opening Data Studio

* Update triggered_from
2026-02-27 18:21:02 +00:00
Nemanja Glumac
fc00027fca Migrate away from deprecated Cypress.env() API (#70189)
As of Cypress 15.10.0, `Cypress.env()` is deprecated. This commit migrates
all usages to the new recommended APIs:

- Non-sensitive config values → `Cypress.expose()` (synchronous)
- Sensitive token values → `cy.env()` (asynchronous)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-27 19:03:29 +01:00
Ngoc Khuat
3be69174e6 Improve logging around notifications (#70099) 2026-02-27 16:55:43 +00:00
Nicola Mometto
11a73d8621 Add position field to dashboard parameters for stable serialization (#70107)
* revert sorting by id on parameters serialization, parameters ordering is significant and must be preserved

* revert, we're storing position

* feat: add :position to parameters, backfill

* feat: backfill position on imported parameters

* chore: tests

* FE: update position

* chore: gitignore python-sources.lock

* chore: ensure we get position

* restrict to dashboard params

* tests

* simpler approach: just sort by position on serialization export/import
2026-02-27 16:06:32 +00:00
Dan Stone
d1e7b483dd Prefer SQL DML for insert-from-source! to fix bigquery metadata consistency (#69753)
The legacy .insertAll API (and newer Storage API) do not work well with
tables that are created/dropped frequently. This is because their
metadata caches can take a long time to up date.

Python transforms use the drop-recreate fallback strategy for bigquery
so we observe this inconsistency for every run after the first, even
with absurd multi minute timeouts.

The SQL API does not have a problem, so we'll use that. We expect a
performance regression but its better to work slowly than not work at
all.

Other changes:

- Removed now redundant bigquery retry loop

- Dropped dictionary support, we cannot support it easily and it cannot be
considered higher prio than bigquery working at all.

- Added correct parameter typing for JSON.

- Fixed ordering issues in tests (assumed row order when it is
non-deterministic)
2026-02-27 15:47:58 +00:00
Ngoc Khuat
aa66ab25d6 Simplify notification result data flow (#70115) 2026-02-27 21:58:54 +07:00
Edward Paget
8207fc3181 feat: error code on streaming response throw/fail (#62351)
* fix: real error codes in streaming responses

* fixup! try faking old errors deeper in the stack

* fixup! wrap fetch endpoint in embedding too

* fixup! treat string only errors differently

* fixup! handle errors in a couple other places

* fixup! hard to figure out what kind of error it is

* fixup! typecasts

* fixup! test arity

* fixup! remove blanket 403 page from dataset

* fixup! simplify backend 403 responses

* fixup! a couple missed tests

* fixup! add mockresponse for mockclient

* fixup! other failing tests

* fixup! last failing test

* fixup! add another OSS check

* fixup! one more oss test

* fixup! update status codes on some more tests

* fixup! use committed function
2026-02-27 09:42:20 -05:00
Nicolò Pretto
b429839f0a remove sdk import error on core app e2e tests (#70188)
try to untangle imports
2026-02-27 14:41:19 +01:00
Jakub Chodorowicz
451f080ede add new variants to action icon and use it in Inspector Header cell (#70180)
* add new variants to action icon and use it in Inspector Header cell

* add is-active state
2026-02-27 12:10:07 +01:00
Dan Stone
4f00289729 Fix bigquery routing test (#70154)
- Was using wrong project / creds
- Using two different sha names only worked when shas were not used as
qualifiers in the query (they are for bigquery)

Added the ability to override the hash-key used for tx/hash-dataset to
support forcing a hash collision (as is required by routing).
2026-02-27 10:58:14 +00:00
Maksym Yakubych
369d410d4d fix: new SQL questions automatically open DataReference sidebar (#70120) 2026-02-26 21:09:43 +00:00
Nicola Mometto
b8c13482df fix: replace usage of source-table-id with primary-source-table-id (#70162) 2026-02-26 14:24:20 -05:00
Sloan Sparger
126eb6b1b1 [Metabot] Prevent OSS SQL gen UI from showing in cloud migrated instances (#69649)
prevent oss sql gen ui from showing for customsers with had set it up locally and migrated to cloud
2026-02-26 12:54:41 -06:00
Noah Moss
4ab19dce4d Ring middleware for public sharing endpoints (#70153)
* Ring middleware for public sharing endpoints

* Refactor test

* move +public-sharing-enabled to address module lint

* Fix documents test
2026-02-26 12:59:54 -05:00
Braden Shepherdson
39b4a21830 [MBQL lib] Refactor a common pattern into new lib functions (#68474)
Many uses of `lib.util/source-card-id` and `source-table-id` were
followed by calling `lib.metadata/card` or `lib.metadata/table`
respectively.

This PR adds two new functions `lib.metadata.calculation/primary-source-card`
and `primary-source-table` which look up the card/table directly.

It also adds `lib.metadata.calculation/primary-source` for the handful
of places where whichever of `primary-source-card` or `primary-source-table`
returns non-nil gets used, such as in some drill-thrus.
2026-02-26 17:32:57 +00:00
Eric Normand
3ba0f90cf0 Automate mutation testing end-to-end with run! orchestrator (#70126)
- Adds a fully automated `run!` entry point to `dev.mutation-testing` that orchestrates the entire mutation testing workflow: generate report → create Linear project → group functions → write tests via `claude -p` → verify → retry →
  commit → create draft PRs
  - Replaces the previous manual, step-by-step SKILL.md instructions (which required the operator to execute each step) with Clojure functions that handle the full pipeline automatically
  - Key new functions: `parse-namespace`, `group-functions`, `build-test-prompt`, `invoke-claude!`, `verify-and-retry!`, `process-group!`, `print-summary!`

  ## Motivation

  The mutation testing workflow previously required ~20 manual steps per namespace, with the Claude operator interpreting skill instructions and running each step. This was slow, error-prone, and required constant supervision. Now you just
  call `(mut-test/run! 'metabase.lib.order-by)` and walk away.

  ## How it works

  1. **`parse-namespace`** — derives source/test paths, short names, auto-detects `.cljc` vs `.clj`
  2. **`group-functions`** — clusters private functions with their public entry points based on test overlap, skips fully-covered functions
  3. **`build-test-prompt`** — assembles a prompt with function source, existing tests, and surviving mutations
  4. **`invoke-claude!`** — shells out to `claude -p` with scoped tool access (`Edit`, `Read`, `Bash(clj-nrepl-eval*)`)
  5. **`verify-and-retry!`** — checks which mutations were killed, retries up to 2x with targeted prompts for survivors
  6. **`process-group!`** — full per-group pipeline: branch → invoke Claude → verify → commit & push → Linear issue → draft PR
  7. **`run!`** — ties it all together, catches errors per group so one failure doesn't stop the batch
2026-02-26 09:51:59 -07:00
Nemanja Glumac
6e799b2ef6 Bump Cypress retries to two (#70157) 2026-02-26 17:42:51 +01:00
Dan Stone
dcd2d7394d Fix snowflake integer type (#70031)
BigInteger is observed
2026-02-26 16:04:33 +00:00
Aleksandr Lesnenko
3c693179e4 fix content translations break visualization settings (#70054)
* fix content translation produces unstable translated keys in viz settings
2026-02-26 10:54:11 -05:00
Braden Shepherdson
b01732fd69 [perf] Tools to repro query + metadata locally, given instance API access (#67952)
Given browser or API access to a Metabase instance, the tools in this PR
can recursively fetch cards and all the metadata (tables, fields, etc.)
required to run them through QP preprocessing and compilation.

Of course this cannot actually _execute_ the compiled form, since you
don't have access to the user's DWH, but it enables debugging of
hard-to-reproduce user bug reports in your local REPL.

This works by making API calls like `/api/card` and
`/api/card/query_metadata` until a complete picture of the metadata is
acquired. This is poured into a `MetadataProvider`, which is all the QP
needs to analyze and compile the query.
2026-02-26 15:49:56 +00:00
Nicola Mometto
a75ee49fd1 restrict a transform inspector test to sql (#70152)
fix: restrict to sql drivers
2026-02-26 15:23:21 +00:00
Eric Normand
dbfe313b4a Filter transforms based on permissions GHY-3152 (#70125)
Filter transforms based on permissions

We were leaking transforms in the :bad_transforms key
2026-02-26 14:55:12 +00:00
github-automation-metabase
088bd180d5 Auto-update documentation for master (#70133)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-02-26 09:46:51 -05:00
metabase-bot[bot]
0226eaa5d2 [chore] Update E2E test timings with recent data (#69996)
Update E2E test timings with averaged data

Collected timing data from 7 days of test runs.
All specs with timing data were included (simple averages).

🤖 Generated automatically by update-e2e-timings workflow

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: GitHub Actions <actions@github.com>
2026-02-26 15:04:49 +01:00
Nicola Mometto
3d68aa1ac4 feat: restrict join-analysis to only table-level join analysis (#70105) 2026-02-26 13:08:34 +00:00
Nemanja Glumac
c18c08b02c Pin patched libraries to their patched version (#70143) 2026-02-26 13:02:41 +01:00
Katya Voropaieva
851bedf5bb Update v59 sample Examples visualizations and dashboard. (#70075)
Adds Box Plot viz and a Scalar viz with conditional colors to the Example dashboard.
The resources/sample-content.edn was regenerated from the live instance content.
2026-02-26 10:45:57 +01:00
Uladzimir Havenchyk
9981cd5323 chore: convert dashboard reducer to typescript and rtk (#69842)
* chore: convert dashboard reducer to typescript

* convert to rtk

* use immer when possible

* fixup

* Apply suggestion from @kamilmielnik

I checked it locally and it is indeed unnecessary guard

Co-authored-by: Kamil Mielnik <kamil@kamilmielnik.com>

---------

Co-authored-by: Kamil Mielnik <kamil@kamilmielnik.com>
2026-02-26 11:47:52 +03:00
Uladzimir Havenchyk
264604b61e chore: convert dashcard mapping files to ts (#69836) 2026-02-26 10:30:10 +03:00
Sébastien
a5a0773eaf Migrate ChartTypeDropdown to use Mantine ComboBox (#69734)
* Migrate ChartTypeDropdown to use Mantine ComboBox

* test fixes

* .

* .

* .

* .

* screenshot
2026-02-26 10:56:02 +07:00
Romeo Van Snick
c463756c60 Remove deprecated unit test helpers (#69275)
* Replace createQueryWithClauses with createTestQuery

# Conflicts:
#	frontend/src/metabase-lib/test-helpers.ts
#	frontend/src/metabase/querying/filters/hooks/use-date-filter/use-date-filter.unit.spec.ts

* Remove unnused helpers

# Conflicts:
#	frontend/src/metabase-lib/test-helpers.ts

* WIP

* Export createMetadataProvider

* Use Lib.createTestHelper

* Remove createQuery helper usage

* Remove createQuery helper

* Remove unneeded getJoinQueryHelpers

* Fix test

* Remove DEFAULT_QUERY

* Move or remove unneeded test helpers

* Fix lint

* Allow auto binning in test spec

* Fix test

* Allow disamiguating column name by index

* Fix tests

* Fix imports
2026-02-26 13:09:27 +11:00
Romeo Van Snick
0f7e8983ca Metakey helper (#68987)
* Use H.metaKey where applicable

* Add helper for meta key options

* Fix import

* Update more instances

* Fix test
2026-02-26 00:51:03 +00:00
Sloan Sparger
3401c9e902 Fix e2e dashboard-questions.cy.spec.js scrollTo flakes (#70114)
* invert assertions about scrollTo hash param and instead assert on visibility

* update e2e/test/scenarios/question/saved.cy.spec.js

* try to improve flakiness

* trying something dumb
2026-02-25 16:22:00 -06:00
Eric Normand
0aeda87a3d [Mutation Testing] Kill mutations in metabase.lib.field/fingerprint-based-default-unit (#69319)
Part of the mutation testing project for metabase.lib.field.

Closes QUE2-338

Functions
Stats
Surviving mutations before: 16
Tests added: 1
Mutations killed by this PR: 16
Mutations remaining: 0
Mutations killed
Replace fingerprint with nil
Replace :type with :type__
Replace :type/DateTime with :DateTime__
Replace let with comment
Replace earliest with nil
Replace latest with nil
Replace when-not with when
Replace days with nil
Replace > with >=
Replace > with nil
Replace days with nil
Replace 1 with 0
Replace :minute with :minute__
Replace 31 with 0
Replace 365 with 0
Replace :week with :week__
Mutations not killed (with rationale)
Replace :day with :day__ — available-temporal-buckets-for-type has its own internal default of :day for datetime types, so the :day return value from fingerprint-based-default-unit is redundant with the fallback
2026-02-25 22:07:25 +00:00
metamben
b1ece80c0d Support DB-local settings in the JVM and JS metadata-providers (#70117)
Closes QUE2-292
2026-02-25 21:52:17 +00:00
metamben
486a6fc826 Add test to prove that #35842 has been fixed (#70080)
Closes #35842
2026-02-25 21:25:19 +00:00
Eric Normand
29f75c1251 test: mutation tests for column-metadata-effective-type and nest-display-name (#69328)
* [Mutation Testing] Add tests for metabase.lib.field/fieldable-columns

* test: add mutation tests for column-metadata-effective-type and nest-display-name

Adds tests that exercise:
- type-of fallback to :base-type when :effective-type is absent
- type-of preference for :effective-type over :base-type
- temporal extraction returning :type/Integer
- temporal truncation preserving effective type
- nested field with nil parent display-name graceful fallback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 21:03:39 +00:00
metamben
66dc6b6cdc Add test to prove that #57978 has been fixed (#70076)
Closes #57978
2026-02-25 23:37:16 +03:00
metamben
7433f38a22 Make visible-columns for cards respect field visibility (#70040)
Closes QUE2-5
2026-02-25 23:36:59 +03:00
Cam Saul
71606de98c Test to prove #28060 is fixed 🔧 (#69865)
* Test to prove #28060 is fixed 🔧

* Update src/metabase/driver/mysql.clj

Co-authored-by: metamben <103100869+metamben@users.noreply.github.com>

* Update src/metabase/driver/mysql.clj

Co-authored-by: metamben <103100869+metamben@users.noreply.github.com>

* Fix syntax error from PR feedback suggestion

* Test fix 🔧

* Fix 🔧

---------

Co-authored-by: metamben <103100869+metamben@users.noreply.github.com>
2026-02-25 17:25:35 +00:00
Braden Shepherdson
d2e8fcac5b [MBQL lib] Export some healthy API functions; drop some API nses (#69666)
Fixes QUE2-350.

There are two changes here:
- Exporting some legit public functions from `lib.core`
- "Befriending" the `enterprise/sandbox` and `lib-be` modules, which are
  deeply coupled to lib and MBQL in any case.

That allows us to remove several exported namespaces:
- `lib.binning`
- `lib.convert`
- `lib.field`
- `lib.metadata.cached-provider`
- `lib.metadata.column`
- `lib.metadata.invocation-tracker`
- `lib.metadata.result-metadata`
- `lib.normalize`
- `lib.query`
- `lib.temporal-bucket`
- `lib.walk`

The functions newly exported from `lib.core` are:

- `lib.field/infer-has-field-values`, called from some BE API handlers
  and AppDB readers, producing a "cooked" value the FE wants rather than
  exposing details of how `:model/FieldValues` is stored. Single-use,
  but a legitimate export.
- `lib.field/json-field?`, checks if the passed in field is a JSON field,
  that's a valid export used in a few places.
- `lib.query/native?`, checks if the first stage of this query is native,
  that's another legit export used in a few places.

`lib.normalize/normalize` and `lib.query/query` were already exported, I
updated some usages of those functions to `lib/foo` instead.
2026-02-25 10:37:07 -05:00
github-automation-metabase
e4d1706837 Auto-update documentation for master (#70096)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-02-25 10:14:22 -05:00
Uladzimir Havenchyk
cc1361936a convert dashboard redux related files to typescript (#69797)
* chore: convert dashcard related redux components to typescript

* fixup

* address review

* another review iteration

* add a missing import

* cleanup
2026-02-25 17:40:38 +03:00
Nemanja Glumac
69c0a2ca90 Upgrade Cypress to v15.10.0 (#69839) 2026-02-25 15:14:45 +01:00
Ngoc Khuat
3d3bbe22db Move pulse preview endpoints to dev-only (#70068) 2026-02-25 11:15:00 +00:00
Romeo Van Snick
ba0ddc9a32 Only switch to full-width native preview at small breakpoint (#70048)
* Only switch to full-width native preview at small breakpoint

* Add e2e test for metabase#67767
2026-02-25 20:22:46 +11:00
Fraser
da223ea0a1 Updated Chromium to v144 in Cypress (#69721)
* Bump chrome to v144
2026-02-25 21:07:41 +13:00
Noah Moss
f53c8d1513 Always run Metabot-triggered refingerprinting in admin context to match sync behavior (#70087)
* always run fingerprinting in admin perms context

* address comment
2026-02-24 17:47:22 -05:00
Fraser
51d7669d7d Convert admin/permissions/utils/urls to TypeScript (#69432)
* Convert `admin/permissions/utils/urls` to TypeScript

Part of the Accelerate TypeScript Migration project.

Closes DEV-1510

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* improve type guard

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Uladzimir Havenchyk <uladzimir@metabase.com>
2026-02-25 10:18:09 +13:00
Anton Kostenko
eb20991259 Fix PR envs cleaning (#70024)
* Fix PR envs cleaning

* Add manual dispatch for pr env destroy
2026-02-24 15:16:50 -05:00
Nicolò Pretto
ec4874cfe5 re-generate lock file to remove stale deps (#70062)
* re-generate lock file to remove stale deps

* add sample apps to the deps for the package
2026-02-24 19:02:35 +01:00
Ngoc Khuat
8f442ca39b Add diagnostic logging for duplicate alert investigation (#70020) 2026-02-24 17:20:02 +00:00
Nicola Mometto
8c6e4aa60b Ensure we can resolve map lookups (#70060)
* feat: explicit handlebars context

* chore: unit tests
2026-02-24 13:53:19 +00:00
Nemanja Glumac
3d3d49dfca Automate Crowdin context harvester and trigger pre-translation (#70035) 2026-02-24 11:11:15 +01:00
Nicolò Pretto
bba60088cb emb-1300: use plugin for handleLink in eajs and fix double sdk plugin init (#69961)
* emb-1300: use plugin for handleLink in eajs and fix double sdk plugin init

* typo and fix unit test

* restore old comment
2026-02-24 09:25:38 +01:00
metamben
981113ef47 Use lib namespace on properties instead of real namespace names (#69808)
* Use lib namespace on properties instead of real namespace names

Closes QUE2-279
2026-02-23 22:53:37 +00:00
Braden Shepherdson
9c8e505c9f [QP] Move logging-only work inside the log/* call (#68362)
Move some logging-only computations into the `(log/* ...)` call so it won't be executed
unless the log level is sufficiently verbose for the log to actually be output.
2026-02-23 16:19:03 -05:00
Noah Moss
f4bbf2984f Add some better defaults & limits to agent API (#70033) 2026-02-23 15:35:18 -05:00
metamben
273f1677a1 Add tests for calculated display_name and fix nested name handling (#69982)
Closes QUE2-288
2026-02-23 23:23:58 +03:00
metamben
12e6b46118 Support all kinds of identity types on sqlserver (#69871)
Closes QUE2-251
2026-02-23 23:23:17 +03:00
Fraser
f27597b904 Convert AddEditSidebar to TypeScript (#69421)
* Convert AddEditSidebar to TypeScript (DEV-1499)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 08:45:56 +13:00
bryan
b2889824b3 Fix flaky count-of-grouping-test for Redshift by using UTC timezone (#67196)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-23 19:18:07 +00:00
Jeff Bruemmer
28cfae2e67 docs - table variables and snippets (#69983)
table variables and snippets
2026-02-23 11:34:02 -05:00
github-automation-metabase
60707e20ec Auto-update documentation for master (#70009)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-02-23 11:21:09 -05:00
bryan
a4e418f280 Fix OpenAPI 3.1.0 validation errors and add CI workflow (#69948) 2026-02-23 09:13:33 -07:00
Stas Gavrylov
c20bf2a943 GDGT-1755: Migrate common components to TypeScript (partial) (#69354)
* GDGT-1755: Migrate Text to TS

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* GDGT-1755: Migrate Label to TS

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* GDGT-1755: Migrate Subhead to TS

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* GDGT-1755: Migrate IconWrapper to TS

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* GDGT-1755: Migrate LabelIcon to TS

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* GDGT-1755: Migrate use-scroll-on-mount to TS

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* GDGT-1755: Missed TS fixes

* GDGT-1755: Migrate QueryButton to TS

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* GDGT-1755: Migrate IconBorder to TS

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* GDGT-1755: Remove Text.tsx wrapper, import from metabase/ui directly

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* GDGT-1755: Adjust line height in places using new Text component

* GDGT-1755: Fix styling inconsistencies

* Update frontend Claude guidelines for typecheck/linting to use bun

* GDGT-1755: Address PR comments

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-23 18:13:01 +02:00
Alexander Polyankin
c66d31fec1 Drop table created in a e2e test (#70023)
* drop table created in a e2e test

* fix
2026-02-23 10:43:37 -05:00
Egor Grushin
93cb3bc327 lock a card display type for the VisualizationCard (#70016)
lock a card display type for the VisualizationCard in the transform inspector
2026-02-23 14:55:23 +00:00
Timothy S. Dean
52325a7503 Write Connections for Actions, Model Persistence, Uploads (#69878) 2026-02-23 09:15:35 -05:00
Fraser
dadf042982 Convert ExpandableString to TypeScript (#69084)
Closes DEV-1429

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-23 15:49:29 +02:00
Dan Stone
18e1fb02aa Revert turning on snowflake/bq tests for transform (#70017)
Revert "Re-enable Python transform tests for BigQuery and Snowflake (#67685)"

This reverts commit b6c1a51630.
2026-02-23 12:41:22 +00:00
Egor Grushin
5dd8b26770 getting rid of annoying warnings (#69975) 2026-02-23 12:17:08 +00:00
Dan Stone
b6c1a51630 Re-enable Python transform tests for BigQuery and Snowflake (#67685)
These tests were temporarily disabled on 2025-09-26 due to flakes.
Re-enabling them now to restore full test coverage.

Lets monitor and see what happens
2026-02-23 10:38:56 +00:00
Nicola Mometto
09ee4178ca Transforms inspector (#68675)
* wip: transform inspector BE

* WIP: inspect tables

* wip

* format join info as code

* wip: join info

* style

* add join stats type and display match rate

* chore: row

* chore: remove limit

* wip

* handle differerent cases for match rate and row count

* add joins table display and icons

* fix copy

* fix missing join table names

* cleanup

* cleanup

* extend match

* fix missing source table type

* add join tooltip

* chore: with join info

* wip

* wip

* add field statistics dummy component

* small ui adjustments

* small ui adjustments

* add filled rows

* order input columns like joins

* update visualization card styling

* wip: update join calc to be iterative

* add target and source stats

* add target and source stats, a filter by output fields and a tab for unmapped fields

* add target and source stats, a filter by output fields and a tab for unmapped fields

* chore: sort by order

* fix: inverse map

* add cross product warning

* add null join warning

* small QoL UI improvements

* wip: render output_row_count

* wip: native joins

* fix a bug with conditional rendering

* wip: try to pull out useful fields

* WIP: claude'd UI selection of visited_fields

* use default automatic display selection logic

* add transform mock page with mock summary section

* add mock api console log

* fix fetch wrong query metadata

* add mock lenses

* add mock lenses api

* REVERTME: ignore for deploy

* feat: schemas

* require

* feat: degeneracy check

* WIP cljc api

* wip context

* wip lenses

* update comments

* chore: visited-fields

* wip

* wip: base

* wip: triggered lens

* wip: lib

* distinguish drill lenses

* fix: return visited-fields

* wip: v2 api

* WIP: vibe'd FE

* tabs/remove mock

* account for params

* params

* ui improvements

* fix

* show fingerprinting

* fingerprint layout

* use BE fallback

* handle more complex joins

* fallback to interesting fields

* fix trigger via cljc

* remove debug

* different text

* compute-card-results in cljs

* condition trigger in cljc

* semi working

* fix card_id correlation

* remove dead code

* rename BE to just inspector

* switch to new

* ensure transform is deleted

* chore: cleanup schema

* bring back the field fingerprint info

* move cljs out of lib

* fix: schema

* minor cleanup

* appease kondo

* kondo

* remove dead code

* feat: centralize native analysis in query_analisis

* chore: remove #p

* clean up

* Gdgt 1663 move fingerprints out of the lens to data inspect (#68996)

* [transforms inspector] depend on trunk macaw (#69022)

depend on trunk macaw

* fix lint and type issues (#69017)

* remove unused lib function

* fix infinite rendering loop

* fix type lib.interestingFields

* fix

* fix: ui bug

* chore: disable visited fields for now

* chore: better unmatched-rows

* chore: exclude not nulls

* minor tweaks

* minor tweaks

* rename

* chore: tighter contract

* minor optimisation

* Gdgt 1669 make cards clickable (#69116)

* Add card link

* link to hash of question

* add clickable title

* remove tooltip

* Use MBQL info to match columns whe npossible (#69120)

use mbql info for column matching

* Gdgt 1675 disable unmatched rows view details button in inspector (#69152)

* remove row index column

* adapt viz UI, hide row index column

* Unmatched rows lens: return cards for both LHS or RHS null scenarios (#69123)

* feat: handle both cases of no join match

* feat: simple degeneracy

* export function

* remove dead fields

* only propose drill lenses that are applicable (#69169)

* Transforms inspector consistent casing for FE (#69174)

* chore: consistency

* chore: some docstrings updats

* feat: snake_case for the FE

* feat: pass visitedFields to interestingFields, remove ad-hoc logic in FE (#69175)

feat: pass visitedFields to interestingFields, remove ad-hoc logic in the FE

* feat: namespace card-ids with params (#69180)

* add estimated lens complexity (hardcoded for now)  (#69181)

* add estimated lens complexity (hardcoded for now)

* refactor: use with-metadata

* add metadata to triggers, not just cards (#69182)

* appease kondo

* chore: don't depend on git sha macaw

* reinstate metabase-enterprise.transforms.core from merge conflict

* Gdgt 1722 new UI for data summary new (#69220)

* Transforms inspector BE tests  (#69225)

* feat: inspector tests

* fix: impls

* fix: nil safe

* feat: integration tests

* make it work for both FE and BE

* fix card-results for non oute rjoin

* e2e test

* fixes

* fix: quoting

* fix for mysql/bigquery/redshift

* gdgt-1721 rework lenses navigation (#69205)

* Transforms inspector module (#69278)

* split transforms inspector into its own module, introduce transforms-rest

* handle kondo modules warnings

* fix: shadow_cljs

* kondo: fixes

* Transforms inspector be review (#69339)

* chore: align

* remove useless comments

* feat: add schema to context

* feat: simplify column-matching

* fix: visited-fields

* fix test

* remove unused

* tighter schema

* extrac-sources test

* fix: missing metadata-provider

* Gdgt 1744 combine separate averages columns into one for data summary (#69355)

* gdgt-1723 New UI for Joins Analysis (#69325)

* Gdgt 1725 allow running transform from empty screen (#69394)

* chore: move crud functions on transforms, avoid -rest modules dependencies

* fix tests

* appease kondo

* fix modules

* fix: small UI bug

* Transforms orphaned columns (#69497)

* add orphaned columns to unmatched-rows analysis

* chore: update tests

* Transforms inspector analytics (#69484)

* feat: flow discover/lens through transform events

* track transform_id in query execution

* feat: add transform-inspector ids/params to query info

* feat: add inspector specific /query endpoint

* feat: hook FE to new api

* feat: add lens_id/params to view

* feat: add them to ua

* fix: pass params too

* fix: use text.type

* fix test

* chore: remove unused requires

* chore: events

* fix FE part

---------

Co-authored-by: Egor Grushin <egor@metabase.com>

* fix: use lib functions instead of manual mbql construction (#69519)

* fix: use lib functions instead of manual mbql construction

* chore: refactor utils

* cleanup

* more manipulation with lib

* bugfix

* strip fields/filters early

* do it in util function

* fix: bad merge resolution

* kondo

* another bad merge resolution

* chore: indent

* Gdgt 1754 handle slow loading lens indicator (#69334)

* lens loading indicator

# Conflicts:
#	frontend/src/metabase/transforms/pages/TransformInspectPage/TransformInspectPage.tsx
#	frontend/src/metabase/transforms/pages/TransformInspectPage/components/LensContent/LensContent.tsx

* add onCardLoaded handler to context

* Gdgt 1724 new UI column (#69282)

* Gdgt 1783 fix wrong args passed to lens query (#69546)

* fix wrong args being passed to lens queries

* improve naming of query parameters

* remove whitespaces from merge resolution

* add transforms api test

* feat: i18n the inspector strings (#69565)

* fix dark mode color issues (#69570)

fix bg fixed to white

* Gdgt 1737 use card metadata instead of parsing card ids (#69583)

* use card metadata instead of parsing ids

* improve way of getting the table name from cards

* use macaw still, TODO

* port some query-analysis to sql-tools (#69732)

* chore: port some query-analysis to sql-tools

* remove sql-tools from transforms

* chore: vendor macaw functions, appease kondo

* refactor

* chore: refactor

* side agnostic

* revert manual delete, closes https://linear.app/metabase/issue/GDGT-1730/pre-merge-blocker-revert-t2delete-on-transform-once-uxw-3021-is-fixed

* fix: always sync audit db on change

* dead code

* fix: use honeysql instead of building sql manually (#69774)

* GDGT-1791 Maximum depth error in console (#69754)

GDGT-1791 fix and refactor useCardLoadingTracker

* Gdgt 1651 add frontend e2e tests for transform inspector UI (#69677)

* GDGT-1823 make guarantee that LensContent is always stores one lens data (#69800)

* fix: 404 on lens not found (#69838)

* fix: 404 on lens not found

* chore: don't pmap

* fix: remove nil values from statistics

* fix

* Transforms inspector self review be (#69843)

* style

* chore: reuse resolve-join-sides

* add from-table-id for mbql queries too

* split context in mbql/native, don't union

* whitespace

* fix: preprocess native queries

* fix: update tests

* fix

* revert unrelated commit that made in the branch erroneously

* Gdgt 1812 better handle missing sources in lens inspector (#69848)

* Gdgt 1817 add snowplow product events fe (#69801)

* Add analytics events

# Conflicts:
#	frontend/src/metabase/transforms/analytics.ts

* add analytics tests

* simplify and improve lens loaded tracking event

* Move the idempotence check to useLensLoadedTracking

* GDGT-1831 replace unstyled button with a box to get rid of button in button error

* chore: transform lens_params

* GDGT-1786: Parametric lens navigation improvement (#69821)

* GDGT-1757: Refactor Column Comparison utils grouping and scoring logic (#69853)

* Improve types with entity ids (#69891)

* add concrete type declarations and usage for InspectorLensId, InspectorCardId, InspectorSectionId

* add better type to lens params

* cleanup of utils

* improve type for displaySettings

* use entity aliases in more places

* use existing type aliases for source and target

* add comment about InspectorTriggerCondition type

* unify prefix for inspector types

* remove spurious catch

* deduplicate types between metabase-api and lib

* Remove unused query

* fix provided tags for transform inspector

* add alerts and drill buttons test for default lens sections (#69899)

* restrict join analysis to single condition joins

* fix: bad merge reoslution

* fix types in lens test

* use isa function to detect type

* use concrete table id type alias

* use Urls.openInNewTab util

* remove empty mocks file

* use concrete table id type

* move lib for transform inspector to src/transforms/lib

* remove superflous log in e2e tests

* add proper SchemaName type

* remove superflous export

* revert move of isFetchBaseQueryError

* remove obsolete test

* use correct SchemaName type alias

* use correct VisualizationDisplay type

* remove leftover unused type

* remove superflous export

* Use CardId type alias

* discern lens also by based on params when tracking loading

* GDGT-1839 addressing the PR comments

* GDGT-1839 addressing the PR comments

* GDGT-1839 addressing the PR comments

* GDGT-1839 move getLensKey and LensHandle from api/tags directory

---------

Co-authored-by: Egor Grushin <egor@metabase.com>
Co-authored-by: Jakub Chodorowicz <jakub.chodorowicz@metabase.com>
Co-authored-by: Jakub Chodorowicz <jakub@chodorowicz.com>
2026-02-23 10:16:35 +00:00
Ngoc Khuat
a68b644ec8 Add prominent CTA button per failed transform in failure email (#69458) 2026-02-23 14:51:28 +07:00
Fraser
ca002fd953 Convert app-embed and app-public to TypeScript (#69438)
* Convert `app-embed` to TypeScript

Part of the Accelerate TypeScript Migration project.

Closes DEV-1516

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 09:49:58 +13:00
Anderson
ba5de0f2fa Show Python upsell on self hosted (#69759)
* Show Python upsell on self hosted

* Add e2e tests

* Fix unit test

* Simplify TransformsUpsellPage a bit more, fix tests

* Simplify logic

* Fix e2e and unit test

* Update self-hosted python upsell as per new design / Code clean up

* Fix unit test and also add a new one for useTransformsBilling

* Remove e2e tests to be moved to a new PR

* Fix cta link and bullet points icon alignment
2026-02-20 22:42:10 +00:00
Nathan Voxland
b4f717609e Add support for OpenID Connect (#69538) 2026-02-20 16:18:58 -06:00
Oleksandr Yakushev
3f898be45a [match] Implement replace-lite and use it instead of replace (#69270) 2026-02-20 23:39:57 +02:00
Edward Paget
071fc345b4 fix: better check for dirty collection state (#69974)
Our frontend code that prevents sending unchanged collections missed
tenant collections being added to the payload. This fixes both the
frontend to make sure we don't send when there are no changed
collections and it updates the backend actually check if collections are
being changed rather than just checking if the collections map exists.
2026-02-20 13:13:27 -06:00
Timofey Kachalov
08c4a89f91 fix(EmbedJS): Downloads fail on guest embedded questions (#69972) 2026-02-20 23:04:33 +04:00
Edward Paget
7d2d6571cb fix: transforms collections set setting on import (#69852)
* fix: transforms collections set setting on import

Also turns off the transforms sync setting if there are not transform
related models in the import

* fix: transform/snippet settings on collection sync

Updates our check for 'did transforms or snippets change on remote' to
examine collections in the respective namespaces so an import with only
transform collections (or snippet collection) creates the correct
settings on the importing instance.

* fixup! better handling for custom vs builtin tags

* fixup! better conflict checking of namespaced cols

* fixup! explicit connectionb for token cache

* fixup! PR feedback
2026-02-20 10:54:57 -06:00
Ryan Laurie
f6f2a31b6a Enable automated patch releases (#69971)
enable automated patch releases
2026-02-20 16:26:26 +00:00
github-automation-metabase
1342901722 Auto-update documentation for master (#69953)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-02-20 09:53:52 -05:00
Timofey Kachalov
a554da88bd Change the way of building the base_ref embedjs bundle (#69910) 2026-02-20 15:55:19 +04:00
Mahatthana (Kelvin) Nomsawadi
7fa10713ea Support passing dataPicker to SDK components to force staged data picker (#69631)
* Adding dataPicker to embedding-data-picker redux store

* Refactor: move `dataPicker` redux

Move from interactive embedding store to embedding data picker store

* Add `dataPicker` type for the SDK

* Fix storybook id value

* Accept `dataPicker` in modular embedding SDK

* Make the doc more suitable for markdown table in the docs site

* Add test for InteractiveQuestion

* Fix drillThroughQuestionProps type

* Add tests for drillThroughQuestionProps.dataPicker

* Fix SDK components can't specify different `entityTypes` or `dataPicker`

* Fix doc error

* Fix comment grammar

* Fix wrong file name

* Clarify redux slice usage with a comment

* Make context more specific to avoid misuse

* Add `question` back to SDK's `entityTypes` on the API doc
2026-02-20 11:27:46 +00:00
Nemanja Glumac
43e3fa694d Revert "Run container as non-root, simplify H2 handling in entrypoint (#64977)" (#69958)
This reverts commit 54bd11c5b9.
2026-02-20 10:42:48 +00:00
lbrdnk
370a4dff14 Enable check_snippet endpoint (#69892)
* Enable the check_snippet endpoint

* Param

* Test

* Test
2026-02-20 10:04:58 +01:00
Uladzimir Havenchyk
dc319ca76a feat(dashboard): move subscriptions to action menu (#64950)
* feat(dashboard): move subscriptions to action menu

* fix unit tests

* fix e2e tests

* fix console warning about uncontrolled component

* fix more tests

* fix remaining test

* bring test back
2026-02-20 08:39:55 +00:00
Romeo Van Snick
b08d62e316 Transforms with params UI (#69728)
* Validate transform queries can be preprocessed

* remove prn

* update backend error messages

* Add support for custom toggleTemplateTagEditor

* Allow passing setTemplateTagConfig to TagEditorParam

* Add support for storing parameter values when question changes

* Add template-tags tab

* Add template tag sidebar

* Allow saving optional template tags or tags with a default value

* Show error message on save

* Add new spec for transform template tags

* Avoid sharing state when navigating

* Add combination test for template tag types in transforms

* Add definitionTab helper

* Add test for multiple template tags

* Fix error message

* Fix tooltips in test

* Remove tooltip check in tests

* Use getErrorMessage to get error message in toast

* Only return message if it is not empty

* Wrap error message in form

* Use H.createTestNativeQuery helper

* Move variableTemplateTags and isVariableTemplateTag to Lib

* Use more conventional key to reset uistate

* update backend error messages again

* Revert "Wrap error message in form"

This is now handled in the api

This reverts commit 9efecaf261.

* Fix test

---------

Co-authored-by: William Zimrin <william@metabase.com>
2026-02-20 13:15:02 +11:00
Alexander Polyankin
ba302fc0ef Diagnostics cleanup pulled from the metrics branch (#69926) 2026-02-20 01:15:01 +00:00
Anderson
716a26ef75 Remove Exit sidebar menu item now that app switcher exists (#69886) 2026-02-19 23:53:20 +00:00
dpsutton
f1de74d0d2 [master] security fixes (#69924)
* security fixes

* Use actions dataset

Symptom:
We started having tests failing

like:

```
expected: {:has_more_values false,
           :values [["African"] ["American"] ["Artisan"]]}
  actual: {:has_more_values false, :values (("American") ("Artisan") ("Asian"))}
    diff: - {:values [["African"] ["American"] ["Artisan"]]}
          + {:values [["American"] ["Artisan"] ["Asian"]]}
```

and

```
expected: {:has_more_values true,
           :values [["African"] ["American"] ["Artisan"] ["Asian"] ["BBQ"]]}
  actual: {:has_more_values true,
           :values (("American")
                    ("Artisan")
                    ("Asian")
                    ("BBQ")
                    ("European Shop"))}
    diff: - {:values [["African"] ["American"] ["Artisan"] ["Asian"] ["BBQ"]]}
          + {:values [["American"]
                      ["Artisan"]
                      ["Asian"]
                      ["BBQ"]
                      ["European Shop"]]}
```

Identify:
```
modified   src/metabase/actions/execution.clj
@@ -35,6 +35,10 @@
                                                               [:model_id      ::lib.schema.id/card]
                                                               [:dataset_query ::lib.schema/native-only-query]]
    request-parameters]
+  (let [db-name (-> ((requiring-resolve 'metabase.test/db)) :name)]
+    (when-not (.startsWith db-name "actions-test-data")
+      (println clojure.test/*testing-vars*)
+      #_(clojure.repl/pst (Exception. "how here?") 500)))
   (log/tracef "Executing action\n\n%s" (u/pprint-to-str action))
   (try
     (let [parameters (for [parameter (:parameters action)]
@@ -61,6 +65,10 @@
 (defn- execute-custom-action! [action request-parameters]
   (let [{action-type :type} action]
     (actions/check-actions-enabled! action)
+    (let [db-name (-> ((requiring-resolve 'metabase.test/db)) :name)]
+      (when-not (.startsWith db-name "actions-test-data")
+        (println clojure.test/*testing-vars*)
+        #_(clojure.repl/pst (Exception. "how here?") 500)))
     (let [model (t2/select-one :model/Card :id (:model_id action))]
       (when (and (= action-type :query) (not= (:database_id model) (:database_id action)))
         ;; the above check checks the db of the model. We check the db of the query action here
@@ -165,8 +173,14 @@
   (let [k (keyword (:kind action-instance))]
     (legacy->current k k)))

+(require 'clojure.repl)
+
 (defn- execute-implicit-action!
   [action request-parameters]
+  (let [db-name (-> ((requiring-resolve 'metabase.test/db)) :name)]
+    (when-not (.startsWith db-name "actions-test-data")
+      (println clojure.test/*testing-vars*)
+      #_(clojure.repl/pst (Exception. "how here?") 500)))
   (let [model-id        (:model_id action)
         implicit-action (parse-implicit-action action)
         {:keys [query row-parameters]} (build-implicit-query action implicit-action request-parameters)
```

I modified the action execution to print out test vars that weren't
using the correct dataset. A few need cross datasets to ensure can't run
on different dbs, and a few don't matter because they assert that
permissions fail or data is missing. But this
execute-action-permission-test didn't, and it's last branch asserts
"Admin can still execute actions" and that seems to have been the cause
of our troubles
2026-02-19 15:16:26 -08:00
Rodrigo López Dato
54bd11c5b9 Run container as non-root, simplify H2 handling in entrypoint (#64977)
* Run container as non-root, simplify H2 setup

* Permissions

* add to bin/Dockerfile

* Update Dockerfile

Co-authored-by: metabase-bot[bot] <109303359+metabase-bot[bot]@users.noreply.github.com>

* Update bin/docker/Dockerfile

Co-authored-by: metabase-bot[bot] <109303359+metabase-bot[bot]@users.noreply.github.com>

* bad bot :(

* docs

* bring back JAVA_TIMEZONE

* don't hardcode default h2 directory in command

* fix link

* update openapi

---------

Co-authored-by: metabase-bot[bot] <109303359+metabase-bot[bot]@users.noreply.github.com>
2026-02-19 19:25:16 -03:00
Nemanja Glumac
2e3efef978 Use core.setFailed instead of throw for missing artifacts (#69897)
When an artifact doesn't exist (expected for new commits), the
fetch-artifact action was using throw new Error() which causes
GitHub Actions to report "Unhandled error" - confusing and noisy.

Using core.setFailed() cleanly marks the step as failed without
the scary error message.

Fixes: DEV-1581

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-19 23:15:33 +01:00
Rodrigo López Dato
9d9433b5b0 docs: Fix step numbering + add details on how to confirm if using H2 (#69581)
* Fix step numbering + add details on how to confirm if using H2

* fix anchor links
2026-02-19 18:19:46 -03:00
Rodrigo López Dato
f56c4e39cb Mention that the appdb schema must always be public (#69905) 2026-02-19 18:19:16 -03:00
bryan
f3fa8226cf Add debug echo to driver-decisions CI step (#69881)
Prints the mage command and args before execution so we can quickly
see what labels/flags were passed when debugging CI driver issues.

Closes: DEV-1577
2026-02-19 21:10:20 +00:00
Rodrigo López Dato
4376ecc379 Update model index limit in docs (#69933) 2026-02-19 18:08:16 -03:00
Braden Shepherdson
2c04482d27 [MBQL lib] Move leaky column logic from models.interface into lib (#69665)
Fixes QUE2-353.

`mi/normalize-result-metadata-column` called some internals to modernize
a possibly-lib, possibly-legacy column from `:result_metadata` on a
card. Pull that logic into Lib as a new exported function.
2026-02-19 20:52:27 +00:00
bryan
be492dc637 Remove unused HTTP action code paths (#69764)
* Remove dead code for actions

* Disable HTTP action type at API level

* clarify thrown exceptions

* Remove HTTP action tests

HTTP actions are now blocked at the API level; these tests cover
functionality that's no longer accessible.

* Remove remaining HTTP action references from api_test

The previous commit missed several HTTP action usages:
- all-actions-default helper function
- list-actions-test action-1 binding
- action-parameters-test (now uses query action)
2026-02-19 20:07:24 +00:00
Anderson
b2fb97a98a Replace 'Set up git sync' with 'Set up remote sync' on Data Studio sidebar (#69883) 2026-02-19 19:11:52 +00:00
William
347f3f11b4 Transforms with parameters (#69592)
We want to support transforms with template tags, as long as those tags aren't required or have default values. However, if we do that, the backend should actually verify that we do in fact have values for all relevant template tags. This pr adds basic validation logic to transform creation and update routes -- essentially, we just run qp.preprocess/preprocess and see if we get an error.
2026-02-19 12:03:00 -06:00
Edward Paget
d59d58e61a Fix narrow symbol downloads with tests (#69811)
* Fix downloading currency values in narrowSymbol format (#69779)

* Add tests for narrowSymbol currency style in downloads

---------

Co-authored-by: Will Bryant <will.bryant@gmail.com>
2026-02-19 11:39:11 -06:00
Timofey Kachalov
c8d8369fec feat(embed-js): Add docs hint for additional wizard properties (#69906) 2026-02-19 21:14:10 +04:00
Sébastien
ecaff38ac0 improved doc generation for EAJS (#69890) 2026-02-19 17:30:20 +01:00
Nemanja Glumac
951f74665d Do not cache UV (#69902) 2026-02-19 17:18:14 +01:00
Dan Stone
acf4530cc6 Move transform metering back into EE module (#69840)
* Move transform metering back into EE module

The defenterprise shadowing was not working as the metering function was
moved

* Add test of EE stats
2026-02-19 16:05:06 +00:00
Rodrigo López Dato
8eccd8a0f9 Capitalize Jar -> JAR (#69900) 2026-02-19 11:26:31 -03:00
Nemanja Glumac
83fdafad01 Introduce Copilot code review instructions (#69846) 2026-02-19 11:14:54 +01:00
github-automation-metabase
71cfad67bb Auto-update documentation for master (#69888)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-02-18 20:43:17 -05:00
Fraser
103e9056e1 Convert QueryButton to TypeScript (#69419)
* Convert QueryButton to TypeScript (DEV-1497)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Tighten types
2026-02-19 14:15:56 +13:00
Cam Saul
984cf44eca More robust visualization settings normalization (#69860)
* More robust visualization settings normalization

* Remove stray `println`
2026-02-18 16:20:00 -08:00
Jeff Bruemmer
d2f7234d7f docs - sql table variable (#69817) 2026-02-18 17:07:30 -05:00
Timothy S. Dean
4ab6f9df02 Transforms DB Connection (#68883)
Problem: When a database is configured for both SQL access and Transforms, any feature with SQL access (like raw SQL questions) inherits the elevated permissions (CREATE, DROP, INSERT, etc.) that Transforms need.

Solution: Admins can _optionally_ configure write credentials alongside existing read credentials. One logical database, two connection types. Specific code paths opt in to using write credentials; by default everything uses a read-only connection.

How: This PR adds a new `write_data_details` column on `Database`. If `write_data_details` isn't configured, everything will still use `details` as before. When it is configured, it acts as a set of overrides on top of `details`, but only in code paths that require write permissions. There should be no behavioral change for existing deployments.
2026-02-18 13:57:16 -07:00
Edward Paget
4c0c9aa08f fix: log parameter not nil value (#68200) 2026-02-18 14:29:50 -06:00
Rodrigo López Dato
32a00c99e5 Explain how DB routing and query caching work together (#69855)
* Explain how DB routing and query caching work together

* Update docs/configuring-metabase/caching.md

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>

---------

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>
2026-02-18 19:38:19 +00:00
github-automation-metabase
559c2f27de Auto-update documentation for master (#69833)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-02-18 11:44:49 -05:00
Nemanja Glumac
fd775c440d Split long running e2e specs into smaller chunks (#69844) 2026-02-18 17:18:36 +01:00
Nemanja Glumac
4a79872533 Clean up more unneeded dependency "resolutions" (#69837) 2026-02-18 17:14:53 +01:00
Timofey Kachalov
127132f8a6 Measure Embedding SDK Hosted Bundle bundle size (#69743) 2026-02-18 19:57:09 +04:00
Nathan Voxland
140d1adcb7 Remote-Sync: Support other git providers (#69675) 2026-02-18 09:37:25 -06:00
lbrdnk
639c8debc9 Enable native entity validation (#69784)
* Enable native entity validation

* Adjust tests

* Adjust test

* Adjuts tests

* Enable check_card and check_transform for native entities

* Adjust test to check child staleness

* Adjust test

* Adjust e2e test
2026-02-18 14:23:18 +01:00
Oleksandr Yakushev
795fa21b9d Add &parents to match-lite and rewrite remaining match/match-one usages (#69259) 2026-02-18 15:04:04 +02:00
lbrdnk
2f043ea9d2 Sqlglot review follow-up (#69788)
* Review remarks (incl. .sqlglot-version removal)

* Use uv.lock

* Restore batch-lookup-table-ids

* Adjust test

* Apply suggestions from code review

Co-authored-by: bryan <bryan.maass@gmail.com>

* Add sqlglot version parsing test

---------

Co-authored-by: bryan <bryan.maass@gmail.com>
2026-02-18 13:23:29 +01:00
Dan Stone
0e113a5c68 Accept string target :type in transform test helper (#69804)
It makes sense to use strings in http tests because that is what the
http-client will accept as json.

If a string target :type is supplied, we convert it to a keyword to
ensure we can dispatch to the intended method.
2026-02-18 11:06:04 +00:00
Uladzimir Havenchyk
e3ec99133a chore: convert IconBorder component to typescript (#69735)
* chore: convert IconBorder component to typescript

* pass rest parameters

* inline component

* fixup
2026-02-18 06:19:21 +00:00
Fraser
cd8c4b1e98 Convert lib/syncing to TypeScript (#69428)
* Convert `lib/syncing` to TypeScript

Part of the Accelerate TypeScript Migration project.

Closes DEV-1507
2026-02-18 16:16:10 +13:00
Romeo Van Snick
ead4e7d4d9 Fix unreferenced dependency flakes (#69535)
* Scroll item into view

* Fix sidebar test

* Wait for page to be visible
2026-02-18 14:15:52 +11:00
Fraser
f3fe43bb86 Convert LoginHistory to TypeScript (#69412)
Convert LoginHistory to TypeScript (DEV-1491)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 14:36:39 +13:00
Fraser
e385103a21 Convert LimitPopover to TypeScript (#69076)
* Convert LimitPopover to TypeScript

Closes DEV-1421

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-18 14:10:11 +13:00
Nathan Voxland
13681a004a Improve event creation remote-sync (#69768) 2026-02-17 18:05:08 -06:00
Fraser
a8715223ae Convert embedding/saved-entity-picker/utils to TypeScript (#69442)
Part of the Accelerate TypeScript Migration project.

Closes DEV-1520

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 11:29:25 +13:00
Fraser
3cbf8919a7 Convert lib/string to TypeScript (#69424)
Part of the Accelerate TypeScript Migration project.

Closes DEV-1505

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 11:29:11 +13:00
Oleksandr Yakushev
d4702e10e0 Merge remaining match -> match-many rewrites (#69686)
* [match] Implement match-many as a replacement for match

* [match] Add proper docs to match-lite and match-many
2026-02-17 23:56:04 +02:00
Braden Shepherdson
c8cd828410 [MBQL lib] Don't export a dynamic var; add with-card-clean-hook macro (#69662)
Fixes QUE2-354.

`lib/with-card-clean-hook` can be used to bind a callback function to
any material changes made by the `lib.convert/clean` process, which
generally indicates errors.
2026-02-17 21:36:48 +00:00
Edward Paget
aa5c870dc4 fix: cancelling remote sync tasks dismisses modal (#67613)
* fix: cancelling remote sync tasks dismisses modal

Previously we didn't close this modal until the task reached a
'cancelled' state which could take several seconds.

* fixup! PR feedback
2026-02-17 15:08:18 -06:00
Kevin Kelleher
f4aa0cc722 Visualizer trend lines and bug fixes: colors, settings, legends, axes (#69296) 2026-02-17 15:54:47 -05:00
metamben
764befaffb Document :metabase.lib.field/simple-display-name (#69590)
* Document :metabase.lib.field/simple-display-name

Closes QUE2-286

* Fix merge
2026-02-17 23:08:32 +03:00
metamben
dbced47022 Name the default join strategy (#69748)
Closes QUE2-285

The original idea was making the :strategy key required and leaving the setting of the value to the normalization, but
I decided not to go down that road. Here's why: there are multiple places on the path of constructing a join where we
can specify the join strategy but don't have to. These steps are not going through normalization and introducing
normalization would be inefficient and feel strange. Also, it would need to be done at several points, so it doesn't
seem a gain in the end. Because of this, all this PR does is naming the default strategy, so that changing it should
be possible at a single point. (Doing this would be a bad idea, so this PR is meant to be a documentation
improvement.)
2026-02-17 23:08:21 +03:00
Edward Paget
224d17965d fix: allow uploads when schema is unrestricted (#69657)
* fix: allow uploads when schema is unrestricted

* fixup! add upload enabled flag

* Update impl.clj

* fixup! where upload flag is set
2026-02-17 13:26:36 -06:00
Abed Habli
0b644a038f Docs v59: Nav UI update -- Grid icon and Admin rename (#69803)
* Update gear icon

* Update docs/data-modeling/json-unfolding.md

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>

* Update docs/data-modeling/json-unfolding.md

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>

* Update docs/embedding/sdk/quickstart-with-sample-app.md

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>

* Update docs/embedding/sdk/quickstart.md

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>

* Update docs/embedding/full-app-embedding-quick-start-guide.md

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>

* Update docs/embedding/full-app-embedding-quick-start-guide.md

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>

* Update docs/people-and-groups/user-provisioning.md

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>

* Update docs/permissions/application.md

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>

* Update docs/permissions/data.md

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>

* Update docs/util/resources/introduction.md

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>

* Update docs/usage-and-performance-tools/audit.md

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>

* Apply suggestion from @jeff-bruemmer

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>

* Update docs/embedding/public-links.md

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>

* Update docs/embedding/public-links.md

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>

* Update docs/people-and-groups/api-keys.md

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>

* Update docs/people-and-groups/api-keys.md

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>

* Update docs/people-and-groups/api-keys.md

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>

* Remove few "ons"

---------

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>
2026-02-17 20:16:14 +01:00
github-automation-metabase
65c102daf2 Auto-update documentation for master (#69781)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-02-17 11:14:20 -05:00
Nathan Voxland
5da65b6c52 Un-publishing remote-sync'ed library tables should remove them from the git repo (#69757) 2026-02-17 09:57:31 -06:00
Jeff Bruemmer
5f66a26a53 docs - one-shot sql gen (#69766) 2026-02-17 15:36:16 +00:00
Jakub Chodorowicz
75db6b5973 split large data model shared spec cypress file (#69744)
* split large data model shared spec cypress file

* split 2nd group further, rename files
2026-02-17 16:31:27 +01:00
Uladzimir Havenchyk
085fc52dca chore: cleanup relationships which we never show (#69786) 2026-02-17 17:24:11 +03:00
Uladzimir Havenchyk
b404c13d0c chore: convert dashboard actions to typescript (#69737)
* chore: convert dashboard actions to typescript

* cleanup

* fix unit test

* address review

* fix typecheck
2026-02-17 17:14:08 +03:00
Uladzimir Havenchyk
727664ccc6 chore: cleanup moment leftovers (#69733) 2026-02-17 16:29:31 +03:00
Jeff Bruemmer
97fa5da0c0 docs - embedding native question (#69762)
native question
2026-02-17 08:28:01 -05:00
Kamil Mielnik
a1b8cbca83 GDGT-1781 Fix flaky "should check out transform into a new workspace from the transform page" e2e test (#69619) 2026-02-17 13:26:48 +00:00
Gabriel Moreno
e0d953e257 Implement improved illustration for in-app Library upsell (#69760)
feat: add new library upsell illustrations
2026-02-17 09:14:08 -04:00
Nemanja Glumac
f405f3d36a Remove set-value and unset-value from "resolutions" (#69787) 2026-02-17 13:10:40 +00:00
Fraser
554bb10750 Convert Heading to TypeScript (#69064)
* Convert Heading to TypeScript

Closes DEV-1409

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Delete heading and replace with title

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-18 00:20:06 +13:00
Kamil Mielnik
c51eeace1d Visualization Typescript Conversions - lib/dom.js (#69630) 2026-02-17 17:30:01 +07:00
Mahatthana (Kelvin) Nomsawadi
da01813210 Fix flaky view-and-curate-content.cy.spec.ts (#69749)
Fix flaky test
2026-02-17 10:03:49 +00:00
Fraser
2a12752ec7 Convert lib/security to TypeScript (#69422)
Part of the Accelerate TypeScript Migration project.

Closes DEV-1504

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 22:45:14 +13:00
lbrdnk
7da41b4874 Add uv to build docs and dev-install (#69727)
* Add uv to build docs and dev-install

* Update docs/developers-guide/build.md

Co-authored-by: bryan <bryan.maass@gmail.com>

* Add also python to mise.toml

---------

Co-authored-by: bryan <bryan.maass@gmail.com>
2026-02-17 09:19:35 +01:00
Kamil Mielnik
7cab7b1458 GDGT-1740 Datamodel Typescript Conversions (#69343) 2026-02-17 15:12:39 +07:00
Fraser
801720f077 Convert lib/schema_metadata to TypeScript (#69425)
* Convert `lib/schema_metadata` to TypeScript

Part of the Accelerate TypeScript Migration project.

Closes DEV-1506

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 14:03:57 +13:00
Cam Saul
bd1e91191c Map Snowflake big integers to :type/BigInteger (#67609)
* Map Snowflake big integers to :type/BigInteger (fix #63600)

* Test fixes 🔧

* Test fixes 🔧

* Test fix 🔧

* More test fixes 🔧

* Another round of test fixes 🔧

* (Hopefully) final test fix 🔧

* Code cleanup

* Revert changes that broke Clickhouse test

* Test fix 🔧
2026-02-16 17:03:13 -08:00
Fraser
d4cb2fb735 Convert Alert to TypeScript (DEV-1493) (#69415)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 13:47:08 +13:00
Rodrigo López Dato
5259af771e Fix navigate_to not doing anything in full-app embedding (#69761) 2026-02-16 23:47:11 +00:00
Romeo Van Snick
f3f72f8cb3 Fix missing availableHeight on model native editor (#69723)
* Fix missing availableHeight on model native editor

* Add repro for metabase#69722
2026-02-17 10:42:47 +11:00
Fraser
a46e4da604 Convert PermissionsTabs to TypeScript (#69080)
* Convert PermissionsTabs to TypeScript

Closes DEV-1425

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-17 12:21:15 +13:00
Fraser
4568848fb0 Convert common/hooks/use-scroll-on-mount to TypeScript (#69440)
* Convert `common/hooks/use-scroll-on-mount` to TypeScript

Part of the Accelerate TypeScript Migration project.

Closes DEV-1518

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:20:51 +13:00
Fraser
a6bf8e9326 Convert lib/encoding to TypeScript (#69413)
* Convert `lib/encoding` to TypeScript

Part of the Accelerate TypeScript Migration project.

Closes DEV-1490

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:19:35 +13:00
Kevin Kelleher
32b8ab866d Default viz behavior fixes (#69664) 2026-02-16 17:39:16 -05:00
Egor Grushin
bbf5aebd5a Gdgt 1774 incremental update field not displaying in transforms (#69740)
* GDGT-1774 fetch metadata to enable field picking in incremental transform settings

* GDGT-1774 add e2e reproduction
2026-02-16 22:01:44 +00:00
Ryan Laurie
55d4145794 Update workflow triggers (#69758)
update workflow triggers
2026-02-16 19:47:56 +00:00
Anderson
98f4633388 Add tracking events for when Dependency Graph is opened for an entity (#69604)
* Add events for transform run tags and dependency diagnostics row selection

* Fix e2e test

* Adjust event as per review / Fix e2e tests

* Add tracking events for when Dependency Graph is opened for an entity

* Add remaining e2e tests

* Update simple_event/jsonschema/1-0-0
2026-02-16 18:59:36 +00:00
Dan Stone
d987764034 Fix bigquery/snowflake transforms tests (#69566)
* Add python transforms .yml for manually verifying flakes

* Fix bigquery/snowflake failing tests

* Fix bigquery name quoting

* Use explicit schema in table gens

* Schema fixes for bigquery
2026-02-16 18:34:24 +00:00
Maksym Yakubych
7bf8c80d42 fix: double new lines are not pasted correctly into metabot prompt input (#69493) 2026-02-16 09:42:37 -06:00
Maksym Yakubych
a971f7972b feat: add @ mention to metabot placeholders (#69374) 2026-02-16 09:41:32 -06:00
Nicola Mometto
cfcba5160e remove task logs from usage analytics (#69739) 2026-02-16 14:30:56 +00:00
metamben
04e14dcee2 Remove :metabase.lib.field/original-temporal-unit and document friends (#69627)
* Remove :metabase.lib.field/original-temporal-unit and document friends

Closes QUE2-287

* Remove unused alias
2026-02-16 16:34:00 +03:00
Jeff Bruemmer
832a04aa91 docs - box plot (#69689)
* box plot docs

* Optimized 3 images, saved 221.8 KB (44.8% smaller)

* Apply suggestion from @jeff-bruemmer

---------

Co-authored-by: Metabase Automation <github-automation@metabase.com>
2026-02-16 13:24:40 +00:00
Abed Habli
c5591f5ece Docs: Add screenshots to Data Studio docs (#69634)
* Add screenshots to Data Studio docs

* Optimized 8 images, saved 757.5 KB (38.2% smaller)

* Update docs/data-studio/data-structure.md

Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>

* Delete docs/data-studio/images/sync-settings.png

---------

Co-authored-by: Metabase Automation <github-automation@metabase.com>
Co-authored-by: Jeff Bruemmer <jeff.bruemmer@gmail.com>
2026-02-16 12:07:28 +00:00
metabase-bot[bot]
bf1610f1cd [chore] Update E2E test timings with recent data (#69685)
Update E2E test timings with averaged data

Collected timing data from 7 days of test runs.
All specs with timing data were included (simple averages).

🤖 Generated automatically by update-e2e-timings workflow

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: GitHub Actions <actions@github.com>
2026-02-16 10:05:05 +00:00
Romeo Van Snick
e1813fedb5 Fix more measure unit tests (#69596)
* Rewrite test to use Lib.createTestQuery

* Force OpaqueDatasetQuery to be used

* Break cyclic imports
2026-02-16 19:51:39 +11:00
Kamil Mielnik
3339413f6e GDGT-1775 Convert Gauge Viz to Functional Component + Typescript (#69468) 2026-02-16 15:19:36 +07:00
Kamil Mielnik
2958401e6e GDGT-1772 Convert Progress Visualization to Typescript + Functional Component (#69449)
* Refactor Progress viz to TypeScript and a function component

* Always run the effect
2026-02-16 15:19:01 +07:00
Nemanja Glumac
63bae84a2d Restore 60 minute timeout for BigQuery and Snowflake (#69687) 2026-02-15 14:39:14 +01:00
Ryan Laurie
ea30b8973d Fix Circular Dependency in useGetPlan (#69672)
fix circular dependency
2026-02-14 19:53:23 +00:00
Oleksandr Yakushev
7beb2e3eb1 [match] Implement match-many as a replacement for match (#68659)
[match-lite] Add support for &match anaphor and better nil handling
2026-02-14 16:24:48 +02:00
Nemanja Glumac
1f5eef83b0 Reduce the CI test timeout to 40 minutes (#69625) 2026-02-14 05:37:44 -07:00
Anderson
8a39fdc126 Add tracking events for transform run tags update and dependency diagnostics row selection (#69531)
* Add events for transform run tags and dependency diagnostics row selection

* Fix e2e test

* Adjust event as per review / Fix e2e tests
2026-02-13 20:38:45 -03:00
Denis Berezin
c2aaa12429 Python library upsell (#69600)
* Temp

* Separate EE and OSS transforms upsell logic

* Remove upsell
2026-02-13 19:52:57 -03:00
Anderson
d38d54ec94 Prevent editing snippets from question editor when remote sync is read-only (#69147)
* Prevent snippets to be edited from question editor when remote sync is read-only

* Fix test
2026-02-13 19:28:27 -03:00
Aleksandr Lesnenko
e525135245 rework chart selection ui (#69289) 2026-02-13 17:27:45 -05:00
Kevin Kelleher
d6da6f1198 Fixes for email clients in dark mode (#69023) 2026-02-13 17:08:15 -05:00
Braden Shepherdson
4d9dc19271 [deps] Ignore "soft" refs (in :fields clauses) in deps analysis (#69198)
Fixes QUE-3044.

The QP will ignore refs in `:fields` clauses which cannot be resolved, since they're output only and not critical to the meaning of the query. This change makes the deps analysis treat these "soft" refs the same way - they are not considered to break the queries.

I added some commented tests that are now captured as part of QUE-3081 rather than part of this PR.
2026-02-13 21:23:18 +00:00
Anderson
93e5f86792 Fix docs link on incremental transforms section (#69648) 2026-02-13 21:19:48 +00:00
Denis Berezin
b57eb0d2c7 Dashboard category filter not correctly populated when using multiple datasources in one graph (#69318)
* Add repro e2e test

* Dashboard filter values when connected to card with multiple datasets

* Fix kondo
2026-02-13 23:51:23 +03:00
Ryan Laurie
c55a5d23a1 Dont show hosting upsell to pro self hosted users (#69522)
dont show hosting upsell to pro self hosted users
2026-02-13 13:19:15 -07:00
Braden Shepherdson
ce9a7027cf [MBQL lib] "Befriend" legacy-mbql module, drop some exported namespaces (#69578)
Fixes QUE2-349.

This removes several namespaces from the export list for `lib` which were only used by `legacy-mbql`.

- `metabase.lib.filter.desugar.jvm`
- `metabase.lib.hierarchy`
- `metabase.lib.schema.binning`
- `metabase.lib.schema.constraints`
- `metabase.lib.schema.middleware-options`
- `metabase.lib.schema.settings`
- `metabase.lib.schema.window`
2026-02-13 20:01:20 +00:00
Nemanja Glumac
ff9690156a Remove forced jsdom resolution (#69575) 2026-02-13 20:18:46 +01:00
github-automation-metabase
34fc6597f3 Auto-update documentation for master (#69606)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-02-13 14:17:42 -05:00
Jeff Bruemmer
6cc10beb56 docs - agent API (#69647) 2026-02-13 18:59:39 +00:00
Dan Stone
fe3b6f83d7 Ensure race free waiting for overlapping transforms (#69287)
* Ensure race free waiting for overlapping transforms

Previously transforms could contend for the is_active slot.
A common outcome (observed with two overlapping schedules) was that one transform would crash (due to the duplicate key violation).

This commit ensures a collision will fallback to waiting to try again rather than crashing (and alerting the user).
2026-02-13 17:35:22 +00:00
Nathan Voxland
45a8927bb8 Revert "Do a rollback before discard all (#69026)" (#69633)
This reverts commit 42d0d35dbd.
2026-02-13 11:01:38 -06:00
Sébastien
60b86d85a5 Fixes isSticky class not being properly applied to dashboard parameters (#69623) 2026-02-13 17:57:16 +01:00
Nemanja Glumac
9c8bf32532 Remove Claude review workflows (#69632)
[ci skip]
2026-02-13 17:56:03 +01:00
Nathan Voxland
fe325b3de7 Improve remote sync errors with missing objects (#69591) 2026-02-13 10:22:09 -06:00
bryan
07fbc6997e Use sqlglot for SQL parsing via GraalVM Polyglot (#68911)
Introduces a unified `sql-tools` API for SQL parsing with swappable backends:
- Macaw (Java, existing) — moved from `metabase.driver.sql`, behavior preserved
- SQLGlot (Python, new) — broader dialect support via GraalVM Polyglot

Architecture: callers go through `sql-tools` which dispatches to either backend
via the `MB_SQL_TOOLS_PARSER_BACKEND` setting (default: sqlglot). The old
Macaw calls (parsed-query -> ast -> field-references) are replaced
with a `sql-tools` API (e.g. `sql-tools/referenced-fields`,
`sql-tools/validate-query`).

SQLGlot runs Python inside GraalVM Polyglot contexts managed by a reusable pool
to amortize initialization cost. The Python source and SQLGlot library are
bundled into the uberjar via a dedicated build step.
2026-02-13 15:57:17 +01:00
Jeff Bruemmer
28585e4119 docs - remove auth (#69594)
remove auth
2026-02-13 15:37:07 +01:00
John Swanson
b6b77a19e7 record group membership removals in activity log (#69316)
* record group membership removals in activity log

Previously the `PUT /api/membership/:group-id/clear` and
`DELETE /api/membership/:id` endpoints did not emit group membership
deletion events.

In the long run it would be better if `t2` were the interface for
PermissionsGroupMembership, so users didn't need to call these "blessed"
functions. But for now, let's go along with the existing pattern here,
have some blessed functions for doing things, and throw exceptions if
someone does a direct `t2/delete!` on a `:model/PermissionsGroupMembership`.

* use macro for more obvious workaround
2026-02-13 05:10:11 -08:00
Timofey Kachalov
890771e40e Prepare Shoppy Sample App GH job to run synthetic monitoring tests (#69492) 2026-02-13 16:59:32 +04:00
Kamil Mielnik
3c724995fb GDGT-1760 Remove Code & BoldCode components (#69348) 2026-02-13 12:29:57 +00:00
Kamil Mielnik
3bd26a0179 GDGT-1761 Remove Grid component (#69351) 2026-02-13 18:42:14 +07:00
Nemanja Glumac
55959131e9 Remove unused Tusk GitHub workflow (#69612) 2026-02-13 11:46:16 +01:00
Nemanja Glumac
9845f0e964 Fix ci-test-config driver quarantine name translation (#69615)
The ci-test-config.json uses driver directory names (e.g., "bigquery-cloud-sdk")
but the mage code uses internal driver keywords (e.g., :bigquery). This mismatch
meant quarantined drivers like bigquery were not being recognized.

Now uses the existing driver-directory->drivers mapping to translate config names
to internal keywords, with fallback to direct keyword conversion for names that
match (like "databricks").

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-13 11:28:19 +01:00
Mahatthana (Kelvin) Nomsawadi
7efad1c105 Correct the modular embedding preview usage count (#69473)
* Correct the modular embedding preview count as :internal

* Add tests

* Fix the modular embedding preview count while not breaking alerts emails

* Fix E2E tests

* Fix wrong e2e syntax
2026-02-13 09:39:32 +00:00
Romeo Van Snick
82d5e7b7ed Native query test helper (#69402)
* Add schema for test-native-query-spec

* Implement test-native-query helper

* Add js wrapper for test-native-query

* Add ts wrapper for test-native-query

* Make common parts optional

* Fix types

* Fix temporal-unit test spec

* Handle snippet tags

* Fix temporal-unit tags

* Fix card tag spec

* Simplify schema

* Add unit tests for createNativeQuery

* Add tests for test-native-query

* Add missing options

* Fix types

* Actually resolve field ref

* Code style

* Add native-query api endpoint

* Add h.createTestNativeQuery helper

* Add test for required, widget-type and default options

* Move parsing into test-spec module as it is part of the implementation

* Add example e2e test use of H.createTestNativeQuery

* Use coercer instead of decoder to validate spec too

* Fix lint

* Move adjust to update-vals

Co-authored-by: Braden Shepherdson <braden@metabase.com>

* Match threading style

* Add test for optional template tagsfg

---------

Co-authored-by: Braden Shepherdson <braden@metabase.com>
2026-02-13 02:53:16 +00:00
Fraser
b69c4d4e26 Convert PermissionsEditorBreadcrumbs to TypeScript (#69052)
* Convert PermissionsEditorBreadcrumbs to TypeScript

Resolves DEV-1397

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-12 22:21:41 -03:00
Fraser
92157c0c06 Convert PermissionsSelectOption to TypeScript (#69054)
* Convert PermissionsSelectOption to TypeScript

Closes DEV-1399

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-12 22:20:33 -03:00
Anderson
3e5a4dc818 When instance is not on trial, still allow them to start Transforms trial if available (#69499)
* show both transforms tiers even when on trial

* Show upgrade modal in place when clicking Upgrade to Pro

* Improve comments

* Show upsell transforms trial variant when available for instance not on trial

* Fix ts

* Say 'Start trial' when trial is available
2026-02-12 22:18:13 -03:00
metamben
72699a1927 Remove :lib/model-display-name (#69579)
Closes QUE2-296
2026-02-13 02:50:46 +03:00
3120 changed files with 170968 additions and 54804 deletions

View File

@@ -12,14 +12,7 @@
## Testing
- **Run a test:** `./bin/mage run-tests namespace/test-name`
- **Run all tests in a namespace:** `./bin/mage run-tests namespace`
- **Run all tests for a module:** `./bin/mage run-tests test/metabase/notification` Because the module lives in that directory.
Note: the `./bin/mage run-tests` command accepts multiple args, so you can pass
`./bin/mage run-tests namespace/test-name namespace/other-test namespace/third-test`
to run 3 tests, or
`./bin/mage run-tests test/metabase/module1 test/metabase/module2` to run 2 modules.
Use `./bin/test-agent` to run backend tests. See the top-level CLAUDE.md for usage.
## Code Readability

View File

@@ -0,0 +1,357 @@
--b-
name: add-tracing
description: Add OpenTelemetry tracing spans to Clojure code following Metabase tracing conventions. Use when instrumenting backend code with trace coverage.
---
# Add Tracing Spans to Clojure Code
This skill helps you add OpenTelemetry (OTel) tracing spans to the Metabase backend codebase using the custom `tracing/with-span` macro.
## Reference Files
- `src/metabase/tracing/core.clj` - `with-span` macro, group registry, SDK lifecycle, `best-effort-sanitize-sql`, Pyroscope integration
- `src/metabase/task/impl.clj` - `defjob` macro that wraps Quartz jobs with root spans
- `.clj-kondo/config/modules/config.edn` - Module boundary configuration
## Module Architecture
The tracing module has a deliberately minimal API surface. **Only 2 namespaces are public** (listed in `:api` in the module config):
| Namespace | Role | Status |
|---|---|---|
| `tracing.core` | Primary API: `with-span`, groups, SDK lifecycle, Pyroscope, MDC, `best-effort-sanitize-sql` | **Public API** |
| `tracing.init` | Side-effect loader for `quartz` and `settings` | **Public API** (init convention) |
| `tracing.attributes` | `best-effort-sanitize-sql` implementation (re-exported via `tracing.core`) | Internal |
| `tracing.settings` | Setting definitions (`MB_TRACING_*` env vars) | Internal |
| `tracing.quartz` | Quartz JDBC proxy + JobListener | Internal |
**Rules:**
- Only require `[metabase.tracing.core :as tracing]` from outside the module. `tracing/best-effort-sanitize-sql` and all other public functions are available from this single namespace.
- Do not add new API namespaces. Add new public functions to `tracing.core` instead.
- Do not require internal namespaces (`tracing.attributes`, `tracing.settings`, `tracing.quartz`) from outside the module.
- `:uses :any` on the `core` module does NOT bypass the target module's `:api` check — internal namespaces are still enforced.
### Cyclic Dependency Avoidance
`tracing/core.clj` is required by many modules across the codebase. It **must NOT** compile-time require `tracing.settings`, as this creates transitive cyclic load dependencies (e.g., `settings/core -> tracing/settings -> tracing/core -> events/impl -> events/core`).
Instead, `tracing/core.clj` uses `requiring-resolve` for settings access:
```clojure
;; CORRECT — lazy runtime resolution, no compile-time dependency
((requiring-resolve 'metabase.tracing.settings/tracing-enabled))
;; WRONG — creates cyclic load dependency
(require '[metabase.tracing.settings :as settings])
(settings/tracing-enabled)
```
External library namespaces (clj-otel API, SDK, exporters) are safe to require normally — they don't participate in Metabase namespace cycles.
**Important:** `requiring-resolve` must use **literal quoted symbols**. Kondo hooks validate that `required-namespaces` are all simple symbols, so dynamic construction fails:
```clojure
;; CORRECT — literal quoted symbol
(requiring-resolve 'metabase.tracing.settings/tracing-endpoint)
;; WRONG — kondo hook rejects this: "Assert failed: (every? simple-symbol? required-namespaces)"
(requiring-resolve (symbol "metabase.tracing.settings" "tracing-endpoint"))
```
## Quick Checklist
When adding tracing spans:
- [ ] Module has `tracing` in its `:uses` set in `.clj-kondo/config/modules/config.edn`
- [ ] Added `[metabase.tracing.core :as tracing]` to ns requires (alphabetically sorted)
- [ ] Span wraps a meaningful I/O boundary (not pure computation)
- [ ] Group matches the domain (check `src/metabase/tracing/core.clj` for registered groups; add a new one if none fit)
- [ ] Span name follows dot-notation convention (`"domain.subsystem.operation"`)
- [ ] Attributes use namespaced keywords (`:search/query-length`, `:db/id`)
- [ ] No sensitive data in attributes (use `best-effort-sanitize-sql` for HoneySQL, never raw SQL)
- [ ] No new tracing namespaces created (add to `tracing.core` instead)
- [ ] No `DO_NOT_ADD_NEW_FILES_HERE.txt` violations in the target directory
- [ ] Run `clj-kondo --lint <files>` to verify 0 errors, 0 warnings
- [ ] Add or update tests in the corresponding `test/` path (see Testing section below)
- [ ] Run tests: `clojure -X:dev:test :only <test-ns>`
## The `with-span` Macro
```clojure
(tracing/with-span group span-name attrs & body)
```
- **group** - A keyword selecting which trace group this span belongs to (e.g., `:tasks`, `:sync`)
- **span-name** - A string identifying the span in traces (e.g., `"search.execute"`)
- **attrs** - A map of span attributes (e.g., `{:db/id 42}`)
- **body** - The code to execute inside the span
**When disabled:** zero overhead -- single atom deref + boolean check, body runs directly.
**When enabled:** creates OTel span AND injects `trace_id`/`span_id` into Log4j2 MDC for log-to-trace correlation.
## Trace Groups
Groups are registered in `src/metabase/tracing/core.clj`. Check that file for the current list. The general rule: **match the group to the domain, not the call site.** If code runs inside a Quartz job but is logically search work, use `:search`, not `:tasks`.
To add a new group:
```clojure
;; In src/metabase/tracing/core.clj
(register-group! :my-domain "Description of what this covers")
```
Users enable groups via `MB_TRACING_GROUPS=tasks,search,sync` (comma-separated, or `"all"`).
## Naming Conventions
### Span Names
Use dot-separated hierarchical names: `"domain.subsystem.operation"`. The domain prefix should match the group name:
```
search.execute -- `:search` group
sync.fingerprint.table -- `:sync` group
task.session-cleanup.delete -- `:tasks` group
db-app.collection-items -- `:db-app` group
```
### Attributes
Use namespaced keywords. The namespace groups related attributes:
```clojure
:db/id -- Database ID (integer)
:db/engine -- Database engine name (string)
:db/statement -- Sanitized SQL (string, via best-effort-sanitize-sql)
:search/engine -- Search engine name (string)
:search/query-length -- Query string length (integer)
:sync/table -- Table name (string)
:sync/step -- Sync step name (string)
:task/name -- Task name (string)
:http/method -- HTTP method (string)
:http/url -- Request URL (string)
```
Invent new namespaced attributes as needed (e.g., `:pulse/id`, `:transform/count`). Keep values as primitives (strings, numbers, booleans) -- no maps or collections.
## Step-by-Step: Adding a Span
### 1. Check module boundaries
Look up the module for your namespace in `.clj-kondo/config/modules/config.edn`. If `tracing` is not in the module's `:uses` set, add it (keep alphabetically sorted):
```edn
my-module
{:team "MyTeam"
:uses #{analytics config tracing util}}
```
### 2. Add the require
```clojure
(ns metabase.my-module.thing
(:require
[metabase.tracing.core :as tracing]
[metabase.util :as u]))
```
`best-effort-sanitize-sql` is available from `tracing.core` — no additional require needed.
### 3. Identify the I/O boundary
Only wrap code at **meaningful I/O boundaries**:
**DO trace:**
- External API calls (embedding APIs, metabot, webhooks)
- Database queries (both app DB and user DB)
- Network requests (HTTP calls to external services)
- Heavy batch processing (batch indexing, batch embedding)
- Top-level orchestration functions that coordinate multiple sub-operations
**DO NOT trace:**
- Pure computation (sorting, filtering, mapping)
- Simple single-row lookups (`t2/select-one :model/Setting :key k`)
- Every function in a call chain (only boundaries matter)
- Trivial operations (string formatting, hash calculations)
### 4. Wrap with `with-span`
```clojure
;; Simple span (no attributes needed)
(tracing/with-span :search "search.init-index" {}
(do-expensive-thing))
;; Span with static attributes
(tracing/with-span :sync "sync.fingerprint.table"
{:db/id (:db_id table)
:sync/table (:name table)}
(fingerprint-fields! table fields))
;; Span with computed attributes
(tracing/with-span :search "search.execute"
{:search/engine (name (:search-engine ctx))
:search/query-length (count (:search-string ctx))}
(search.engine/results ctx))
;; Span with sanitized SQL (for dynamic HoneySQL queries)
(let [hsql {:delete-from [(t2/table-name :model/Session)]
:where [:< :created_at oldest-allowed]}]
(tracing/with-span :tasks "task.session-cleanup.delete"
{:db/statement (tracing/best-effort-sanitize-sql hsql)}
(t2/query-one hsql)))
;; Sub-spans breaking a function into I/O phases
(let [embedding (tracing/with-span :search "search.semantic.embedding"
{:search.semantic/provider (:provider model)}
(get-embedding model search-string))
results (tracing/with-span :search "search.semantic.db-query" {}
(into [] xform reducible))]
(process results))
;; Per-item iteration spans
(doseq [e (search.engine/active-engines)]
(tracing/with-span :search "search.ingestion.update" {:search/engine (name e)}
(search.engine/update! e batch)))
```
### 5. Add tests
Create or update tests in the corresponding `test/` path. Follow the patterns in existing tracing tests:
- **Reference tests:** `test/metabase/tracing/quartz_test.clj`, `test/metabase/server/middleware/trace_test.clj`
- Use `tracing/init-enabled-groups!` / `tracing/shutdown-groups!` with `try`/`finally` to manage group lifecycle
- Test both enabled and disabled paths (verify zero overhead when group is off)
- Use `reify` mocks for Java interfaces (Connection, PreparedStatement, JobListener, etc.)
- Add `(set! *warn-on-reflection* true)` and type-hint proxy/reify calls to avoid reflection warnings
```clojure
(deftest my-span-enabled-test
(testing "when group is enabled, span is created"
(try
(tracing/init-enabled-groups! "my-group" "INFO")
;; ... test that span behavior occurs ...
(finally
(tracing/shutdown-groups!)))))
(deftest my-span-disabled-test
(testing "when group is disabled, code runs without tracing"
(tracing/shutdown-groups!)
;; ... test that code still works, no wrapping applied ...
))
```
### 6. Lint and run tests
```bash
# Lint modified source and test files — expect 0 errors, 0 warnings
clj-kondo --lint path/to/modified/file.clj path/to/test/file.clj
# Run tests (requires Java 21+)
clojure -X:dev:test :only my-ns.test-ns
```
Expect: all tests pass, 0 failures, 0 errors, no reflection warnings from your files.
## Sanitizing SQL for Attributes
When including SQL in span attributes, **always** use `tracing/best-effort-sanitize-sql`. This converts HoneySQL maps to parameterized SQL strings where values become `?` placeholders -- no data leaks.
```clojure
(let [hsql {:delete-from [:core_session]
:where [:< :created_at some-timestamp]}]
(tracing/with-span :tasks "task.cleanup.delete"
{:db/statement (tracing/best-effort-sanitize-sql hsql)}
(t2/query-one hsql)))
;; Trace attribute: db/statement = "DELETE FROM core_session WHERE created_at < ?"
```
**Rules:**
- Never put raw SQL strings or user-provided values in attributes
- Use `best-effort-sanitize-sql` only for app DB (HoneySQL) queries
- For external/user DB queries, trace only timing and counts, not SQL content
## Defjob and Root Spans
The `defjob` macro in `metabase.task.impl` automatically wraps every Quartz job with a `:tasks` root span:
```clojure
(task/defjob ^{DisallowConcurrentExecution true} SessionCleanup [_]
(cleanup-sessions!))
;; Automatically creates span: "task.SessionCleanup" {:task/name "SessionCleanup"}
```
You do NOT need a root span inside `defjob` bodies. Add **child spans** for I/O inside the job.
For code on plain `Thread`s (not Quartz), add the root span manually:
```clojure
(defn init! []
(tracing/with-span :search "search.task.init" {}
(search/init-index!)))
```
## What NOT to Do
### Span Usage Mistakes
```clojure
;; WRONG - pure computation, no I/O
(tracing/with-span :search "search.format-results" {}
(map format-result results))
;; WRONG - trivial single-row lookup
(tracing/with-span :db-app "db-app.get-setting" {}
(t2/select-one :model/Setting :key "my-setting"))
;; WRONG - raw SQL in attributes (data leak)
(tracing/with-span :tasks "task.cleanup" {:db/statement raw-sql-string}
(execute! raw-sql-string))
;; WRONG - wrong group (search work should use :search, not :tasks)
(tracing/with-span :tasks "search.execute" {} ...)
;; WRONG - redundant nesting (do-search already has a span)
(tracing/with-span :search "search.process" {}
(let [results (do-search ctx)]
(tracing/with-span :search "search.format" {}
(format-results results))))
```
### Architecture Mistakes
```clojure
;; WRONG - creating a new tracing namespace
(ns metabase.tracing.my-feature ...)
;; WRONG - requiring internal tracing namespaces from outside the module
(ns metabase.my-module.thing
(:require [metabase.tracing.attributes :as trace-attrs] ;; internal!
[metabase.tracing.settings :as tracing.settings] ;; internal!
[metabase.tracing.quartz :as tracing.quartz])) ;; internal!
;; WRONG - adding compile-time requires to tracing/core.clj for settings or SDK
;; This creates cyclic load dependencies because tracing/core is widely required
(ns metabase.tracing.core
(:require [metabase.tracing.settings :as settings])) ;; causes cycle!
;; WRONG - dynamic symbol construction with requiring-resolve (kondo rejects it)
(requiring-resolve (symbol "metabase.tracing.settings" "tracing-enabled"))
```
## Configuration
All settings are env-var-only (defined in `src/metabase/tracing/settings.clj`):
```bash
# Core
MB_TRACING_ENABLED=true # Enable tracing (default: false)
MB_TRACING_ENDPOINT=host:4317 # OTLP collector endpoint (default: http://localhost:4317)
MB_TRACING_GROUPS=tasks,search,sync # Comma-separated groups or "all" (default: all)
MB_TRACING_SERVICE_NAME=metabase # Service name in traces (default: hostname)
MB_TRACING_LOG_LEVEL=DEBUG # Log threshold for traced threads: TRACE/DEBUG/INFO (default: INFO)
# Batch span processor tuning
MB_TRACING_MAX_QUEUE_SIZE=2048 # Max spans queued for export; drops when full (default: 2048)
MB_TRACING_EXPORT_TIMEOUT_MS=10000 # Max wait for batch export to complete (default: 10000)
MB_TRACING_SCHEDULE_DELAY_MS=5000 # Delay between consecutive batch exports (default: 5000)
```

View File

@@ -0,0 +1,260 @@
---
name: e2e-test-create
description: >
Analyze React component source code to understand UI structure,
then generate idiomatic Cypress E2E tests following Metabase conventions.
Falls back to Playwright MCP browser exploration only when code reading
and screenshot debugging are insufficient.
disable-model-invocation: true
allowed-tools:
- Bash
- Read
- Write
- Grep
- Glob
- Skill
- "mcp__playwright__*"
---
# Code-Reading-First → Generate Cypress Tests (Metabase)
You are writing Cypress E2E tests for the **Metabase** codebase.
Before generating ANY test code, you MUST analyze React component source code
to understand DOM structure, selectors, and user flows.
## Phase 0 — Research
1. Read existing helpers before writing anything:
- `e2e/support/helpers/` — all shared helpers (restore, signInAs, openOrdersTable, etc.)
- `e2e/support/cypress_sample_database.ts` — table/field schema constants (ORDERS, PRODUCTS, etc.)
- `e2e/support/cypress_sample_instance_data.ts` — instance-specific IDs (ORDERS_DASHBOARD_ID, NORMAL_USER_ID, etc.)
2. Glob `e2e/test/scenarios/` to find the closest existing spec to the area under test.
Study its patterns — match them exactly.
3. Glob `frontend/src/metabase/` to find React components for the feature area.
## Phase 1 — Code Analysis
Read React component source to understand DOM structure. No browser needed — source code has everything.
1. **Find relevant components**: Glob and grep `frontend/src/metabase/` for the feature area.
2. **Extract selectors**: Grep for `data-testid` in relevant components.
3. **Note visible text**: Read component JSX for button labels, headings, placeholders.
4. **Note aria attributes**: Grep for `aria-label` in relevant components.
5. **Understand user flows**: Read event handlers (onClick, onSubmit, onChange) to understand interactions.
6. **Find API calls**: Grep for `Api.use`, `fetch`, `useQuery`, endpoint definitions to identify API calls to intercept.
7. **Cross-reference with existing specs**: Find specs in the same area and reuse their proven selectors and `cy.intercept` patterns.
## Phase 2 — Start Backend
Use `MB_EDITION=oss` by default. Only use `MB_EDITION=ee` when the user explicitly asks to write an enterprise test.
Start the backend using `run_in_background: true` (NOT `&`).
`bin/e2e-backend` automatically detects if a backend is already running and reuses it.
```bash
MB_EDITION=oss bin/e2e-backend
```
Do NOT manually generate snapshots by running unrelated test specs.
The `bun test-cypress` runner has `GENERATE_SNAPSHOTS: true` by default and automatically
generates snapshots before running any spec. When running tests in Phase 4 via the `/e2e-test` skill,
snapshots will be generated on the first run if they don't already exist.
Restore clean test data:
```bash
curl -sf -X POST http://localhost:4000/api/testing/restore/default
```
## Phase 3 — Generate Cypress Spec
### File location & naming
Place specs in `e2e/test/scenarios/<area>/` mirroring the URL structure.
Name: `<feature>.cy.spec.js` (NOT `.cy.spec.ts`).
### Metabase conventions (mandatory)
- **Helpers via `cy.H`**: All helpers are accessed via `const { H } = cy;` — NOT via direct imports from `e2e/support/helpers`.
```js
const { H } = cy;
describe("feature name", () => {
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
});
});
```
- **Sample Database schema** (table/field definitions): Import from `cypress_sample_database`.
```js
import { ORDERS, ORDERS_ID, PRODUCTS } from "e2e/support/cypress_sample_database";
```
- **Instance data** (dashboard IDs, user IDs, etc.): Import from `cypress_sample_instance_data`.
```js
import { ORDERS_DASHBOARD_ID } from "e2e/support/cypress_sample_instance_data";
```
- **Selectors** (priority order):
1. `cy.findByText()` / `cy.findByLabelText()` / `cy.findByRole()` — from `@testing-library/cypress`
2. `cy.findByTestId()` — for `data-testid` attributes
3. `cy.get("[data-testid='...']")` — fallback
4. NEVER use positional selectors, CSS class names, or XPaths.
- **Navigation helpers**: Use existing helpers like `H.openOrdersTable()`, `H.openNativeEditor()`,
`H.visitDashboard(id)`, `H.visitQuestion(id)` instead of raw `cy.visit()` chains.
Grep `e2e/support/helpers/` to discover what's available for your area.
- **API setup over UI setup**: Use `cy.request()` or existing API helpers to set up state.
Only use the UI for the flow you're actually testing.
- **Assertions**: Assert on visible text, URL, aria state — not DOM structure.
- **Waits**: Never use `cy.wait(ms)`. Use `cy.intercept()` + `cy.wait("@alias")` for API calls,
or `cy.findByText().should("be.visible")` for DOM readiness.
- **Isolation**: Each `it()` block must be independently runnable.
Don't depend on state from a previous `it()`.
### Spec structure template
```js
const { H } = cy;
import { ORDERS_DASHBOARD_ID } from "e2e/support/cypress_sample_instance_data";
import { ORDERS, ORDERS_ID } from "e2e/support/cypress_sample_database";
describe("area > sub-area > feature (#issue-number)", () => {
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
});
it("should do the primary happy-path thing", () => {
// test
});
it("should handle the edge case", () => {
// test
});
});
```
### Intercepts
When you identified API calls during code analysis, stub or wait on them:
```js
cy.intercept("POST", "/api/dataset").as("dataset");
// ... trigger action ...
cy.wait("@dataset");
```
## Phase 4 — Validate
After generating specs:
1. Check that all imported helpers exist (Grep `e2e/support/helpers/`).
2. **You MUST use the `/e2e-test` skill** to run tests — do NOT run `bun test-cypress` directly.
The `/e2e-test` skill handles edition selection, snapshot management, and correct env vars.
`/e2e-test GREP="should do the thing" --spec e2e/test/scenarios/<path>`
If you created multiple `it()` blocks, run each one individually to isolate failures.
## Phase 5 — Fix Failures (up to 2 attempts)
When a test fails, **try to fix it from Cypress output first**:
1. Read the failure screenshot (path printed under `(Screenshots)`).
2. Read the error message and code frame from the console output.
3. Fix the test and re-run (back to Phase 4, step 2).
If you cannot diagnose the issue after 2 attempts, proceed to Phase 6.
## Phase 6 — Playwright Fallback
Only reach this phase after 2 failed fix attempts from Phase 5. The backend is already running.
Restore clean test data:
```bash
curl -sf -X POST http://localhost:4000/api/testing/restore/default
```
**Bypass CSP** headers before navigating (Metabase serves strict CSP that blocks dev server scripts).
Use `browser_run_code` to set this up:
```js
async (page) => {
// Strip CSP headers so the page loads (mirrors Cypress chromeWebSecurity: false)
await page.context().route('**/*', async (route) => {
const response = await route.fetch();
const headers = { ...response.headers() };
delete headers['content-security-policy'];
delete headers['content-security-policy-report-only'];
await route.fulfill({ response, headers });
});
// Sign in via API
const response = await page.request.post('http://localhost:4000/api/session', {
data: { username: 'admin@metabase.test', password: '12341234' }
});
const session = await response.json();
await page.context().addCookies([{
name: 'metabase.DEVICE',
value: session.id,
domain: 'localhost',
path: '/'
}]);
await page.goto('http://localhost:4000');
await page.waitForLoadState('networkidle');
return 'signed in';
}
```
**Maintain an observation log incrementally.** After EVERY significant Playwright interaction,
IMMEDIATELY append what you observed to the scratch file BEFORE performing the next interaction:
```bash
cat >> /tmp/e2e-observations.md << 'OBSERVATION'
## [Page/Flow name]
- URL: /question/notebook#...
- Clicked: "Box plot" button → visible text "Box plot", role: radio
- Selectors: data-testid="viz-type-button", findByText("Box plot")
- API call: POST /api/dataset (triggered on viz change)
- Key state: after selecting viz type, summary sidebar shows metric picker
OBSERVATION
```
For each page/flow:
- Take an accessibility snapshot (`browser_snapshot`).
- Click through interactive elements, fill forms, trigger modals.
- **Append to the observation log immediately after each step**: URLs, visible text, aria labels, `data-testid` attrs, API calls.
- Screenshot key states.
After exploration:
1. Read back your observation log: `cat /tmp/e2e-observations.md`
2. Fix the test using observed selectors and behavior.
3. Re-run the test (back to Phase 4).
4. Clean up: `rm -f /tmp/e2e-observations.md`
## Phase 7 — Cleanup
After all tests pass (or after giving up on fixing failures), **always kill the backend on port 4000**:
```bash
lsof -ti:4000 | xargs kill 2>/dev/null || true
```
Do NOT use broad `pkill` patterns — there may be other Metabase instances on different ports.
The backend process started in Phase 2 will NOT be killed automatically when the Claude session ends.
Leaving it running wastes resources and can interfere with future sessions. Always clean up.
## What NOT to do
- Do NOT use Playwright as the first step — always analyze source code first.
- Do NOT kill the backend between phases — it stays running throughout.
- Do NOT invent selectors you didn't find in source code or observe in the browser.
- Do NOT hardcode database IDs — import from `cypress_sample_database` or `cypress_sample_instance_data`.
- Do NOT use `cy.wait(1000)` or any numeric wait.
- Do NOT create setup flows through the UI when an API helper exists.
- Do NOT put tests in `cypress/e2e/` — Metabase uses `e2e/test/scenarios/`.
- Do NOT import helpers directly from `e2e/support/helpers` — use `const { H } = cy;`.

View File

@@ -0,0 +1,78 @@
---
name: e2e-test
description: Run Cypress E2E tests, analyze failures including screenshots, and stress test for flakiness
disable-model-invocation: false
---
Run Cypress E2E test: $ARGUMENTS
## Edition selection
**Default: `MB_EDITION=oss`** — runs without enterprise tokens, simpler and faster.
Only use `MB_EDITION=ee` when the user explicitly asks to write or run an enterprise test.
**For `MB_EDITION=ee`:**
These tokens must be exported in the shell before starting Claude Code:
- `CYPRESS_MB_ALL_FEATURES_TOKEN` — all features (most EE tests need this)
- `CYPRESS_MB_PRO_SELF_HOSTED_TOKEN` — pro self-hosted (permissions, SSO, sandboxing)
- `CYPRESS_MB_STARTER_CLOUD_TOKEN` — starter plan (rarely needed)
- `CYPRESS_MB_PRO_CLOUD_TOKEN` — pro cloud (rarely needed)
**NEVER echo, print, or log token values. Only check if they are set:**
```bash
echo "ALL_FEATURES: ${CYPRESS_MB_ALL_FEATURES_TOKEN:+set}" && echo "PRO_SELF_HOSTED: ${CYPRESS_MB_PRO_SELF_HOSTED_TOKEN:+set}" && echo "STARTER: ${CYPRESS_MB_STARTER_CLOUD_TOKEN:+set}" && echo "PRO_CLOUD: ${CYPRESS_MB_PRO_CLOUD_TOKEN:+set}"
```
If tokens are missing, tell the user: "EE tokens are not set. Either export them and restart Claude Code, or add `MB_EDITION=oss` to run OSS-only tests."
## Running
First check if snapshots exist:
```bash
ls e2e/snapshots/default.sql 2>/dev/null && echo "snapshots exist" || echo "snapshots missing"
```
If snapshots exist, skip regeneration for speed:
```bash
MB_EDITION=oss CYPRESS_VIDEO=false CYPRESS_RETRIES=0 CYPRESS_GUI=false GENERATE_SNAPSHOTS=false bun test-cypress $ARGUMENTS
```
If snapshots are missing (first run), let the runner generate them:
```bash
MB_EDITION=oss CYPRESS_VIDEO=false CYPRESS_RETRIES=0 CYPRESS_GUI=false bun test-cypress $ARGUMENTS
```
When running enterprise tests, replace `MB_EDITION=oss` with `MB_EDITION=ee` in the commands above.
If `$ARGUMENTS` is empty, ask the user which spec to run.
If the user provides a test name or fuzzy name instead of a spec path, find the spec file:
```bash
find e2e/test/scenarios -name "*FUZZY_NAME*.cy.spec.*" -type f
```
Then run with `--spec "path/to/matched.cy.spec.js"`.
To filter by test name, use the `GREP` env var (not `--env grep=`, which breaks on commas):
```bash
GREP="test name here" MB_EDITION=oss bun test-cypress --spec path/to/spec.js
```
## Stress test (flaky detection)
When the user asks to verify a test is not flaky:
```bash
MB_EDITION=oss bin/e2e-stress-test $ARGUMENTS
```
Set `E2E_STRESS_RUNS=N` for more iterations (default: 5). Stops on first failure and prints screenshot paths. Read those screenshots to analyze the failure.
## Analyzing failures
When tests fail:
1. Read the **screenshot** for each failure — paths are printed in Cypress output under `(Screenshots)`, typically at `cypress/screenshots/<spec-name>/<test-name> (failed).png`
2. Read the error message and code frame from the console output
3. Identify the root cause: is it a test bug, a product bug, or a timing issue?
4. Suggest a fix with specific code changes

View File

@@ -5,227 +5,110 @@ description: Run mutation testing on a Clojure namespace, generate tests to kill
# Mutation Testing Skill
This skill runs mutation testing on a target namespace, writes tests to kill surviving mutations, and creates draft PRs with Linear issues for each function.
This skill runs mutation testing end-to-end: generates a coverage report, groups functions, shells out to `claude -p` to write tests, verifies mutations are killed, and creates draft PRs with Linear issues.
## Prerequisites
- A running nREPL connected to the Metabase dev environment
- `LINEAR_API_KEY` environment variable set with a valid Linear personal API key
- `gh` CLI authenticated with GitHub
- `claude` CLI available on PATH
## Reference Files
- `dev/src/dev/coverage.clj` — mutation testing tool (generates reports, runs mutations)
- `dev/src/dev/mutation_testing.clj` — Linear API client and PR template helpers
- `mutation-testing-report.lib.card.md` — example report from the pilot
- `dev/src/dev/coverage.clj` — mutation testing engine (generates reports, runs mutations)
- `dev/src/dev/mutation_testing.clj` — orchestration, Linear API, PR helpers
## Invocation
The argument is a Clojure namespace, e.g.:
The argument is a Clojure namespace, optionally followed by a base branch:
```
/mutation-testing metabase.lib.order-by
/mutation-testing metabase.lib.order-by --base-branch release-x.52.x
/mutation-testing metabase.lib.order-by --project-id abc-123
/mutation-testing metabase.lib.order-by --base-branch release-x.52.x --project-id abc-123
```
## Workflow
- `--base-branch`: defaults to the value in config (set via `set-config!`), or `"master"` if not configured.
- `--project-id`: if provided, issues are added to this existing Linear project instead of creating a new one.
### Step 1: Parse the namespace
## Steps
From the argument (e.g., `metabase.lib.order-by`), derive:
- **Test namespace**: append `-test` (e.g., `metabase.lib.order-by-test`)
- **Short name**: the last segment (e.g., `order-by`)
- **Source path**: `src/metabase/lib/order_by.cljc` (replace `-` with `_` in path segments)
- **Test path**: `test/metabase/lib/order_by_test.cljc`
### 1. Parse arguments
### Step 2: Set up Linear and load tools
Parse `$ARGUMENTS` to extract:
- **namespace** (required) — the first positional argument
- **`--base-branch`** (optional) — if present, the next argument is the base branch name
- **`--project-id`** (optional) — if present, the next argument is an existing Linear project ID
### 2. Load and configure
```clojure
(require '[dev.coverage :as cov] :reload)
(require '[dev.mutation-testing :as mut-test] :reload)
(require '[dev.coverage :as cov] :reload)
```
If this is the first invocation (or the REPL was restarted), set up the Linear team:
```clojure
;; Find the team
(mut-test/list-teams!)
;; Set team ID from the output
(mut-test/set-config! {:team-id "<team-id>"})
(mut-test/set-config! {:team-id "<id>"})
```
If config is already set from a previous invocation, skip this.
### Step 3: Generate baseline report and create Linear project
### 3. Run
```clojure
(cov/generate-report
'<target-ns>
['<test-ns>]
"mutation-testing-report.lib.<short-name>.before.md")
(mut-test/run! '<target-ns>)
;; Or with options:
(mut-test/run! '<target-ns> {:base-branch "release-x.52.x"})
(mut-test/run! '<target-ns> {:project-id "abc-123"})
(mut-test/run! '<target-ns> {:base-branch "release-x.52.x" :project-id "abc-123"})
```
Read the generated report file. It has three sections:
- **Uncovered Functions** — never called by any test
- **Partially Covered Functions** — called but with surviving mutations
- **Fully Covered Functions** — all mutations killed
Pass the opts map with `:base-branch` and/or `:project-id` if those flags were provided in the arguments.
Then create a Linear project for this namespace (uses report stats in the description):
This will:
1. Generate a baseline mutation testing report
2. Create a Linear project for the namespace
3. Group functions by coverage relationships
4. For each group: create branch → invoke Claude to write tests → verify → retry if needed → commit & push → create Linear issue → create draft PR
5. Print a summary with PR links
### 4. Handle failures
If a group fails mid-processing, `run!` catches the error and continues to the next group. To retry a single group manually:
```clojure
(mut-test/create-project-for-namespace!
"<target-ns>"
"mutation-testing-report.lib.<short-name>.before.md")
;; => {:project-id "...", :name "Mutation Testing: metabase.lib.order-by"}
;; Automatically sets :project-id in config for subsequent create-issue! calls
;; Get the parsed namespace info
(def parsed (mut-test/parse-namespace '<target-ns>))
;; Run coverage to get the data
(def coverage-results (cov/test-namespace (:target-ns parsed) [(:test-ns parsed)]))
;; Group and find the one you want
(def groups (mut-test/group-functions coverage-results))
;; Process just that group
(mut-test/process-group! parsed (nth groups <index>))
```
### Step 4: Plan the work
### 5. Verify the prompt (dry run)
Before creating branches, analyze the report to plan how to group the work:
1. **Identify killable functions.** For each function with surviving mutations, assess whether any mutations are killable. Run `cov/test-mutations` to see the full mutation list. If ALL mutations for a function are unkillable (schema-only, semantically equivalent, etc.), skip that function — no branch or PR needed.
2. **Group private functions by public entry point.** Private functions cannot be tested directly. Group them with the public function that exercises them. Create one branch/PR per group, not per private function.
3. **Track mutations per group.** The `:mutations-before` and `:not-killed` fields in the PR should only reflect mutations for functions covered by that specific PR — never mutations from other functions.
### Step 5: Process each group
For each group of functions that has killable mutations:
#### 5a. Create a branch
To inspect what Claude will see without invoking it:
```clojure
(mut-test/create-branch! "<target-ns>" "<primary-fn-name>")
;; => "mutation-testing-lib-order-by-orderable-columns"
(def parsed (mut-test/parse-namespace '<target-ns>))
(def coverage-results (cov/test-namespace (:target-ns parsed) [(:test-ns parsed)]))
(def groups (mut-test/group-functions coverage-results))
(println (mut-test/build-test-prompt
(merge (select-keys parsed [:target-ns :test-ns :source-path :test-path])
(select-keys (first groups) [:fn-names :mutations]))))
```
Use the primary public function name for the branch. If the group covers private functions too, name the branch after the public entry point.
#### 5b. Read context
- Read the source function(s) from the source file
- Read the full test file to understand existing test patterns, helpers, and metadata providers used
- If the function is private, identify which public function(s) call it
#### 5c. Write tests
Generate the **simplest tests** that kill the surviving mutations while still making semantic sense. Guidelines:
- **Never call private functions directly.** Always test through public API functions. Use the coverage data to identify which public functions exercise the private function.
- **Keep tests simple.** Each test should verify one meaningful behavior. Don't over-engineer tests just to chase mutation kills.
- **Follow existing patterns.** Match the style of the existing tests: same metadata providers, same helper functions, same assertion patterns.
- **Insert tests near related existing tests** for the same function, not at the end of the file. This minimizes merge conflicts between branches. If there are no existing tests for the function, find the most logical location based on the order of functions in the source file.
**IMPORTANT: Use `file_edit` (not `clojure_edit`) when editing test files.** The `clojure_edit` tool reformats the entire file, introducing unnecessary whitespace changes that pollute the diff. Use `file_edit` with exact string matching to make surgical insertions that only touch the lines you intend to change.
#### 5d. Verify mutations are killed
Use the REPL to check that the new tests kill the targeted mutations:
```clojure
;; Reload the test namespace to pick up new tests
(require '<test-ns> :reload)
;; Test mutations for the specific function
(cov/test-mutations
'<target-ns>/<fn-name>
;; Set of test names that cover this function
#{'<test-ns>/<test-name-1> '<test-ns>/<test-name-2>})
```
Check the `:survived` key in the result. If mutations still survive:
- Try writing additional tests
- If a mutation is truly unkillable (semantically equivalent), note it for the PR description
- If a mutation reveals dead/unreachable code, consider suggesting removal as a code improvement
#### 5e. Handle unkillable mutations and improvements
**Do NOT edit the source namespace just for documentation.** Instead:
- **Unkillable mutations**: Note them in the PR description with rationale
- **Code improvements** (dead code removal, expression simplification) that directly relate to surviving mutants:
1. Make the change in the source file
2. Verify all tests still pass with the change
3. Note the file path, line range, and the improved code
4. **Reset the change**: `git checkout -- <source-file>` — do NOT commit it
5. After creating the PR, use `add-suggested-change!` to post the improvement as a GitHub suggested change (Step 5h)
6. The reviewer decides whether to accept it
#### 5f. Commit and push
```clojure
;; Only commit the test file — never commit source changes for improvements
(mut-test/commit-and-push! "<target-ns>" "<primary-fn-name>"
["test/metabase/lib/<short_name>_test.cljc"])
```
#### 5g. Create Linear issue and draft PR
```clojure
;; Create a Linear issue (use the primary function name)
(def issue (mut-test/create-issue-for-function! "<target-ns>" "<primary-fn-name>"))
;; => {:identifier "QUE-1234", :url "https://linear.app/...", ...}
;; Create draft PR linked to the Linear issue
;; :fn-names lists ALL functions covered by this PR
;; :mutations-before counts only mutations for functions in this PR
;; :not-killed lists only unkillable mutations for functions in this PR
(def pr-url
(mut-test/create-draft-pr!
{:target-ns "<target-ns>"
:fn-name "<primary-fn-name>"
:linear-identifier (:identifier issue)
:mutations-before 9 ;; surviving mutations for these functions only
:tests-added 2 ;; number of new test functions
:killed ["Replace :segment-id with :segment-id__"
"Replace cycle-path with nil"]
:not-killed [{:description "Replace acc with nil"
:rationale "reduce always has single iteration"}]
:suggested-changes []}))
;; => "https://github.com/metabase/metabase/pull/12345"
```
#### 5h. Add code improvement suggestions (if any)
If you identified code improvements in step 5e (and reset them), post each as a GitHub suggested change:
```clojure
(mut-test/add-suggested-change!
{:pr-url pr-url
:path "src/metabase/lib/<short_name>.cljc"
:start-line 42 ;; first line of the code to replace
:end-line 45 ;; last line of the code to replace
:suggestion "(improved-code-here)" ;; the replacement code
:comment "**Suggested improvement:** <description of the change and why it's related to the surviving mutant>"})
```
### Step 6: Return to master and repeat
```clojure
(mut-test/return-to-master!)
```
Repeat Step 5 for the next group.
### Step 7: Summary
Print a summary of what was done:
- Link to the Linear project for this namespace
- Number of functions processed
- Number of functions skipped (all mutations unkillable)
- Number of draft PRs created
- Number of mutations killed vs. unkillable
- List of test PRs with links
- List of mutation rule PRs with links (if any were created)
## Suggesting New Mutation Rules
If you notice patterns in surviving mutations that suggest the mutation testing tool should have additional rules (e.g., a common Clojure form that isn't being mutated), open a separate PR proposing additions to `dev.coverage/mutation-rules` with an explanation.
## Configuration
The `dev.mutation-testing` namespace reads `LINEAR_API_KEY` from the environment and stores team/project IDs in an atom.
- **Team ID**: Set once per REPL session via `(mut-test/list-teams!)` and `(mut-test/set-config! {:team-id "..."})` (Step 2)
- **Project ID**: Set automatically when `create-project-for-namespace!` is called (Step 3) — one project per namespace
- **Team ID**: Set once per REPL session via `(mut-test/list-teams!)` and `(mut-test/set-config! {:team-id "..."})`
- **Project ID**: Set automatically when `run!` calls `create-project-for-namespace!`
- **Base Branch**: Defaults to `"master"`. Override via `(mut-test/set-config! {:base-branch "release-x.52.x"})` or pass `{:base-branch "..."}` as the second arg to `run!`
- **Project ID**: If set (via `set-config!` or `run!` opts), `run!` reuses the existing Linear project instead of creating a new one

View File

@@ -12,8 +12,9 @@ allowed-tools: Read, Grep, Bash, Glob
Review pull requests with a focus on:
- Readability and maintainability
- Appropriate test coverage
- Compliance with project coding standards and conventions
- Code quality and best practices
- Clear and correct JSDoc comments
- Type safety and proper TypeScript usage
- React best practices (when applicable)
- React best practices (when applicable)

View File

@@ -227,7 +227,9 @@
;;
:invalid-arity {:skip-args [metabase.lib.util.match/match
metabase.lib.util.match/match-lite]} ; TODO (cam): can we fix these?
metabase.lib.util.match/match-lite
metabase.lib.util.match/match-many
metabase.lib.util.match/replace-lite]} ; TODO (cam): can we fix these?
:refer-all {:level :warning, :exclude [clojure.test]}
:unused-referred-var {:exclude {compojure.core [GET DELETE POST PUT]}}
@@ -246,12 +248,17 @@
(metabase.lib.util.match/match)
(metabase.lib.util.match/match-one)
(metabase.lib.util.match/match-lite)
(metabase.lib.util.match/match-many)
(metabase.lib.util.match/replace)
(metabase.lib.util.match/replace-in)
(metabase.lib.util.match/replace-lite)
(metabase.driver-api.core/match)
(metabase.driver-api.core/match-one)
(metabase.driver-api.core/match-lite)
(metabase.driver-api.core/match-many)
(metabase.driver-api.core/replace)
(metabase.driver-api.core/replace-in)
(metabase.driver-api.core/replace-lite)
(metabase.query-processor.middleware.cache-backend.interface/with-cached-results)
(metabase.util.regex/rx [opt])
(metabase.util/prog1 [<>])
@@ -303,11 +310,14 @@
metabase.lib.convert/->legacy-MBQL {:message "Conversion to legacy MBQL is discouraged, please use Lib to manipulate MBQL queries"}
metabase.lib.core/->legacy-MBQL {:message "Conversion to legacy MBQL is discouraged, please use Lib to manipulate MBQL queries"}
metabase.lib.equality/find-column-indexes-for-refs {:message "Use lib.equality/closest-matches-in-metadata or lib.equality/find-closest-matches-for-refs instead of lib.equality/find-column-indexes-for-refs"}
metabase.lib.util.match/match {:message "Use lib.util.match/match-many instead of lib.util.match/match."}
metabase.lib.util.match/match-one {:message "Use lib.util.match/match-lite instead of lib.util.match/match-one."}
metabase.lib.util.match/replace {:message "Use lib.util.match/replace-lite instead of lib.util.match/replace."}
metabase-enterprise.transforms.interface/execute! {:message "Use metabase-enterprise.transforms.execute/execute! instead of metabase-enterprise.transforms.interface/execute!"}
next.jdbc/active-tx? {:message "Use metabase.app-db.core/in-transaction? to see if we are in a transaction"}
toucan2.core/debug {:message "Don't commit code using t2/debug"}
toucan2.tools.debug/debug {:message "Don't commit code using t2/debug"}
macaw.core/parsed-query {:message "Use metabase.driver.util/parsed-query instead of macaw.core/parsed-query"}}
}
:discouraged-namespace
{camel-snake-kebab.core {:message "CSK is not Turkish-safe, use the versions in metabase.util instead."}
@@ -412,6 +422,8 @@
ring.util.codec codec
ring.util.response response
saml20-clj.core saml
metabase.transforms-base.util transforms-base.u
metabase.transforms.util transforms.u
toucan2.core t2}}}
:lint-as

View File

@@ -133,7 +133,7 @@
version}}
analyze
{:team "Querying"
{:team "Graphy"
:api #{metabase.analyze.core
metabase.analyze.query-results}
:uses #{config
@@ -152,6 +152,7 @@
metabase.api.docs
metabase.api.init
metabase.api.macros
metabase.api.macros.scope
metabase.api.open-api
metabase.api.response
metabase.api.routes.common
@@ -214,6 +215,7 @@
settings
task
task-history
tracing
util}}
auth-identity
@@ -275,6 +277,7 @@
config
events
models
permissions
premium-features
request
settings
@@ -395,6 +398,7 @@
request
revisions
settings
tracing
transforms
upload
util}}
@@ -547,6 +551,8 @@
metabase.driver.common
metabase.driver.common.parameters
metabase.driver.common.parameters.parse
metabase.driver.connection
metabase.driver.connection.workspaces
metabase.driver.ddl.interface
metabase.driver.h2
metabase.driver.impl
@@ -555,6 +561,7 @@
metabase.driver.postgres
metabase.driver.settings
metabase.driver.sql
metabase.driver.sql-jdbc
metabase.driver.sql-jdbc.connection
metabase.driver.sql-jdbc.execute
metabase.driver.sql-jdbc.execute.diagnostic
@@ -568,7 +575,8 @@
metabase.driver.sql.util
metabase.driver.sync
metabase.driver.util}
:uses #{app-db
:uses #{analytics
app-db
appearance
auth-provider
classloader
@@ -581,7 +589,9 @@
premium-features
query-processor
settings
sql-tools
system
tracing
util
warehouse-schema
warehouses}}
@@ -660,7 +670,8 @@
:api #{metabase.events.core
metabase.events.init}
:uses #{models
util}}
util}
:friends #{tracing}}
formatter
{:team "UX West"
@@ -689,7 +700,7 @@
util}}
graph
{:team "Querying"
{:team "Graphy"
:api #{metabase.graph.core}
:uses #{util}}
@@ -731,32 +742,18 @@
lib
{:team "Querying"
:api #{metabase.lib.binning
metabase.lib.convert ; Only so [[metabase.queries.models.card]] can set dynamic vars.
metabase.lib.core
:api #{metabase.lib.core
metabase.lib.equality
metabase.lib.field
metabase.lib.filter.desugar.jvm ; only so [[metabase.legacy-mbql.jvm-util]] can use defined regexes
metabase.lib.hierarchy
metabase.lib.init
metabase.lib.metadata
metabase.lib.metadata.cached-provider
metabase.lib.metadata.column
metabase.lib.metadata.invocation-tracker
metabase.lib.metadata.protocols
metabase.lib.metadata.result-metadata
metabase.lib.normalize
metabase.lib.options
metabase.lib.query
metabase.lib.schema
metabase.lib.schema.actions
metabase.lib.schema.aggregation
metabase.lib.schema.binning
metabase.lib.schema.common
metabase.lib.schema.constraints
metabase.lib.schema.expression
metabase.lib.schema.expression.temporal
metabase.lib.schema.expression.window
metabase.lib.schema.filter
metabase.lib.schema.id
metabase.lib.schema.info
metabase.lib.schema.join
@@ -765,26 +762,27 @@
metabase.lib.schema.measure
metabase.lib.schema.metadata
metabase.lib.schema.metadata.fingerprint
metabase.lib.schema.middleware-options
metabase.lib.schema.parameter
metabase.lib.schema.ref
metabase.lib.schema.settings
metabase.lib.schema.template-tag
metabase.lib.schema.temporal-bucketing
metabase.lib.schema.test-spec
metabase.lib.schema.util
metabase.lib.schema.validate
metabase.lib.temporal-bucket
metabase.lib.types.isa
metabase.lib.util
metabase.lib.util.match
metabase.lib.walk}
metabase.lib.util.match}
:uses #{config
graph
legacy-mbql
types
util}
:friends #{query-processor}}
:friends #{legacy-mbql ;; Reuses internal schemas and a few helper functions. Deeply coupled to MBQL.
lib-be ;; BE-specific extension of lib; tight coupling is acceptable.
query-processor ;; Necessarily manipulates MBQL; better to pull most logic into a new module.
source-swap ;; Manipulates MBQL queries; tight coupling with lib is expected.
enterprise/sandbox ;; Really part of the QP, though its lives in the enterprise/ tree.
}}
lib-be
{:team "Querying"
@@ -795,6 +793,16 @@
settings
util}}
lib-metric
{:team "UX West"
:api #{metabase.lib-metric.core
metabase.lib-metric.schema}
:uses #{lib
lib-be
settings
types
util}}
llm
{:team "Metabot"
:api #{metabase.llm.api
@@ -808,6 +816,7 @@
premium-features
request
settings
sql-tools
sync
util
warehouse-schema}}
@@ -843,12 +852,29 @@
events
lib
lib-be
metrics
models
permissions
remote-sync
search
util}}
metrics
{:team "UX West"
:api #{metabase.metrics.api
metabase.metrics.core}
:uses #{api
collections
lib
lib-be
lib-metric
models
parameters
query-processor
request
server
util}}
model-persistence
{:team "Querying"
:api #{metabase.model-persistence.api
@@ -869,6 +895,7 @@
system
task
task-history
tracing
util}}
models
@@ -878,6 +905,7 @@
metabase.models.interface
metabase.models.resolution
metabase.models.serialization
metabase.models.transforms.transform
metabase.models.util.spec-update
metabase.models.visualization-settings}
:uses #{api
@@ -894,6 +922,7 @@
search
settings
transforms
transforms-base
util}
:friends #{transforms}}
@@ -940,6 +969,7 @@
system
task
task-history
tracing
users
util}}
@@ -983,6 +1013,7 @@
premium-features
remote-sync
settings
tracing
util
enterprise/advanced-permissions}}
@@ -1028,6 +1059,7 @@
settings
startup
task
tracing
util}}
product-feedback
@@ -1095,6 +1127,7 @@
request
task
task-history
tracing
util
enterprise/advanced-permissions
enterprise/sandbox}}
@@ -1120,6 +1153,7 @@
events
lib
lib-be
metrics
models
parameters
permissions
@@ -1185,6 +1219,7 @@
metabase.query-processor.middleware.add-remaps
metabase.query-processor.middleware.annotate
metabase.query-processor.middleware.cache-backend.db
metabase.query-processor.middleware.catch-exceptions
metabase.query-processor.middleware.constraints
metabase.query-processor.middleware.limit
metabase.query-processor.middleware.permissions
@@ -1236,6 +1271,7 @@
server
settings
system
tracing
users
util}}
@@ -1317,6 +1353,7 @@
settings
startup
task
tracing
transforms
util}}
@@ -1363,6 +1400,7 @@
session
settings
system
tracing
users
util
enterprise/sso}}
@@ -1384,6 +1422,7 @@
sso
system
task
tracing
util}}
settings
@@ -1430,6 +1469,32 @@
system
util}}
source-swap
{:team "Graphy"
:api #{metabase.source-swap.core
metabase.source-swap.schema}
:uses #{driver
lib
sql-tools
util}}
sql-parsing
{:team "DevEx"
:api #{metabase.sql-parsing.core}
:uses #{analytics
util}}
sql-tools
{:team "DevEx"
:api #{metabase.sql-tools.core
metabase.sql-tools.init}
:uses #{analytics
driver
lib
settings
sql-parsing
util}}
sso
{:team "UX West"
:api #{metabase.sso.api
@@ -1458,7 +1523,7 @@
util}}
sync
{:team "Querying"
{:team "Graphy"
:api #{metabase.sync.api
metabase.sync.core
metabase.sync.init
@@ -1480,6 +1545,7 @@
settings
task
task-history
tracing
util
warehouse-schema
warehouses}}
@@ -1496,6 +1562,7 @@
metabase.task.core}
:uses #{app-db
classloader
tracing
util}}
task-history
@@ -1512,6 +1579,7 @@
query-processor
request
task
tracing
util}}
tenants
@@ -1559,12 +1627,18 @@
models
util}}
tracing
{:team "DevEx"
:api #{metabase.tracing.core
metabase.tracing.init}
:uses #{events
settings
task
util}}
transforms
{:team "Querying"
:api #{metabase.transforms.api.transform
metabase.transforms.api.transform-job
metabase.transforms.api.transform-tag
metabase.transforms.core
:api #{metabase.transforms.core
metabase.transforms.feature-gating
metabase.transforms.init
metabase.transforms.schema
@@ -1576,22 +1650,67 @@
driver
events
lib
lib-be
models
premium-features
queries
query-processor
request
revisions
settings
sync
task
task-history
tracing
transforms-base
util}
:friends #{models
enterprise/transforms-python
enterprise/workspaces}}
transforms-base
{:team "Querying"
:api #{metabase.transforms-base.init
metabase.transforms-base.interface
metabase.transforms-base.util}
:uses #{driver
events
lib
lib-be
query-processor
sync
util}
:friends #{transforms
enterprise/transforms-python
enterprise/workspaces}}
transforms-inspector
{:team "Gadget"
:api #{metabase.transforms-inspector.core
metabase.transforms-inspector.schema}
:uses #{driver
lib
lib-be
query-processor
sql-tools
transforms-base
util}}
transforms-rest
{:team "Gadget"
:api #{metabase.transforms-rest.api.transform
metabase.transforms-rest.api.transform-job
metabase.transforms-rest.api.transform-tag}
:uses #{api
driver
events
models
query-processor
request
server
sql-tools
transforms
transforms-base
transforms-inspector
util}}
types
{:team "Querying"
:api #{metabase.types.core
@@ -1885,7 +2004,9 @@
request
server
util
enterprise/metabot-v3}}
enterprise/api
enterprise/metabot-v3
enterprise/workspaces}}
enterprise/ai-entity-analysis
{:team "Metabot"
@@ -2074,10 +2195,11 @@
warehouses}}
enterprise/dependencies
{:team "Querying"
{:team "Graphy"
:api #{metabase-enterprise.dependencies.api
metabase-enterprise.dependencies.core
metabase-enterprise.dependencies.init}
metabase-enterprise.dependencies.init
metabase-enterprise.dependencies.models.dependency}
:uses #{analyze
api
app-db
@@ -2098,8 +2220,10 @@
request
revisions
settings
sql-tools
task
transforms
transforms-base
util}}
enterprise/email
@@ -2211,11 +2335,13 @@
search
server
settings
sql-tools
store-api
sync
system
task
transforms
transforms-base
users
util
warehouses
@@ -2257,6 +2383,21 @@
enterprise/serialization
enterprise/transforms-python}}
enterprise/replacement
{:team "Graphy"
:api #{metabase-enterprise.replacement.api}
:uses #{api
app-db
events
lib
lib-be
models
queries
source-swap
task
util
enterprise/dependencies}}
enterprise/sandbox
{:team "UX West"
:api #{metabase-enterprise.sandbox.api.routes
@@ -2320,6 +2461,7 @@
search
settings
task
tracing
util
enterprise/llm
enterprise/metabot-v3}}
@@ -2418,7 +2560,7 @@
enterprise/transforms
{:team "Querying"
:api #{}
:uses #{}}
:uses #{premium-features}}
enterprise/transforms-python
{:team "Querying"
@@ -2439,6 +2581,7 @@
query-processor
settings
transforms
transforms-base
util}}
enterprise/upload-management
@@ -2454,8 +2597,8 @@
{:team "Metabot"
:api #{metabase-enterprise.workspaces.api
metabase-enterprise.workspaces.init}
:friends #{enterprise/agent-api}
:uses #{api
api-keys
app-db
config
database-routing
@@ -2464,15 +2607,23 @@
lib
lib-be
models
premium-features
queries
;; NOTE: we will remove this once we get "pure transforms" module.
query-processor
request
settings
sql-tools
system
transforms
transforms-base
util
enterprise/transforms-python}}}
enterprise/transforms-python}}
enterprise/writable-connection
{:team "Querying"
:api #{}
:uses #{premium-features}}}
;; namespaces matching these patterns (with `re-find`) are excluded from the module linter. Since regex literals
;; aren't allowed in EDN just used the `(str <regex>)` version i.e. two slashes instead of one.

View File

@@ -3,7 +3,7 @@
;; TODO -- seems silly to maintain different blacklists and whitelists here than we use for the deftest `^:parallel`
;; checker...
{:explicitly-safe
#{ ;; these toucan methods might actually set global values if it's used outside of a transaction,
#{;; these toucan methods might actually set global values if it's used outside of a transaction,
;; but since mt/with-temp runs in a transaction, so we'll ignore them in this case.
toucan2.core/delete!
toucan2.core/update!

View File

@@ -55,7 +55,6 @@
help-link-custom-destination
instance-creation
is-hosted?
is-metabot-enabled
jdbc-data-warehouse-max-connection-pool-size
jdbc-data-warehouse-unreturned-connection-timeout-seconds
jwt-attribute-email

View File

@@ -120,7 +120,10 @@
metabase.dashboards.models.dashboard-test/with-dash-in-collection [[:inner 0]]
metabase.driver-api.core/match [[:inner 0]]
metabase.driver-api.core/match-one [[:inner 0]]
metabase.driver-api.core/match-lite [[:inner 0]]
metabase.driver-api.core/match-many [[:inner 0]]
metabase.driver-api.core/replace [[:inner 0]]
metabase.driver-api.core/replace-lite [[:inner 0]]
metabase.driver-api.core/replace-in [[:inner 0]]
metabase.driver.bigquery-cloud-sdk.query-processor/with-temporal-type [[:default]]
metabase.driver.druid.client/DELETE [[:default]]
@@ -156,7 +159,9 @@
metabase.lib.util.match/match [[:inner 0]]
metabase.lib.util.match/match-one [[:inner 0]]
metabase.lib.util.match/match-lite [[:inner 0]]
metabase.lib.util.match/match-many [[:inner 0]]
metabase.lib.util.match/replace [[:inner 0]]
metabase.lib.util.match/replace-lite [[:inner 0]]
metabase.lib.util.match/replace-in [[:inner 0]]
metabase.models.interface/define-batched-hydration-method [[:inner 0]]
metabase.models.interface/define-simple-hydration-method [[:inner 0]]

8
.gitattributes vendored
View File

@@ -1,7 +1,9 @@
# Custom merge driver for YAML migration files
# Treats each changeset as an atomic unit to avoid conflicts within changesets
# Custom merge driver for per-release YAML migration files.
# Treats each changeset as an atomic unit to avoid conflicts within changesets.
# Only applies to per-release *_update_migrations.yaml files — directory-based
# migrations (e.g. 060/*.yaml) have one file per migration and don't need this.
# Execute this in your shell to enable custom merge driver:
# echo '\n[include]\npath=../.gitconfig' >> .git/config
#
# TODO (Bryan 12/12/25) -- reenable this when we can completely verify correctness
# resources/migrations/*.yaml merge=yaml-migrations
# resources/migrations/*_update_migrations.yaml merge=yaml-migrations

View File

@@ -1,5 +1,5 @@
# execute this in your shell to enable custom merge driver:
# echo '\n[include]\npath=../.gitconfig' >> .git/config
[merge "yaml-migrations"]
name = YAML migration changeset-aware merge
name = Per-release YAML migration changeset-aware merge
driver = bin/mage -merge-yaml-migrations %O %A %B %L %P

268
.github/CODEOWNERS vendored
View File

@@ -1,266 +1,30 @@
# Before changing this file, make sure you have read how we use it:
# https://www.notion.so/metabase/Pull-Request-Code-Review-Guide-374f05df889748b69d67af753f9bc9b8?pvs=4#36550c04ceab40b5a8b8bee6264b090c
# @metabase/core-backend-querying-platform
src/metabase/lib @metabase/core-backend-querying-platform
src/metabase/lib_be @metabase/core-backend-querying-platform
src/metabase/query_processor @metabase/core-backend-querying-platform
# @metabase/graphy
src/metabase/graph @metabase/graphy
enterprise/backend/src/metabase_enterprise/dependencies @metabase/graphy
enterprise/frontend/src/metabase-enterprise/dependencies @metabase/graphy
# @metabase/embedding
enterprise/frontend/src/embedding-sdk-package @metabase/embedding
enterprise/frontend/src/embedding-sdk-bundle @metabase/embedding
enterprise/frontend/src/embedding-sdk-shared @metabase/embedding
frontend/src/metabase/embedding-sdk @metabase/embedding
frontend/src/metabase/public/containers/PublicOrEmbeddedQuestion @metabase/embedding
frontend/src/metabase/public/containers/PublicOrEmbeddedDashboard @metabase/embedding
frontend/src/metabase/ui @metabase/core-frontend-ux-west
# snowplow/* @metabase/data
src/metabase/analytics/prometheus.clj @metabase/cloud-ops
.github @metabase/devexp
.clj-kondo @metabase/devexp
deps.edn @metabase/backend-developers
# @metabase/tech-writers
docs @metabase/tech-writers
docs/developers-guide # exclude from tech-writer ownership
#
# Backend modules <=> teams that own them
#
# @metabase/devexp
.github @metabase/devexp
# modules/drivers @metabase/drivers
src/metabase/driver/sql_jdbc/actions.clj @metabase/core-ux-east
test/metabase/driver/sql_jdbc/actions_test.clj @metabase/core-ux-east
# enterprise/backend/src/metabase_enterprise/advanced_config @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/advanced_config @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/advanced_permissions @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/advanced_permissions @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/airgap @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/airgap @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/api @metabase/devexp
# enterprise/backend/test/metabase_enterprise/api @metabase/devexp
# enterprise/backend/src/metabase_enterprise/audit_app @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/audit_app @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/auth_provider @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/auth_provider @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/billing @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/billing @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/cache @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/cache @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/content_verification @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/content_verification @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/core @metabase/devexp
# enterprise/backend/test/metabase_enterprise/core @metabase/devexp
# enterprise/backend/src/metabase_enterprise/dashboard_subscription_filters @metabase/core-ux-east
# enterprise/backend/test/metabase_enterprise/dashboard_subscription_filters @metabase/core-ux-east
# enterprise/backend/src/metabase_enterprise/enhancements @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/enhancements @metabase/core-backend-admin-webapp
# # enterprise/backend/src/metabase_enterprise/llm
# # enterprise/backend/test/metabase_enterprise/llm
# enterprise/backend/src/metabase_enterprise/query_reference_validation @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/query_reference_validation @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/sandbox @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/sandbox @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/scim @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/scim @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/search ...
# enterprise/backend/test/metabase_enterprise/search ...
# enterprise/backend/src/metabase_enterprise/serialization @metabase/core-ux-east
# enterprise/backend/test/metabase_enterprise/serialization @metabase/core-ux-east
# enterprise/backend/src/metabase_enterprise/snippet_collections @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/snippet_collections @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/sso @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/sso @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/stale @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/stale @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/stats @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/stats @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/task @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/task @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/upload_management @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/upload_management @metabase/core-backend-admin-webapp
src/metabase/actions @metabase/core-ux-east
test/metabase/actions @metabase/core-ux-east
# src/metabase/activity_feed @metabase/core-backend-admin-webapp
# test/metabase/activity_feed @metabase/core-backend-admin-webapp
# src/metabase/analytics @metabase/core-backend-admin-webapp
# test/metabase/analytics @metabase/core-backend-admin-webapp
# src/metabase/analyze @metabase/drivers
# test/metabase/analyze @metabase/drivers
# src/metabase/api @metabase/devexp
# test/metabase/api @metabase/devexp
# src/metabase/audit @metabase/core-backend-admin-webapp
# test/metabase/audit @metabase/core-backend-admin-webapp
# src/metabase/auth_provider @metabase/core-backend-admin-webapp
# test/metabase/auth_provider @metabase/core-backend-admin-webapp
# src/metabase/bookmarks @metabase/core-backend-admin-webapp
# test/metabase/bookmarks @metabase/core-backend-admin-webapp
# src/metabase/cmd @metabase/devexp
# test/metabase/cmd @metabase/devexp
# src/metabase/channel @metabase/core-ux-east
# test/metabase/channel @metabase/core-ux-east
# src/metabase/cloud_migration ...
# test/metabase/cloud_migration ...
# src/metabase/cmd @metabase/devexp
# test/metabase/cmd @metabase/devexp
# src/metabase/config @metabase/devexp
# test/metabase/config @metabase/devexp
# src/metabase/core @metabase/devexp
# test/metabase/core @metabase/devexp
# src/metabase/db @metabase/core-backend-admin-webapp
# test/metabase/db @metabase/core-backend-admin-webapp
# src/metabase/driver @metabase/drivers
# test/metabase/driver @metabase/drivers
# src/metabase/eid_translation @metabase/core-backend-admin-webapp
# test/metabase/eid_translation @metabase/core-backend-admin-webapp
# src/metabase/embed @metabase/embedding
# test/metabase/embed @metabase/embedding
# src/metabase/events @metabase/devexp
# test/metabase/events @metabase/devexp
# src/metabase/formatter @metabase/core-backend-querying
# test/metabase/formatter @metabase/core-backend-querying
# src/metabase/indexed_entities @metabase/core-backend-querying
# test/metabase/indexed_entities @metabase/core-backend-querying
# src/metabase/integrations ...
# test/metabase/integrations ...
# src/metabase/legacy_mbql @metabase/core-backend-querying
# test/metabase/legacy_mbql @metabase/core-backend-querying
# src/metabase/lib @metabase/core-backend-querying
# test/metabase/lib @metabase/core-backend-querying
# src/metabase/logger @metabase/devexp
# test/metabase/logger @metabase/devexp
# src/metabase/login_history @metabase/core-backend-admin-webapp
# test/metabase/login_history @metabase/core-backend-admin-webapp
# src/metabase/model_persistence @metabase/core-backend-querying
# test/metabase/model_persistence @metabase/core-backend-querying
# src/metabase/models @metabase/devexp
# test/metabase/models @metabase/devexp
# src/metabase/moderation @metabase/core-backend-admin-webapp
# test/metabase/moderation @metabase/core-backend-admin-webapp
# src/metabase/notification @metabase/core-ux-east
# test/metabase/notification @metabase/core-ux-east
# src/metabase/permissions @metabase/core-backend-admin-webapp
# test/metabase/permissions @metabase/core-backend-admin-webapp
# src/metabase/plugins @metabase/devexp
# test/metabase/plugins @metabase/devexp
# src/metabase/premium_features @metabase/core-backend-admin-webapp
# test/metabase/premium_features @metabase/core-backend-admin-webapp
# src/metabase/public_settings @metabase/core-backend-admin-webapp
# test/metabase/public_settings @metabase/core-backend-admin-webapp
# src/metabase/public_sharing @metabase/core-backend-admin-webapp
# test/metabase/public_sharing @metabase/core-backend-admin-webapp
# src/metabase/pulse @metabase/core-ux-east
# test/metabase/pulse @metabase/core-ux-east
# src/metabase/query_analysis @metabase/core-backend-querying
# test/metabase/query_analysis @metabase/core-backend-querying
# src/metabase/query_processor @metabase/core-backend-querying
# test/metabase/query_processor @metabase/core-backend-querying
# src/metabase/request @metabase/devexp
# test/metabase/request @metabase/devexp
# src/metabase/revisions @metabase/core-backend-admin-webapp
# test/metabase/revisions @metabase/core-backend-admin-webapp
# src/metabase/sample_data @metabase/core-backend-admin-webapp
# test/metabase/sample_data @metabase/core-backend-admin-webapp
# src/metabase/search ...
# test/metabase/search ...
# src/metabase/segments @metabase/core-backend-querying
# test/metabase/segments @metabase/core-backend-querying
# src/metabase/server @metabase/devexp
# test/metabase/server @metabase/devexp
# src/metabase/session @metabase/core-backend-admin-webapp
# test/metabase/session @metabase/core-backend-admin-webapp
# src/metabase/setup @metabase/core-backend-admin-webapp
# test/metabase/setup @metabase/core-backend-admin-webapp
# src/metabase/sso @metabase/core-backend-admin-webapp
# test/metabase/sso @metabase/core-backend-admin-webapp
# src/metabase/sync @metabase/drivers
# test/metabase/sync @metabase/drivers
# src/metabase/task @metabase/devexp
# test/metabase/task @metabase/devexp
# src/metabase/tiles @metabase/core-backend-dashviz
# test/metabase/tiles @metabase/core-backend-dashviz
# src/metabase/timeline @metabase/core-backend-dashviz
# test/metabase/timeline @metabase/core-backend-dashviz
# src/metabase/types @metabase/core-backend-querying
# test/metabase/types @metabase/core-backend-querying
# src/metabase/upload @metabase/core-backend-admin-webapp
# test/metabase/upload @metabase/core-backend-admin-webapp
# src/metabase/user_key_value @metabase/core-backend-admin-webapp
# test/metabase/user_key_value @metabase/core-backend-admin-webapp
# src/metabase/util @metabase/devexp
# test/metabase/util @metabase/devexp
# src/metabase/xrays @metabase/core-backend-querying
# test/metabase/xrays @metabase/core-backend-querying
# @metabase/cloud-ops
src/metabase/analytics/prometheus.clj @metabase/cloud-ops

View File

@@ -45,17 +45,31 @@ runs:
});
if (!artifacts.data?.artifacts?.[0]?.id) {
throw new Error(`No artifacts found for ${artifactName}`);
core.setFailed(`No artifacts found for ${artifactName}`);
return;
}
const artifact_id = artifacts.data.artifacts[0].id;
const download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: artifact_id,
archive_format: 'zip',
});
let download;
const maxAttempts = 3;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: artifact_id,
archive_format: 'zip',
});
break;
} catch (err) {
console.log(`Attempt ${attempt}/${maxAttempts} failed: ${err.message}`);
if (attempt === maxAttempts) throw err;
const delay = attempt * 15000; // 15s, 30s
console.log(`Retrying in ${delay / 1000}s...`);
await new Promise(r => setTimeout(r, delay));
}
}
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/mb.zip`, Buffer.from(download.data));

View File

@@ -7,9 +7,15 @@ outputs:
m2-cache-key:
description: "The cache key for the Maven cache."
value: ${{ steps.keys-factory.outputs.m2-cache-key }}
m2-restore-key:
description: "The fallback restore key prefix for the Maven cache."
value: ${{ steps.keys-factory.outputs.m2-restore-key }}
node-cache-key:
description: "The cache key for the Node.js cache."
value: ${{ steps.keys-factory.outputs.node-cache-key }}
node-restore-key:
description: "The fallback restore key prefix for the Node.js cache."
value: ${{ steps.keys-factory.outputs.node-restore-key }}
eslint-cache-key:
description: "The cache key for Eslint cache"
value: ${{ steps.keys-factory.outputs.eslint-cache-key }}
@@ -31,7 +37,9 @@ runs:
CACHE_SUFFIX="${{ runner.os }}-$WEEK_OF_YEAR"
echo "m2-cache-key=$M2_CACHE_PREFIX-$CACHE_SUFFIX" >> $GITHUB_OUTPUT
echo "m2-restore-key=$M2_CACHE_PREFIX-" >> $GITHUB_OUTPUT
echo "node-cache-key=$NODE_CACHE_PREFIX-$CACHE_SUFFIX" >> $GITHUB_OUTPUT
echo "node-restore-key=$NODE_CACHE_PREFIX-" >> $GITHUB_OUTPUT
echo "eslint-cache-key=$ESLINT_CACHE_PREFIX-$CACHE_SUFFIX" >> $GITHUB_OUTPUT
echo "cypress-cache-key=$CYPRESS_CACHE_PREFIX-$CACHE_SUFFIX" >> $GITHUB_OUTPUT
shell: bash

View File

@@ -1,14 +1,13 @@
name: Prepare back-end environment
description: |
Prepare the backend. Installs jdk, Clojure, and fetches the cache for m2.
The CI config fetches `ci-test-config.json` from master and lets us ignore tests at the var level if they are flaking.
inputs:
java-version:
required: true
default: '21'
default: "21"
clojure-version:
required: true
default: '1.12.0.1488'
default: "1.12.0.1488"
runs:
using: "composite"
@@ -17,18 +16,33 @@ runs:
uses: actions/setup-java@v4
with:
java-version: ${{ inputs.java-version }}
distribution: 'temurin'
distribution: "temurin"
- name: Install Clojure CLI
shell: bash
continue-on-error: true
run: |
curl -O https://download.clojure.org/install/linux-install-${{ inputs.clojure-version }}.sh &&
sudo bash ./linux-install-${{ inputs.clojure-version }}.sh
for attempt in 1 2 3; do
curl --retry 3 --retry-delay 5 --fail -O https://download.clojure.org/install/linux-install-${{ inputs.clojure-version }}.sh \
&& sudo bash ./linux-install-${{ inputs.clojure-version }}.sh \
&& break
echo "::warning::Clojure install attempt $attempt failed, retrying in 10s..."
sleep 10
done
if ! clojure --version; then
echo "::error::Clojure CLI installation failed after 3 attempts"
exit 1
fi
- name: Get M2 cache key
uses: ./.github/actions/get-cache-keys
id: get-cache-keys
- name: Install uv (for Python package installation)
uses: astral-sh/setup-uv@v7
with:
enable-cache: false
version: "0.10.2" # keep in sync with mise.toml
github-token: ${{ github.token }}
- name: Restore M2 cache
uses: actions/cache/restore@v4
with:
@@ -36,3 +50,5 @@ runs:
~/.m2
~/.gitlibs
key: ${{ steps.get-cache-keys.outputs.m2-cache-key }}
restore-keys: |
${{ steps.get-cache-keys.outputs.m2-restore-key }}

View File

@@ -15,11 +15,10 @@ outputs:
runs:
using: "composite"
steps:
- name: Install Chrome v111
- name: Install Chrome v144
uses: browser-actions/setup-chrome@v2
with:
# https://chromium.cypress.io/linux/stable/111.0.5563.146
chrome-version: 1097615
chrome-version: 144
id: setup-chrome
- name: Get cache keys

View File

@@ -11,17 +11,10 @@ inputs:
runs:
using: "composite"
steps:
- name: Prepare Node.js
uses: actions/setup-node@v4
- name: Prepare Node + Bun environment
uses: ./.github/actions/prepare-node-bun
with:
node-version: ${{ inputs.node-version }} # this overrides, if present
node-version-file: ".nvmrc"
- name: Install bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.7"
token: ${{ github.token }}
node-version: ${{ inputs.node-version }}
- name: Get cache keys
uses: ./.github/actions/get-cache-keys
@@ -34,6 +27,8 @@ runs:
~/.m2
~/.gitlibs
key: ${{ steps.get-cache-keys.outputs.m2-cache-key }}
restore-keys: |
${{ steps.get-cache-keys.outputs.m2-restore-key }}
- name: Get bun cache directory
id: bun-cache
@@ -51,6 +46,8 @@ runs:
${{ steps.bun-cache.outputs.dir }}
node_modules
key: ${{ steps.get-cache-keys.outputs.node-cache-key }}
restore-keys: |
${{ steps.get-cache-keys.outputs.node-restore-key }}
# postinstall lifecycle hook can be dangerous in CI so we only run it locally (see package.json)
# If a job wants to patch packages, it does so explicitly in the CI. This setting is opt-out.

View File

@@ -0,0 +1,20 @@
name: Prepare Node + Bun environment
description: "Set up Node.js and Bun for CI jobs"
inputs:
node-version:
description: "The version of Node.js to use, overriding .nvmrc"
required: false
runs:
using: "composite"
steps:
- name: Prepare Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
node-version-file: ".nvmrc"
- name: Install bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.7"
token: ${{ github.token }}

View File

@@ -0,0 +1,132 @@
name: Prepare Shoppy E2E environment
description: Shared setup steps for Shoppy E2E tests (regular and synthetic)
inputs:
commit:
description: "Commit SHA for artifact retrieval"
required: true
sample-app-branch-name:
description: "Branch name for the sample app"
required: true
sample-app-environment:
description: "Environment (production/development)"
required: true
staging-mb-all-features-token:
description: "Staging MB all features token"
required: true
shoppy-iam-role:
description: "Shoppy IAM role for AWS credentials"
required: true
shoppy-metabase-app-db-s3-bucket:
description: "S3 bucket for Shoppy app DB dump"
required: true
shoppy-metabase-app-db-s3-key:
description: "S3 key for Shoppy app DB dump"
required: true
shoppy-metabase-app-db-user:
description: "Shoppy Metabase app DB user"
required: true
shoppy-metabase-app-db-database:
description: "Shoppy Metabase app DB database"
required: true
metastore-dev-server-url:
description: "Metastore dev server URL"
required: true
shoppy-datadog-application-id:
description: "Shoppy Datadog application ID"
required: true
shoppy-datadog-client-token:
description: "Shoppy Datadog client token"
required: true
outputs:
chrome-path:
description: "Path to Chrome executable"
value: ${{ steps.cypress-prep.outputs.chrome-path }}
sample_app_branch_exists:
description: "Whether the sample app branch exists"
value: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists }}
runs:
using: "composite"
steps:
- uses: ./.github/actions/maximize-disk-space
# We have to use the latest version that does not have this issue https://github.com/docker/compose/pull/12752
- name: Set up Docker Compose
uses: docker/setup-compose-action@v1
with:
version: latest
- name: Retrieve uberjar artifact for ee
uses: ./.github/actions/fetch-artifact
with:
commit: ${{ inputs.commit }}
edition: ee
extract-path: "target/uberjar"
output-name: "metabase.jar"
- name: Retrieve Embedding SDK dist artifact
uses: actions/download-artifact@v4
with:
name: embedding-sdk-${{ inputs.commit }}
path: ${{ github.workspace }}/resources/embedding-sdk
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare Cypress environment
id: cypress-prep
uses: ./.github/actions/prepare-cypress
- name: Check if sample app branch exists
id: check-sample-app-branch
shell: bash
env:
SAMPLE_APP_BRANCH_NAME: ${{ inputs.sample-app-branch-name }}
run: |
REPO_URL="https://github.com/metabase/shoppy.git"
echo "Checking if branch '${SAMPLE_APP_BRANCH_NAME}' exists in ${REPO_URL}..."
if git ls-remote --exit-code --heads "$REPO_URL" "${SAMPLE_APP_BRANCH_NAME}"; then
echo "sample_app_branch_exists=true" >> $GITHUB_OUTPUT
else
echo "sample_app_branch_exists=false" >> $GITHUB_OUTPUT
fi
- name: Configure AWS credentials
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' }}
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ inputs.shoppy-iam-role }}
role-session-name: GitHub_to_AWS_via_FederatedOIDC
aws-region: us-east-1
- name: Download Shoppy's App DB PostgreSQL dump from S3
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' }}
shell: bash
run: |
aws s3 cp s3://${{ inputs.shoppy-metabase-app-db-s3-bucket }}/${{ inputs.shoppy-metabase-app-db-s3-key }} ./e2e/tmp/db_dumps/shoppy_metabase_app_db_dump.sql
- name: Prepare and launch Shoppy containers
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' }}
shell: bash
env:
SAMPLE_APP_BRANCH_NAME: ${{ inputs.sample-app-branch-name }}
SAMPLE_APP_ENVIRONMENT: ${{ inputs.sample-app-environment }}
CYPRESS_MB_ALL_FEATURES_TOKEN: ${{ inputs.staging-mb-all-features-token }}
METASTORE_DEV_SERVER_URL: ${{ inputs.metastore-dev-server-url }}
METABASE_APP_DB_USER: ${{ inputs.shoppy-metabase-app-db-user }}
METABASE_APP_DB: ${{ inputs.shoppy-metabase-app-db-database }}
SHOPPY_DATADOG_APPLICATION_ID: ${{ inputs.shoppy-datadog-application-id }}
SHOPPY_DATADOG_CLIENT_TOKEN: ${{ inputs.shoppy-datadog-client-token }}
SHOPPY_DATADOG_SITE: "us5.datadoghq.com"
SHOPPY_DATADOG_ENV: "staging"
run: npx tsx ./e2e/runner/embedding-sdk/sample-apps/start-ci.ts shoppy-e2e
- name: Prune build cache
shell: bash
run: docker builder prune -af
- name: Log free disk space
shell: bash
run: df -h /

View File

@@ -0,0 +1,110 @@
name: Embedding SDK Bundle Size Check
description: |
Downloads the current branch's uberjar and builds the base ref's
embedding-sdk.js from source, compares their sizes,
and sets output if the size change exceeds the threshold.
inputs:
edition:
description: 'Metabase edition (ee or oss)'
required: true
base-ref-commit:
description: 'Commit SHA of the base ref (e.g. merge base).'
required: true
threshold-percent:
description: 'Percent threshold for bundle size change'
required: true
outputs:
status:
description: "Bundle size status: increased, decreased, or stable"
value: ${{ steps.compare.outputs.status }}
size_change_percent:
description: "size change percent (positive = increase, negative = decrease)"
value: ${{ steps.compare.outputs.size_change_percent }}
runs:
using: "composite"
steps:
- name: Prepare backend
uses: ./.github/actions/prepare-backend
- name: Prepare frontend
uses: ./.github/actions/prepare-frontend
- name: Download current uberjar
uses: actions/download-artifact@v4
with:
name: metabase-${{ inputs.edition }}-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
path: temp/current-uberjar
- name: Extract embedding-sdk bundles from current uberjar
shell: bash
run: |
# Try new layout first (app/embedding-sdk/legacy/), fall back to old layout (app/embedding-sdk.js)
unzip -j temp/current-uberjar/target/uberjar/metabase.jar \
"frontend_client/app/embedding-sdk/legacy/embedding-sdk.js" \
-d temp/current-bundle 2>/dev/null || \
unzip -j temp/current-uberjar/target/uberjar/metabase.jar \
"frontend_client/app/embedding-sdk.js" \
-d temp/current-bundle
# Extract chunks directory if it exists (new layout only)
mkdir -p temp/current-chunks
unzip -j temp/current-uberjar/target/uberjar/metabase.jar \
"frontend_client/app/embedding-sdk/chunks/*" \
-d temp/current-chunks 2>/dev/null || true
- name: Checkout base ref
uses: actions/checkout@v4
with:
ref: ${{ inputs.base-ref-commit }}
path: temp/base-ref-src
- name: Build base-ref embedding-sdk.js
shell: bash
run: |
cd temp/base-ref-src
bun install
MB_EDITION=${{ inputs.edition }} bun run build-release
- name: Compare sizes and set output
id: compare
shell: bash
run: |
# --- Legacy (monolithic) bundle comparison ---
current_size=$(wc -c < temp/current-bundle/embedding-sdk.js) || { echo "::error::Failed to read current bundle"; exit 1; }
# Base ref: try new layout first, fall back to old
if [ -f "temp/base-ref-src/resources/frontend_client/app/embedding-sdk/legacy/embedding-sdk.js" ]; then
base_ref_size=$(wc -c < "temp/base-ref-src/resources/frontend_client/app/embedding-sdk/legacy/embedding-sdk.js")
else
base_ref_size=$(wc -c < "temp/base-ref-src/resources/frontend_client/app/embedding-sdk.js")
fi
[ "$current_size" -gt 0 ] && [ "$base_ref_size" -gt 0 ] || { echo "::error::Bundle file is empty"; exit 1; }
diff=$((current_size - base_ref_size))
percent=$(( diff * 100 / base_ref_size ))
echo "=== Legacy (monolithic) bundle ==="
echo "Current size: $current_size bytes"
echo "Base ref size: $base_ref_size bytes"
echo "Difference: $diff bytes ($percent%)"
threshold="${{ inputs.threshold-percent }}"
if [ "$percent" -gt "$threshold" ]; then
status="increased"
elif [ "$percent" -lt "$(( -threshold ))" ]; then
status="decreased"
else
status="stable"
fi
echo "status=$status" >> $GITHUB_OUTPUT
echo "size_change_percent=$percent" >> $GITHUB_OUTPUT
# --- Chunked bundle (informational) ---
if ls temp/current-chunks/*.js 1>/dev/null 2>&1; then
chunks_size=$(cat temp/current-chunks/*.js | wc -c)
echo ""
echo "=== Chunked bundle (bootstrap + chunks) ==="
echo "Total chunked size: $chunks_size bytes"
echo "chunks_size=$chunks_size" >> $GITHUB_OUTPUT
else
echo ""
echo "No chunked bundle found (expected on master)"
fi

View File

@@ -1,52 +1,55 @@
# Metabase Developer Assistant Instructions
## Frontend Development
For detailed coding standards and conventions, see `CLAUDE.md` and the skills in `.claude/skills/`.
### Repository Structure
## Code Review Standards
```
frontend/src/
├── metabase/ # Main application components and pages
├── metabase-lib/ # Query building and data modeling utilities
├── metabase-types/ # TypeScript type definitions
└── metabase/ui/ # Design system components
Review focus areas: Security → Performance → Testing → Documentation (in priority order)
e2e/ # Cypress end-to-end tests
frontend/test/ # Jest unit tests
```
When reviewing code, use these emoji prefixes to categorize feedback:
- 🔒 Security concerns
- ⚡ Performance opportunities
- 🧹 Cleanup needs
- 📚 Documentation gaps
- 🚨 Blocking issues
- 💭 Clarification questions
### Technology Stack
### Security Critical Issues
- **Framework**: React 18 with TypeScript
- **State**: Redux Toolkit
- **Styling**: CSS Modules (preferred) > Emotion styled-components
- **UI**: `metabase/ui` components built with Mantine v8
- **Build**: Rspack (primary), Webpack (legacy)
- **Testing**: Jest + React Testing Library, Cypress
- Check for hardcoded secrets, API keys, or credentials
- Look for SQL injection and XSS vulnerabilities
- Verify proper input validation and sanitization
- Review authentication and authorization logic
- Ensure enterprise features use the plugin system (no enterprise code in OSS)
### Coding Standards
### Performance Red Flags
#### Component Preferences
- Identify N+1 database query problems
- Spot inefficient loops and algorithmic issues
- Check for memory leaks and resource cleanup
- Review caching opportunities for expensive operations
- Prefer `metabase/ui`components over `metabase/common/components`
- Use `.tsx` for components, `.ts` for utilities
### Code Quality Essentials
#### Styling
- Functions should be focused and appropriately sized
- Use clear, descriptive naming conventions
- Ensure proper error handling throughout
- Remove dead code and unused imports
ALWAYS prefer Mantine style props,then CSS modules. DO NOT suggest styled components, they are deprecated.
### Review Style
#### TypeScript Migration
- Only flag actual issues worth mentioning
- Do not post "looks good" comments or congratulate following conventions
- Be specific and actionable in feedback
- Explain the "why" behind recommendations
- Ask clarifying questions when code intent is unclear
When heavily editing `.js`/`.jsx` files, create a separate PR to convert to TypeScript first, then implement changes.
### Review Format
#### Enterprise Features
- Condense feedback into as few comments as possible
- Prefer a single summary comment over dozens of inline comments
- Only use inline comments for critical issues that require immediate attention
- For non-critical issues, link to the specific line from the summary instead
- Delete all stale comments and summaries on each new push or CI run
Enterprise functionality MUST use the plugin system. It is very important to not expose enterprise code in the OSS version.
#### Testing Requirements
All PRs should include tests. Prefer Unit tests over E2E tests.
#### Localization
All user-facing strings MUST be localized using the ttag library. Localized strings should be complete phrases, do not concatenate a few separately localized strings. You should add context to strings where the meaning of the string might not be obvious in isolation: e.g. "Home" might have different words in some languages depending on whether you're talking about a dwelling or the landing page for a website.
Language-specific rules are in `.github/instructions/*.instructions.md`.

View File

@@ -30,7 +30,7 @@ frontend_configs: &frontend_configs
- "**/tsconfig*.json"
- "package.json"
- "babel.config.json"
- ".eslintrc.js"
- "eslint.config.mjs"
- "postcss.config.js"
- "rspack.config.js"
- "rspack.main.config.js"
@@ -199,3 +199,15 @@ cljs:
- *shared_specs
- ".github/workflows/cljs.yml"
- "shadow-cljs.edn"
openapi:
- ".github/workflows/openapi.yml"
- "src/metabase/api/**"
- "src/metabase/api_routes/**"
- "src/metabase/*_rest/api.clj"
- "src/metabase/*/api.clj"
- "src/metabase/*/api/*.clj"
- "enterprise/backend/src/metabase_enterprise/**/api.clj"
- "enterprise/backend/src/metabase_enterprise/**/api/*.clj"
- "resources/openapi/**"
- "package.json"

View File

@@ -0,0 +1,9 @@
---
applyTo: "**/*.{clj,cljs,cljc}"
---
# Clojure/ClojureScript Code Review
When reviewing Clojure or ClojureScript code, apply the guidelines in these files:
- `.claude/skills/_shared/clojure-style-guide.md`
- `.claude/skills/clojure-review/SKILL.md`

View File

@@ -0,0 +1,9 @@
---
applyTo: "docs/**/*.md"
---
# Documentation Review
When reviewing documentation, apply the guidelines in these files:
- `.claude/skills/_shared/metabase-style-guide.md`
- `.claude/skills/docs-review/SKILL.md`

View File

@@ -0,0 +1,11 @@
---
applyTo: "**/*.{ts,tsx,js,jsx}"
---
# TypeScript/JavaScript Code Review
When reviewing TypeScript or JavaScript code, apply the guidelines in these files:
- `.claude/skills/typescript-review/SKILL.md`
- `docs/developers-guide/frontend.md`
- `frontend/CLAUDE.md`

View File

@@ -0,0 +1,52 @@
// This script is used in .github/workflows/cross-version.yml
// to generate a matrix for cross-version migration testing
/**
* Generate a CI matrix for cross-version migration testing (EE only).
*
* Creates pairs of [source, target] for testing HEAD against the last N
* minor versions in both upgrade and downgrade directions.
*
* @param {number} currentVersion - The current major version number (e.g., 59)
* @param {number} count - Number of previous minor versions to test against (default: 3)
* @returns {{ config: Array<{ source: string, target: string }> }}
*
* @example
* buildMatrix(59)
* // Returns:
* // {
* // config: [
* // { source: "HEAD", target: "v1.59.x" },
* // { source: "v1.59.x", target: "HEAD" },
* // { source: "HEAD", target: "v1.58.x" },
* // { source: "v1.58.x", target: "HEAD" },
* // { source: "HEAD", target: "v1.57.x" },
* // { source: "v1.57.x", target: "HEAD" },
* // ]
* // }
*/
function buildCrossVersionMatrix(currentVersion, count = 3) {
if (!Number.isInteger(currentVersion) || currentVersion < 1) {
throw new Error(
`Invalid currentVersion: ${currentVersion}. Must be a positive integer.`,
);
}
if (!Number.isInteger(count) || count < 1) {
throw new Error(`Invalid count: ${count}. Must be a positive integer.`);
}
const config = Array.from({ length: count }, (_, i) => currentVersion - i)
.filter((majorVersion) => majorVersion >= 1)
.flatMap((majorVersion) => {
const version = `v1.${majorVersion}.x`;
return [
{ source: "HEAD", target: version },
{ source: version, target: "HEAD" },
];
});
return { config };
}
module.exports = { buildCrossVersionMatrix };

View File

@@ -0,0 +1,242 @@
const { buildCrossVersionMatrix } = require("./build-cross-version-matrix");
describe("buildCrossVersionMatrix", () => {
describe("default behavior", () => {
it("should generate matrix with 3 versions by default", () => {
const result = buildCrossVersionMatrix(59);
expect(result).toEqual({
config: [
{ source: "HEAD", target: "v1.59.x" },
{ source: "v1.59.x", target: "HEAD" },
{ source: "HEAD", target: "v1.58.x" },
{ source: "v1.58.x", target: "HEAD" },
{ source: "HEAD", target: "v1.57.x" },
{ source: "v1.57.x", target: "HEAD" },
],
});
});
it("should use EE prefix (v1)", () => {
const result = buildCrossVersionMatrix(50);
expect(result.config[0].target).toBe("v1.50.x");
expect(result.config[1].source).toBe("v1.50.x");
});
it("should create pairs in both directions (upgrade and downgrade)", () => {
const result = buildCrossVersionMatrix(59);
const entries = result.config;
// Check HEAD -> version (upgrade test)
expect(entries.filter((e) => e.source === "HEAD")).toHaveLength(3);
// Check version -> HEAD (downgrade test)
expect(entries.filter((e) => e.target === "HEAD")).toHaveLength(3);
});
});
describe("custom count", () => {
it("should generate matrix with custom count", () => {
const result = buildCrossVersionMatrix(59, 5);
expect(result.config).toHaveLength(10); // 5 versions × 2 directions
expect(result.config).toEqual([
{ source: "HEAD", target: "v1.59.x" },
{ source: "v1.59.x", target: "HEAD" },
{ source: "HEAD", target: "v1.58.x" },
{ source: "v1.58.x", target: "HEAD" },
{ source: "HEAD", target: "v1.57.x" },
{ source: "v1.57.x", target: "HEAD" },
{ source: "HEAD", target: "v1.56.x" },
{ source: "v1.56.x", target: "HEAD" },
{ source: "HEAD", target: "v1.55.x" },
{ source: "v1.55.x", target: "HEAD" },
]);
});
it("should generate matrix with count=1", () => {
const result = buildCrossVersionMatrix(59, 1);
expect(result.config).toHaveLength(2);
expect(result.config).toEqual([
{ source: "HEAD", target: "v1.59.x" },
{ source: "v1.59.x", target: "HEAD" },
]);
});
it("should generate matrix with large count", () => {
const result = buildCrossVersionMatrix(59, 10);
expect(result.config).toHaveLength(20);
// Check first and last entries
expect(result.config[0]).toEqual({
source: "HEAD",
target: "v1.59.x",
});
expect(result.config[18]).toEqual({
source: "HEAD",
target: "v1.50.x",
});
expect(result.config[19]).toEqual({
source: "v1.50.x",
target: "HEAD",
});
});
});
describe("edge cases", () => {
it("should handle version 1", () => {
const result = buildCrossVersionMatrix(1);
expect(result).toEqual({
config: [
{ source: "HEAD", target: "v1.1.x" },
{ source: "v1.1.x", target: "HEAD" },
],
});
});
it("should stop when version would go below 1", () => {
const result = buildCrossVersionMatrix(2, 5);
// Can only generate v1.2.x and v1.1.x (2 versions, not 5)
expect(result.config).toHaveLength(4);
expect(result).toEqual({
config: [
{ source: "HEAD", target: "v1.2.x" },
{ source: "v1.2.x", target: "HEAD" },
{ source: "HEAD", target: "v1.1.x" },
{ source: "v1.1.x", target: "HEAD" },
],
});
});
it("should handle high version numbers", () => {
const result = buildCrossVersionMatrix(100, 2);
expect(result).toEqual({
config: [
{ source: "HEAD", target: "v1.100.x" },
{ source: "v1.100.x", target: "HEAD" },
{ source: "HEAD", target: "v1.99.x" },
{ source: "v1.99.x", target: "HEAD" },
],
});
});
});
describe("error handling", () => {
it("should throw for undefined currentVersion", () => {
expect(() => buildCrossVersionMatrix()).toThrow(
"Invalid currentVersion: undefined. Must be a positive integer.",
);
});
it("should throw for non-integer currentVersion", () => {
expect(() => buildCrossVersionMatrix(59.5)).toThrow(
"Invalid currentVersion: 59.5. Must be a positive integer.",
);
});
it("should throw for zero currentVersion", () => {
expect(() => buildCrossVersionMatrix(0)).toThrow(
"Invalid currentVersion: 0. Must be a positive integer.",
);
});
it("should throw for negative currentVersion", () => {
expect(() => buildCrossVersionMatrix(-1)).toThrow(
"Invalid currentVersion: -1. Must be a positive integer.",
);
});
it("should throw for NaN currentVersion", () => {
expect(() => buildCrossVersionMatrix(NaN)).toThrow(
"Invalid currentVersion: NaN. Must be a positive integer.",
);
});
it("should throw for non-integer count", () => {
expect(() => buildCrossVersionMatrix(59, 2.5)).toThrow(
"Invalid count: 2.5. Must be a positive integer.",
);
});
it("should throw for zero count", () => {
expect(() => buildCrossVersionMatrix(59, 0)).toThrow(
"Invalid count: 0. Must be a positive integer.",
);
});
it("should throw for negative count", () => {
expect(() => buildCrossVersionMatrix(59, -1)).toThrow(
"Invalid count: -1. Must be a positive integer.",
);
});
});
describe("matrix structure for GitHub Actions", () => {
it("should return object with config array", () => {
const result = buildCrossVersionMatrix(59);
expect(result).toHaveProperty("config");
expect(Array.isArray(result.config)).toBe(true);
});
it("should produce valid JSON for GitHub Actions", () => {
const result = buildCrossVersionMatrix(59);
const json = JSON.stringify(result);
expect(() => JSON.parse(json)).not.toThrow();
const parsed = JSON.parse(json);
expect(parsed.config).toHaveLength(6);
expect(parsed.config[0]).toEqual({
source: "HEAD",
target: "v1.59.x",
});
});
it("should have consistent entry structure", () => {
const result = buildCrossVersionMatrix(59, 5);
result.config.forEach((entry) => {
expect(entry).toHaveProperty("source");
expect(entry).toHaveProperty("target");
expect(typeof entry.source).toBe("string");
expect(typeof entry.target).toBe("string");
// Either source or target should be HEAD
expect(entry.source === "HEAD" || entry.target === "HEAD").toBeTruthy();
});
});
});
describe("version ordering", () => {
it("should generate versions in descending order", () => {
const result = buildCrossVersionMatrix(59, 5);
const versions = result.config
.filter((e) => e.source === "HEAD")
.map((e) => e.target);
expect(versions).toEqual([
"v1.59.x",
"v1.58.x",
"v1.57.x",
"v1.56.x",
"v1.55.x",
]);
});
it("should interleave upgrade and downgrade tests", () => {
const result = buildCrossVersionMatrix(59, 2);
expect(result.config[0]).toEqual({
source: "HEAD",
target: "v1.59.x",
});
expect(result.config[1]).toEqual({
source: "v1.59.x",
target: "HEAD",
});
expect(result.config[2]).toEqual({
source: "HEAD",
target: "v1.58.x",
});
expect(result.config[3]).toEqual({
source: "v1.58.x",
target: "HEAD",
});
});
});
});

View File

@@ -19,7 +19,7 @@ while [[ $# -gt 0 ]]; do
done
echo "Cloning python-runner repository..."
git clone --depth 1 https://${METABASE_AUTOMATION_USER_TOKEN}@github.com/metabase/python-runner-container.git python-runner
git clone --depth 1 -b extensible-resource-schema https://${METABASE_AUTOMATION_USER_TOKEN}@github.com/metabase/python-runner-container.git python-runner
if [ "$SKIP_START" = false ]; then
echo "Starting python-runner..."

20
.github/team.json vendored
View File

@@ -4,14 +4,23 @@
"name": "Querying",
"label": ".Team/Querying",
"members": [
"ranquild",
"romeovs",
"bshepherdson",
"metamben",
"wzimrin",
"rileythomp",
"camsual"
]
},
{
"name": "Graphy",
"label": ".Team/Graphy",
"members": [
"ranquild",
"romeovs",
"ericnormand",
"galdre",
"rileythomp"
"dpsutton",
"escherize",
"bgrabow"
]
},
{
@@ -36,9 +45,6 @@
"name": "DevEx",
"label": ".Team/DevEx",
"members": [
"camsaul",
"dpsutton",
"escherize",
"fraserdrops",
"nemanjaglumac"
]

View File

@@ -15,14 +15,14 @@ jobs:
be-tests-mariadb:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 60
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
version:
- name: MariaDB 10.2
junit-name: be-tests-mariadb-10-2-ee
image: circleci/mariadb:10.2.23
- name: MariaDB 10.6
junit-name: be-tests-mariadb-10-6-ee
image: circleci/mariadb:10.6
env:
enable-ssl-tests: 'false'
- name: MariaDB Latest
@@ -100,7 +100,7 @@ jobs:
MB_MYSQL_SSL_TEST_USER: metabase
MB_MYSQL_SSL_TEST_PASSWORD: ${{ secrets.MYSQL_RDS_SSL_INSTANCE_PASSWORD }}
# for historic reasons (I don't want to mess around with required jobs) the job name should be something like
# "be-tests-mariadb-10-2-ee (0)"
# "be-tests-mariadb-10-6-ee (0)"
name: "${{ matrix.version.name }} ${{ matrix.job.name }}"
steps:
- uses: actions/checkout@v4
@@ -129,14 +129,14 @@ jobs:
be-tests-mysql:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 60
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
version:
- name: MySQL 8.0
junit-name: be-tests-mysql-8-0-ee
image: cimg/mysql:8.0
- name: MySQL 8.4
junit-name: be-tests-mysql-8-4-ee
image: cimg/mysql:8.4
env:
enable-ssl-tests: 'false'
enable-aws-iam-tests: 'false'
@@ -224,7 +224,7 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_AURORA_IAM_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_AURORA_IAM_SECRET_ACCESS_KEY }}
# for historic reasons (I don't want to mess around with required jobs) the job name should be something like
# "be-tests-mariadb-10-2-ee (0)"
# "be-tests-mariadb-10-6-ee (0)"
name: "${{ matrix.version.name }} ${{ matrix.job.name }}"
steps:
- uses: actions/checkout@v4
@@ -253,14 +253,14 @@ jobs:
be-tests-postgres:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 60
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
version:
- name: Postgres 12.x
- name: Postgres 14.x
junit-name: postgres-ee
docker-image: postgres:12-alpine
docker-image: postgres:14-alpine
env:
enable-ssl-tests: 'false'
enable-aws-iam-tests: 'false'

View File

@@ -11,8 +11,9 @@ name: Backend-Cloverage
on:
workflow_dispatch:
schedule:
- cron: '0 10 * * 1-5' # every weekday at 5am US eastern time
# re-enable once we figure out the correctness of the reports in DEV-1104
# schedule:
# - cron: '0 10 * * 1-5' # every weekday at 5am US eastern time
jobs:
setup-oss:

View File

@@ -36,10 +36,22 @@ jobs:
- run: clojure -X:dev:ee:ee-dev:drivers:drivers-dev:test:eastwood
name: Run Eastwood linter
be-linter-eastwood-test:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 30
name: Eastwood (test)
steps:
- uses: actions/checkout@v4
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- run: clojure -X:dev:ee:ee-dev:drivers:drivers-dev:test:eastwood/test
name: Run Eastwood linter on tests
be-tests:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 60
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
@@ -184,6 +196,7 @@ jobs:
be-tests-result:
needs:
- be-linter-eastwood
- be-linter-eastwood-test
- be-linter-clj-kondo
- be-cljfmt
- be-check

View File

@@ -1,98 +0,0 @@
name: "Claude Code Review"
on:
pull_request:
types:
- closed
- synchronize
- labeled
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}-claude-code-review
cancel-in-progress: true
jobs:
review:
if: |
(
github.event.action == 'closed' &&
github.event.pull_request.merged == true
) ||
contains(github.event.pull_request.labels.*.name, 'Claude Code Review')
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
# Make sure the value of GITHUB_TOKEN will not be persisted in repo's config
persist-credentials: false
# Apparently comments on specific lines and top-level comments about the PR as a whole are TWO TOTALLY DIFFERENT
# TYPES OF THINGS. So we have to fetch and delete separately.
- name: Delete old Claude PR review comments
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
shell: bash
run: |
# Get old comment IDs from the bot
old_line_comment_ids=$(gh api "/repos/$REPO/pulls/$PR_NUMBER/comments" --jq '.[] | select(.user.login=="metabase-bot[bot]") | .id')
echo "Deleting line comments: $old_line_comment_ids"
# Delete each comment
for id in $old_line_comment_ids; do
gh api -X DELETE "/repos/$REPO/pulls/comments/$id"
echo "Deleted line comment $id"
done
# Get old top-level comment IDs (technically "issue" comments on the PR?)
old_top_level_comment_ids=$(gh api "/repos/$REPO/issues/$PR_NUMBER/comments" --jq '.[] | select(.user.login=="metabase-bot[bot]") | .id')
echo "Deleting top-level comments: $old_top_level_comment_ids"
# Delete each comment
for id in $old_top_level_comment_ids; do
gh api -X DELETE "/repos/$REPO/issues/comments/$id"
echo "Deleted top-level comment $id"
done
- name: Generate GitHub App Token for Claude
uses: actions/create-github-app-token@v1
id: claude-app-token
with:
app-id: ${{ secrets.METABASE_BOT_APP_ID }}
private-key: ${{ secrets.METABASE_BOT_APP_PRIVATE_KEY }}
permission-contents: read # Required for reading repository files and PR diffs
permission-pull-requests: write # Required for creating inline comments and PR comments
- name: Fetch Community Clojure Style Guide
run: >-
curl https://raw.githubusercontent.com/bbatsov/clojure-style-guide/refs/heads/master/README.adoc -o CLOJURE_STYLE_GUIDE.adoc
- uses: anthropics/claude-code-action@v1
timeout-minutes: 10
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ steps.claude-app-token.outputs.token }}
prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}
Review this pull request for compliance with project coding standards and conventions.
**Use the appropriate review skill based on the file types changed:**
- Use clojure-review for Clojure/ClojureScript files
- Use typescript-review for TypeScript/JavaScript files
- Follow the review guidelines and checklists in each skill
DO NOT post any comments confirming that something is correct - only post comments for style violations or concerns.
Note: The PR branch is already checked out in the current working directory.
Use `gh pr comment` for top-level feedback.
Use `mcp__github_inline_comment__create_inline_comment` to highlight specific code issues.
Only post GitHub comments - don't submit review text as messages.
claude_args: |
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),ReadFile(CLOJURE_STYLE_GUIDE.adoc)"

View File

@@ -1,103 +0,0 @@
name: "Claude Docs Review"
on:
pull_request:
types:
- opened
- synchronize
- reopened
paths:
- 'docs/**/*.md'
- 'docs/**/*.mdx'
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to review'
required: true
type: number
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}-claude-docs-review
cancel-in-progress: true
jobs:
review:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
# Make sure the value of GITHUB_TOKEN will not be persisted in repo's config
persist-credentials: false
- name: Generate GitHub App Token for Claude
uses: actions/create-github-app-token@v1
id: claude-app-token
with:
app-id: ${{ secrets.METABASE_BOT_APP_ID }}
private-key: ${{ secrets.METABASE_BOT_APP_PRIVATE_KEY }}
permission-contents: read # Required for reading repository files and PR diffs
permission-pull-requests: write # Required for creating inline comments and PR comments
- name: Delete old Claude docs review comments
env:
GH_TOKEN: ${{ steps.claude-app-token.outputs.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }}
shell: bash
run: |
# Delete pending reviews first (these can be deleted entirely)
pending_review_ids=$(gh api "/repos/$REPO/pulls/$PR_NUMBER/reviews" --jq '.[] | select(.user.login=="metabase-bot[bot]" and .state=="PENDING") | .id')
if [ -n "$pending_review_ids" ]; then
echo "Deleting pending reviews: $pending_review_ids"
for id in $pending_review_ids; do
gh api -X DELETE "/repos/$REPO/pulls/$PR_NUMBER/reviews/$id"
echo "Deleted pending review $id"
done
fi
# Delete individual inline comments from submitted reviews (can't delete the review itself)
old_line_comment_ids=$(gh api "/repos/$REPO/pulls/$PR_NUMBER/comments" --jq '.[] | select(.user.login=="metabase-bot[bot]") | .id')
if [ -n "$old_line_comment_ids" ]; then
echo "Deleting line comments: $old_line_comment_ids"
for id in $old_line_comment_ids; do
gh api -X DELETE "/repos/$REPO/pulls/comments/$id"
echo "Deleted line comment $id"
done
fi
# Delete top-level review comments
old_top_level_comment_ids=$(gh api "/repos/$REPO/issues/$PR_NUMBER/comments" --jq '.[] | select(.user.login=="metabase-bot[bot]") | .id')
if [ -n "$old_top_level_comment_ids" ]; then
echo "Deleting top-level comments: $old_top_level_comment_ids"
for id in $old_top_level_comment_ids; do
gh api -X DELETE "/repos/$REPO/issues/comments/$id"
echo "Deleted top-level comment $id"
done
fi
- uses: anthropics/claude-code-action@v1
timeout-minutes: 10
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ steps.claude-app-token.outputs.token }}
show_full_output: true
prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }}
Review the documentation changes in this pull request.
Use the docs-review skill to review all changed documentation files for compliance with the Metabase writing style guide.
IMPORTANT: Only review markdown files (.md and .mdx). Skip any .txt files or other non-markdown files.
DO NOT post any comments confirming that something is correct - only post comments for style violations or concerns.
claude_args: |
--allowedTools "Skill,mcp__github__get_pull_request,mcp__github__get_pull_request_files,mcp__github__create_pending_pull_request_review,mcp__github__get_pull_request_diff,mcp__github__add_comment_to_pending_review,mcp__github__add_pull_request_review_comment_to_pending_review,mcp__github__delete_pending_pull_request_review,mcp__github__submit_pending_pull_request_review"

View File

@@ -43,27 +43,35 @@ jobs:
app-id: ${{ secrets.METABASE_BOT_APP_ID }}
private-key: ${{ secrets.METABASE_BOT_APP_PRIVATE_KEY }}
- name: Build Claude prompt
if: steps.codespell_check.outcome == 'failure'
run: |
cat > /tmp/claude_prompt.txt << 'PROMPT_HEADER'
You are tasked with fixing spelling errors detected by codespell in this repository.
## Codespell Output
The following spelling errors were detected:
```
PROMPT_HEADER
cat codespell_output.txt >> /tmp/claude_prompt.txt
cat >> /tmp/claude_prompt.txt << 'PROMPT_FOOTER'
```
## Instructions
1. **Review the codespell output and identify reported spelling errors** - The usual format codespell uses is `typo ==> correction`
2. **Fix every spelling error** - Use the Edit tool to fix each misspelling with the suggested correction
- If codespell suggests multiple corrections, choose the most appropriate one based on context
Human reviewers will verify the changes before merging, so fix all errors reported by codespell.
PROMPT_FOOTER
- name: Fix spelling errors with Claude Code
if: steps.codespell_check.outcome == 'failure'
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ steps.claude-app-token.outputs.token }}
prompt: |
You are tasked with fixing spelling errors detected by codespell in this repository.
## Codespell Output
The following spelling errors were detected:
```
$(cat codespell_output.txt)
```
## Instructions
1. **Review the codespell output and identify reported spelling errors** - The usual format codespell uses is `typo ==> correction`
2. **Fix every spelling error** - Use the Edit tool to fix each misspelling with the suggested correction
- If codespell suggests multiple corrections, choose the most appropriate one based on context
Human reviewers will verify the changes before merging, so fix all errors reported by codespell.
prompt_file: /tmp/claude_prompt.txt
claude_args: |
--allowedTools "Write,Read,Glob,Grep,Edit,MultiEdit,Bash(cat*),Bash(ls*)"
--disallowedTools "Bash(git commit*),Bash(git push*),Bash(gh*)"

View File

@@ -38,7 +38,7 @@ jobs:
base-ref: ${{ env.BASE_REF }}
head-ref: ${{ env.HEAD_REF }}
migrations-postgres-12-ee:
migrations-postgres-14-ee:
runs-on: ubuntu-22.04
timeout-minutes: 10
env:
@@ -50,7 +50,7 @@ jobs:
MB_DB_USER: circle_test
services:
postgres:
image: postgres:12-alpine
image: postgres:14-alpine
ports:
- "5432:5432"
env:
@@ -90,7 +90,7 @@ jobs:
base-ref: ${{ env.BASE_REF }}
head-ref: ${{ env.HEAD_REF }}
migrations-mysql-8-0-ee:
migrations-mysql-8-4-ee:
runs-on: ubuntu-22.04
timeout-minutes: 10
env:
@@ -102,7 +102,7 @@ jobs:
MB_DB_USER: root
services:
mysql:
image: cimg/mysql:8.0
image: cimg/mysql:8.4
ports:
- "3306:3306"
steps:
@@ -138,7 +138,7 @@ jobs:
base-ref: ${{ env.BASE_REF }}
head-ref: ${{ env.HEAD_REF }}
migrations-mariadb-10-2-ee:
migrations-mariadb-10-6-ee:
runs-on: ubuntu-22.04
timeout-minutes: 10
env:
@@ -150,7 +150,7 @@ jobs:
MB_DB_USER: root
services:
mariadb:
image: circleci/mariadb:10.2.23
image: circleci/mariadb:10.6
ports:
- "3306:3306"
steps:

102
.github/workflows/cross-version.yml vendored Normal file
View File

@@ -0,0 +1,102 @@
name: Cross-Version Migration Tests
on:
workflow_dispatch:
inputs:
source_version:
description: "Source version (e.g., v1.58.6 or HEAD)"
required: true
type: string
target_version:
description: "Target version (e.g., v1.58.7 or HEAD)"
required: true
type: string
pull_request:
paths:
- "cross-version/**"
- ".github/workflows/cross-version.yml"
- ".github/scripts/build-cross-version-matrix.js"
schedule:
# Run nightly at 3 AM UTC
- cron: "0 3 * * *"
jobs:
# Generate matrix for scheduled/PR runs
generate-matrix:
if: github.event_name != 'workflow_dispatch'
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: .github
- name: Generate version matrix
id: set-matrix
uses: actions/github-script@v7
with:
script: |
const { buildCrossVersionMatrix } = require('${{ github.workspace }}/.github/scripts/build-cross-version-matrix');
const { config } = buildCrossVersionMatrix(${{ vars.CURRENT_VERSION }});
const matrix = JSON.stringify({ include: config });
core.setOutput("matrix", matrix);
console.log("matrix:", matrix);
# Matrix-based tests for scheduled/PR runs
test-matrix:
if: github.event_name != 'workflow_dispatch'
needs: generate-matrix
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: Prepare release scripts
uses: ./.github/actions/prepare-release-scripts
- name: Run cross-version test
env:
CURRENT_VERSION: ${{ vars.CURRENT_VERSION }}
run: |
cd cross-version
./test.sh --source "${{ matrix.source }}" --target "${{ matrix.target }}"
- name: Show logs on failure
if: failure()
run: |
cd cross-version
docker compose logs metabase || true
docker compose logs postgres || true
# Manual test for workflow_dispatch
test-manual:
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Prepare release scripts
uses: ./.github/actions/prepare-release-scripts
- name: Run cross-version test
env:
CURRENT_VERSION: ${{ vars.CURRENT_VERSION }}
run: |
cd cross-version
./test.sh --source "${{ inputs.source_version }}" --target "${{ inputs.target_version }}"
- name: Show logs on failure
if: failure()
run: |
cd cross-version
docker compose logs metabase || true
docker compose logs postgres || true

View File

@@ -0,0 +1,79 @@
name: Disabled Drivers Slack Notification
on:
workflow_dispatch:
schedule:
- cron: "0 9 * * 1" # every Monday at 9:00 UTC
jobs:
notify-disabled-drivers:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/setup-node@v4
with:
node-version: lts/Jod
- name: Install Slack SDK
run: npm install @slack/web-api
- name: Check disabled drivers and notify Slack
uses: actions/github-script@v7
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
with:
script: |
const { WebClient } = require('@slack/web-api');
// Fetch the config
const response = await fetch('https://raw.githubusercontent.com/metabase/ci-test-config/refs/heads/master/ci-test-config.json');
const config = await response.json();
const drivers = config.ignored?.drivers || [];
const lastUpdated = config.metadata?.last_updated || 'unknown';
const reason = config.metadata?.reason || 'No reason provided';
if (drivers.length === 0) {
console.log('No drivers are currently disabled. Skipping Slack notification.');
return;
}
if (!process.env.SLACK_BOT_TOKEN) {
throw new Error('SLACK_BOT_TOKEN is required');
}
const slack = new WebClient(process.env.SLACK_BOT_TOKEN);
await slack.chat.postMessage({
channel: 'engineering-ci',
text: 'CI Disabled Drivers Report',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `:no_entry: *CI Disabled Drivers*\n${drivers.map(d => `\`${d}\``).join(' ')}`
},
accessory: {
type: 'button',
text: {
type: 'plain_text',
text: 'View Config',
emoji: true
},
url: 'https://github.com/metabase/ci-test-config/blob/master/ci-test-config.json'
}
},
{
type: 'context',
elements: [
{
type: 'mrkdwn',
text: `${reason} · :clock1: *${lastUpdated}*`
}
]
}
]
});
console.log('Slack notification sent successfully');

View File

@@ -8,7 +8,7 @@ on:
target_branch:
description: "Target branch for documentation update"
required: false
default: "release-x.58.x"
default: "release-x.59.x"
type: string
jobs:
@@ -16,6 +16,6 @@ jobs:
name: Call documentation update for release branch
uses: ./.github/workflows/docs-generate-base.yml
with:
target_branch: ${{ inputs.target_branch || 'release-x.58.x' }}
target_branch: ${{ inputs.target_branch || 'release-x.59.x' }}
secrets:
automation_token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}

View File

@@ -109,7 +109,7 @@ jobs:
junit-name: 'be-tests-druid-ee'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-mariadb-10-2-ee:
be-tests-mariadb-10-6-ee:
if: inputs.driver == 'Maria DB'
runs-on: ubuntu-22.04
timeout-minutes: 90
@@ -124,15 +124,15 @@ jobs:
MB_MYSQL_TEST_USER: root
services:
mariadb:
image: circleci/mariadb:10.2.23
image: circleci/mariadb:10.6
ports:
- "3306:3306"
steps:
- uses: actions/checkout@v4
- name: Test MariaDB driver (10.2)
- name: Test MariaDB driver (10.6)
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mariadb-10-2-ee'
junit-name: 'be-tests-mariadb-10-6-ee'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-mariadb-latest-ee:
@@ -298,7 +298,7 @@ jobs:
junit-name: 'be-tests-mongo-latest-ee'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-mysql-8-0-ee:
be-tests-mysql-8-4-ee:
if: inputs.driver == 'MySQL'
runs-on: ubuntu-22.04
timeout-minutes: 90
@@ -313,15 +313,15 @@ jobs:
MB_MYSQL_TEST_USER: root
services:
mysql:
image: cimg/mysql:8.0
image: cimg/mysql:8.4
ports:
- "3306:3306"
steps:
- uses: actions/checkout@v4
- name: Test MySQL driver (8.0)
- name: Test MySQL driver (8.4)
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mysql-8-0-ee'
junit-name: 'be-tests-mysql-8-4-ee'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-mysql-latest-ee:
@@ -438,7 +438,7 @@ jobs:
MB_POSTGRESQL_TEST_USER: mb_test
services:
postgres:
image: postgres:12-alpine
image: postgres:14-alpine
ports:
- "5432:5432"
env:
@@ -447,7 +447,7 @@ jobs:
POSTGRES_HOST_AUTH_METHOD: trust
steps:
- uses: actions/checkout@v4
- name: Test Postgres driver (12)
- name: Test Postgres driver (14)
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-postgres-ee'

View File

@@ -77,6 +77,9 @@ jobs:
--pr-labels="${{ join(github.event.pull_request.labels.*.name, ',') }}"
--skip="${{ inputs.skip }}"
)
echo "Running:"
echo "./bin/mage -driver-decisions ${MAGE_ARGS[@]}"
echo ""
# First call: show human-readable output for debugging
./bin/mage -driver-decisions "${MAGE_ARGS[@]}"
# Second call: output only key=value lines for GITHUB_OUTPUT
@@ -87,7 +90,7 @@ jobs:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.athena-should-run == 'true' || needs.determine-driver-skips.outputs.athena-quarantine-conflict == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 40
env:
CI: 'true'
DRIVERS: athena
@@ -148,11 +151,10 @@ jobs:
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/transforms-python-test]
:fail-fast? true
# DS 2025-09-26 temporarily disabled while we diagnose flakes
# - name: Transforms Python Tests
# test-args: >-
# :only-tags [:mb/transforms-python-test]
# needs-python-runner: true
- name: Transforms Python Tests
test-args: >-
:only-tags [:mb/transforms-python-test]
needs-python-runner: true
env:
CI: 'true'
DRIVERS: bigquery-cloud-sdk
@@ -211,7 +213,7 @@ jobs:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.clickhouse-should-run == 'true' || needs.determine-driver-skips.outputs.clickhouse-quarantine-conflict == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
@@ -294,7 +296,7 @@ jobs:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.druid-should-run == 'true' || needs.determine-driver-skips.outputs.druid-quarantine-conflict == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 40
env:
CI: 'true'
DRIVERS: druid
@@ -343,7 +345,7 @@ jobs:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.druid-jdbc-should-run == 'true' || needs.determine-driver-skips.outputs.druid-jdbc-quarantine-conflict == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 40
env:
CI: 'true'
DRIVERS: druid-jdbc
@@ -392,7 +394,7 @@ jobs:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.databricks-should-run == 'true' || needs.determine-driver-skips.outputs.databricks-quarantine-conflict == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 40
env:
CI: 'true'
DRIVERS: databricks
@@ -441,7 +443,7 @@ jobs:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.databricks-should-run == 'true' || needs.determine-driver-skips.outputs.databricks-quarantine-conflict == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 40
env:
CI: 'true'
DRIVERS: databricks
@@ -491,7 +493,7 @@ jobs:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.h2-should-run == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 40
env:
CI: 'true'
name: H2
@@ -522,7 +524,7 @@ jobs:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.h2-should-run == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 40
env:
CI: 'true'
name: H2 (OSS)
@@ -554,14 +556,14 @@ jobs:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.mysql-mariadb-should-run == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
version:
- name: MariaDB 10.2
junit-name: be-tests-mariadb-10-2-ee
image: circleci/mariadb:10.2.23
- name: MariaDB 10.6
junit-name: be-tests-mariadb-10-6-ee
image: circleci/mariadb:10.6
env:
enable-ssl-tests: 'false'
enable-aws-iam-tests: 'false'
@@ -571,9 +573,9 @@ jobs:
env:
enable-ssl-tests: 'false'
enable-aws-iam-tests: 'false'
- name: MySQL 8.0
junit-name: be-tests-mysql-8-0-ee
image: mysql:8.0
- name: MySQL 8.4
junit-name: be-tests-mysql-8-4-ee
image: mysql:8.4
env:
enable-ssl-tests: 'false'
enable-aws-iam-tests: 'false'
@@ -691,7 +693,7 @@ jobs:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.mongo-should-run == 'true' || needs.determine-driver-skips.outputs.mongo-quarantine-conflict == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
@@ -772,7 +774,7 @@ jobs:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.mongo-ssl-should-run == 'true' || needs.determine-driver-skips.outputs.mongo-ssl-quarantine-conflict == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
@@ -849,7 +851,7 @@ jobs:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.mongo-sharded-cluster-should-run == 'true' || needs.determine-driver-skips.outputs.mongo-sharded-cluster-quarantine-conflict == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 40
env:
CI: 'true'
DRIVERS: mongo
@@ -895,7 +897,7 @@ jobs:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.oracle-should-run == 'true' || needs.determine-driver-skips.outputs.oracle-quarantine-conflict == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
@@ -981,14 +983,14 @@ jobs:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.postgres-should-run == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
version:
- name: Postgres 12.x
- name: Postgres 14.x
junit-name: postgres-ee
docker-image: postgres:12-alpine
docker-image: postgres:14-alpine
env:
enable-ssl-tests: 'false'
enable-aws-iam-tests: 'false'
@@ -1092,7 +1094,7 @@ jobs:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.presto-jdbc-should-run == 'true' || needs.determine-driver-skips.outputs.presto-jdbc-quarantine-conflict == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 40
env:
CI: 'true'
DRIVERS: presto-jdbc
@@ -1174,7 +1176,7 @@ jobs:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.redshift-should-run == 'true' || needs.determine-driver-skips.outputs.redshift-quarantine-conflict == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
@@ -1275,12 +1277,10 @@ jobs:
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/transforms-python-test]
:fail-fast? true
# DS 2025-09-26 temporarily disabled while we diagnose flakes
# - name: Transforms Python Tests
# test-args: >-
# :only-tags [:mb/transforms-python-test]
# needs-python-runner: true
- name: Transforms Python Tests
test-args: >-
:only-tags [:mb/transforms-python-test]
needs-python-runner: true
env:
CI: 'true'
DRIVERS: snowflake
@@ -1348,7 +1348,7 @@ jobs:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.sparksql-should-run == 'true' || needs.determine-driver-skips.outputs.sparksql-quarantine-conflict == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 40
env:
CI: 'true'
DRIVERS: sparksql
@@ -1395,7 +1395,7 @@ jobs:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.sqlite-should-run == 'true' || needs.determine-driver-skips.outputs.sqlite-quarantine-conflict == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 40
env:
CI: 'true'
DRIVERS: sqlite
@@ -1521,7 +1521,7 @@ jobs:
# needs: determine-driver-skips
# if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
# runs-on: ubuntu-latest
# timeout-minutes: 60
# timeout-minutes: 40
# env:
# CI: 'true'
# DRIVERS: starburst
@@ -1589,7 +1589,7 @@ jobs:
needs: [determine-driver-skips, login-to-ecr]
if: ${{ needs.determine-driver-skips.outputs.vertica-should-run == 'true' || needs.determine-driver-skips.outputs.vertica-quarantine-conflict == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 40
env:
CI: 'true'
DRIVERS: vertica

View File

@@ -41,12 +41,13 @@ env:
jobs:
e2e-tests:
runs-on: ${{ inputs.runner }}
timeout-minutes: 45
timeout-minutes: 40
name: e2e-tests-${{ inputs.name }}-${{ inputs.edition }}
env:
MB_EDITION: ${{ inputs.edition }}
# Any env starting with `CYPRESS_` will be available to all Cypress tests via `Cypress.env()`
# Example: you can get `CYPRESS_FOO` with `Cypress.env("FOO")`
# Sensitive env vars (tokens, secrets) starting with `CYPRESS_` are accessed via async `cy.env()`
# Example: `cy.env(["FOO"]).then(({ FOO }) => ...)`
# For non-sensitive config, use `Cypress.expose()` in cypress config instead (synchronous)
CYPRESS_MB_ALL_FEATURES_TOKEN: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
CYPRESS_MB_STARTER_CLOUD_TOKEN: ${{ secrets.STAGING_MB_STARTER_CLOUD_TOKEN }}
CYPRESS_MB_PRO_CLOUD_TOKEN: ${{ secrets.STAGING_MB_PRO_CLOUD_TOKEN }}

View File

@@ -19,8 +19,9 @@ jobs:
react-version: [18, 19]
fail-fast: false
env:
# Any env starting with `CYPRESS_` will be available to all Cypress tests via `Cypress.env()`
# Example: you can get `CYPRESS_FOO` with `Cypress.env("FOO")`
# Sensitive env vars (tokens, secrets) starting with `CYPRESS_` are accessed via async `cy.env()`
# Example: `cy.env(["FOO"]).then(({ FOO }) => ...)`
# For non-sensitive config, use `Cypress.expose()` in cypress config instead (synchronous)
CYPRESS_MB_ALL_FEATURES_TOKEN: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
CYPRESS_MB_STARTER_CLOUD_TOKEN: ${{ secrets.STAGING_MB_STARTER_CLOUD_TOKEN }}
CYPRESS_MB_PRO_CLOUD_TOKEN: ${{ secrets.STAGING_MB_PRO_CLOUD_TOKEN }}
@@ -155,8 +156,9 @@ jobs:
env:
MB_RUN_MODE: "e2e"
METASTORE_DEV_SERVER_URL: ${{ secrets.METASTORE_DEV_SERVER_URL }}
# Any env starting with `CYPRESS_` will be available to all Cypress tests via `Cypress.env()`
# Example: you can get `CYPRESS_FOO` with `Cypress.env("FOO")`
# Sensitive env vars (tokens, secrets) starting with `CYPRESS_` are accessed via async `cy.env()`
# Example: `cy.env(["FOO"]).then(({ FOO }) => ...)`
# For non-sensitive config, use `Cypress.expose()` in cypress config instead (synchronous)
CYPRESS_MB_ALL_FEATURES_TOKEN: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
CYPRESS_MB_STARTER_CLOUD_TOKEN: ${{ secrets.STAGING_MB_STARTER_CLOUD_TOKEN }}
CYPRESS_MB_PRO_CLOUD_TOKEN: ${{ secrets.STAGING_MB_PRO_CLOUD_TOKEN }}

View File

@@ -9,7 +9,6 @@ env:
TZ: US/Pacific # to make Node match the instance tz
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HASH: ${{ github.event.pull_request.head.sha || github.sha }}-${{ github.run_attempt }}
PR_NUMBER: ${{ github.event.pull_request.number || '' }}
MB_EDITION: ee
jobs:
get-sample-apps-data:
@@ -21,12 +20,7 @@ jobs:
TEST_SUITES: |
[
"metabase-nodejs-react-sdk-embedding-sample-e2e",
"metabase-nextjs-sdk-embedding-sample-e2e",
"shoppy-e2e"
]
TEST_SUITES_TO_RUN_IN_MASTER_ONLY: |
[
"shoppy-e2e"
"metabase-nextjs-sdk-embedding-sample-e2e"
]
SAMPLE_APP_MAIN_BRANCH_NAME: "main"
SAMPLE_APP_STABLE_RELEASE_BRANCH_NAME_SUFFIX: "stable"
@@ -50,7 +44,6 @@ jobs:
} = require('${{ github.workspace }}/release/dist/index.cjs');
const testSuites = JSON.parse(process.env.TEST_SUITES);
const testSuitesToRunInMasterOnly = JSON.parse(process.env.TEST_SUITES_TO_RUN_IN_MASTER_ONLY);
const ref = '${{ github.base_ref || github.ref }}';
const branchName = ref.replace(/^refs\/heads\//, '');
@@ -74,19 +67,16 @@ jobs:
const sampleAppBranchName = `${currentBranchVersion}-${{ env.SAMPLE_APP_STABLE_RELEASE_BRANCH_NAME_SUFFIX }}`
const shouldRunTestsForVersion = (currentReleaseVersion - currentBranchVersion) <= (supportedReleasesCount - 1);
const releaseBranchTestSuites = testSuites.filter(testSuite => !testSuitesToRunInMasterOnly.includes(testSuite));
return {
branch_name: sampleAppBranchName,
matrix: shouldRunTestsForVersion
? releaseBranchTestSuites
: []
matrix: shouldRunTestsForVersion ? testSuites : [],
};
}
return {
branch_name: '',
matrix: []
matrix: [],
};
# In order to check compatibility between Sample Apps and Embedding SDK, the job for each sample app:
@@ -98,6 +88,7 @@ jobs:
e2e-sample-apps-tests:
needs: [get-sample-apps-data]
strategy:
fail-fast: false
matrix:
test_suite: ${{ fromJSON(needs.get-sample-apps-data.outputs.matrix) }}
environment: ["production", "development"]
@@ -168,28 +159,8 @@ jobs:
echo "sample_app_branch_exists=false" >> $GITHUB_OUTPUT
fi
- name: Configure AWS credentials
if: ${{ matrix.test_suite == 'shoppy-e2e' }}
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.SHOPPY_IAM_ROLE }}
role-session-name: GitHub_to_AWS_via_FederatedOIDC
aws-region: us-east-1
- name: Download Shoppy's App DB PostgreSQL dump from S3
if: ${{ matrix.test_suite == 'shoppy-e2e' }}
run: |
aws s3 cp s3://${{ secrets.SHOPPY_METABASE_APP_DB_S3_BUCKET }}/${{ secrets.SHOPPY_METABASE_APP_DB_S3_KEY }} ./e2e/tmp/db_dumps/shoppy_metabase_app_db_dump.sql
- name: Prepare and launch Sample Apps for shoppy-e2e test suite
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' && matrix.test_suite == 'shoppy-e2e' }}
env:
METABASE_APP_DB_USER: ${{ secrets.SHOPPY_METABASE_APP_DB_USER }}
METABASE_APP_DB: ${{ secrets.SHOPPY_METABASE_APP_DB_DATABASE }}
run: npx tsx ./e2e/runner/embedding-sdk/sample-apps/start-ci.ts shoppy-e2e
- name: Prepare and launch Sample Apps for other test suites
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' && matrix.test_suite != 'shoppy-e2e' }}
- name: Prepare and launch Sample Apps
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' }}
run: npx tsx ./e2e/runner/embedding-sdk/sample-apps/start-ci.ts ${{ matrix.test_suite }}
- name: Prune build cache
@@ -210,7 +181,7 @@ jobs:
uses: actions/upload-artifact@v4
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' && steps.run-e2e-tests.outcome != 'success' }}
with:
name: cypress-recording-sample-app-${{ matrix.test_suite }}-latest
name: cypress-recording-sample-app-${{ matrix.test_suite }}-${{ matrix.environment }}-latest
path: |
./e2e/tmp/${{ steps.get-sample-app-name.outputs.sample_app_name }}/cypress
if-no-files-found: ignore
@@ -220,6 +191,150 @@ jobs:
run: |
exit 1
# Shoppy e2e tests run separately using the shared composite action for setup.
e2e-shoppy-tests:
needs: [get-sample-apps-data]
strategy:
fail-fast: false
matrix:
environment: ["production", "development"]
if: |
!cancelled() && needs.get-sample-apps-data.result == 'success' &&
(github.ref == 'refs/heads/master' || github.base_ref == 'master')
runs-on: ubuntu-22.04
timeout-minutes: 45
name: e2e-sample-app-shoppy-e2e-tests-${{ matrix.environment }}-bundle
env:
MB_EDITION: ee
MB_RUN_MODE: e2e
METASTORE_DEV_SERVER_URL: ${{ secrets.METASTORE_DEV_SERVER_URL }}
CYPRESS_MB_ALL_FEATURES_TOKEN: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
SAMPLE_APP_BRANCH_NAME: ${{ needs.get-sample-apps-data.outputs.branch_name }}
SAMPLE_APP_ENVIRONMENT: ${{ matrix.environment }}
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- name: Prepare Shoppy E2E environment
uses: ./.github/actions/prepare-shoppy-e2e
id: shoppy-prep
with:
commit: ${{ github.event.pull_request.head.sha || github.sha }}
sample-app-branch-name: ${{ needs.get-sample-apps-data.outputs.branch_name }}
sample-app-environment: ${{ matrix.environment }}
staging-mb-all-features-token: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
shoppy-iam-role: ${{ secrets.SHOPPY_IAM_ROLE }}
shoppy-metabase-app-db-s3-bucket: ${{ secrets.SHOPPY_METABASE_APP_DB_S3_BUCKET }}
shoppy-metabase-app-db-s3-key: ${{ secrets.SHOPPY_METABASE_APP_DB_S3_KEY }}
shoppy-metabase-app-db-user: ${{ secrets.SHOPPY_METABASE_APP_DB_USER }}
shoppy-metabase-app-db-database: ${{ secrets.SHOPPY_METABASE_APP_DB_DATABASE }}
metastore-dev-server-url: ${{ secrets.METASTORE_DEV_SERVER_URL }}
shoppy-datadog-application-id: ${{ secrets.SHOPPY_METABASE_APP_DATADOG_APPLICATION_ID }}
shoppy-datadog-client-token: ${{ secrets.SHOPPY_METABASE_APP_DATADOG_CLIENT_TOKEN }}
- name: Run e2e tests for Shoppy
id: run-e2e-tests
if: ${{ steps.shoppy-prep.outputs.sample_app_branch_exists == 'true' }}
continue-on-error: true
env:
CYPRESS_BROWSER: ${{ steps.shoppy-prep.outputs.chrome-path }}
run: node e2e/runner/run_cypress_ci.js shoppy-e2e
- name: Upload Cypress Artifacts upon failure
uses: actions/upload-artifact@v4
if: ${{ steps.shoppy-prep.outputs.sample_app_branch_exists == 'true' && steps.run-e2e-tests.outcome != 'success' }}
with:
name: cypress-recording-sample-app-shoppy-e2e-${{ matrix.environment }}-latest
path: |
./e2e/tmp/shoppy/cypress
if-no-files-found: ignore
- name: Fail job if tests failed
if: ${{ steps.shoppy-prep.outputs.sample_app_branch_exists == 'true' && steps.run-e2e-tests.outcome != 'success' }}
run: |
exit 1
# Synthetic monitoring tests run only on master merges, in parallel with the regular e2e tests.
# They use the shoppy production environment and include performance measurement tests.
e2e-shoppy-synthetic-tests:
needs: [get-sample-apps-data]
if: |
!cancelled() && needs.get-sample-apps-data.result == 'success' &&
github.event_name == 'push' &&
github.ref == 'refs/heads/master'
runs-on: ubuntu-22.04
timeout-minutes: 45
name: e2e-shoppy-synthetic-monitoring-tests
env:
MB_EDITION: ee
MB_RUN_MODE: e2e
METASTORE_DEV_SERVER_URL: ${{ secrets.METASTORE_DEV_SERVER_URL }}
CYPRESS_MB_ALL_FEATURES_TOKEN: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
SAMPLE_APP_BRANCH_NAME: ${{ needs.get-sample-apps-data.outputs.branch_name }}
SAMPLE_APP_ENVIRONMENT: production
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Prepare Shoppy E2E environment
uses: ./.github/actions/prepare-shoppy-e2e
id: shoppy-prep
with:
commit: ${{ github.event.pull_request.head.sha || github.sha }}
sample-app-branch-name: ${{ needs.get-sample-apps-data.outputs.branch_name }}
sample-app-environment: production
staging-mb-all-features-token: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
shoppy-iam-role: ${{ secrets.SHOPPY_IAM_ROLE }}
shoppy-metabase-app-db-s3-bucket: ${{ secrets.SHOPPY_METABASE_APP_DB_S3_BUCKET }}
shoppy-metabase-app-db-s3-key: ${{ secrets.SHOPPY_METABASE_APP_DB_S3_KEY }}
shoppy-metabase-app-db-user: ${{ secrets.SHOPPY_METABASE_APP_DB_USER }}
shoppy-metabase-app-db-database: ${{ secrets.SHOPPY_METABASE_APP_DB_DATABASE }}
metastore-dev-server-url: ${{ secrets.METASTORE_DEV_SERVER_URL }}
shoppy-datadog-application-id: ${{ secrets.SHOPPY_METABASE_APP_DATADOG_APPLICATION_ID }}
shoppy-datadog-client-token: ${{ secrets.SHOPPY_METABASE_APP_DATADOG_CLIENT_TOKEN }}
- name: Get merged PR number
if: ${{ steps.shoppy-prep.outputs.sample_app_branch_exists == 'true' }}
id: get-merged-pr-number
run: |
PR=$(gh api repos/${{ github.repository }}/commits/${{ github.sha }}/pulls --jq '.[0].number' 2>/dev/null || echo "")
if [ -z "$PR" ] || [ "$PR" = "null" ]; then
echo "number=" >> "$GITHUB_OUTPUT"
else
echo "number=$PR" >> "$GITHUB_OUTPUT"
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Run synthetic monitoring tests
id: run-e2e-tests
if: ${{ steps.shoppy-prep.outputs.sample_app_branch_exists == 'true' }}
continue-on-error: true
env:
CYPRESS_BROWSER: ${{ steps.shoppy-prep.outputs.chrome-path }}
PR_NUMBER: ${{ steps.get-merged-pr-number.outputs.number }}
INCLUDE_SYNTHETIC_MONITORING_TESTS: "true"
run: node e2e/runner/run_cypress_ci.js shoppy-e2e
- name: Upload Cypress Artifacts upon failure
uses: actions/upload-artifact@v4
if: ${{ steps.shoppy-prep.outputs.sample_app_branch_exists == 'true' && steps.run-e2e-tests.outcome != 'success' }}
with:
name: cypress-recording-shoppy-synthetic-tests-latest
path: |
./e2e/tmp/shoppy/cypress
if-no-files-found: ignore
- name: Fail job if tests failed
if: ${{ steps.shoppy-prep.outputs.sample_app_branch_exists == 'true' && steps.run-e2e-tests.outcome != 'success' }}
run: |
exit 1
# The job checks integration of the Embedding SDK with consumer's apps that use different frameworks/bundlers,
# or when we want to test some tricky integration cases like conflicting types.
# The job
@@ -229,6 +344,7 @@ jobs:
# For more details see `Embedding SDK integration tests with Host Apps` in `enterprise/frontend/src/embedding-sdk-package/dev.md`
e2e-host-apps-tests:
strategy:
fail-fast: false
matrix:
test_suite:
[

View File

@@ -23,6 +23,10 @@ on:
description: "Whether to run host and sample apps compatibility tests."
required: true
type: boolean
bundle-size-check:
description: "Whether to run bundle size check."
required: true
type: boolean
skip:
description: Whether to skip running tests due to one of the artifacts failing to build.
required: true
@@ -124,8 +128,59 @@ jobs:
uses: ./.github/workflows/embedding-sdk-host-sample-apps-tests.yml
secrets: inherit
embedding-sdk-bundle-size-check:
if: inputs.bundle-size-check
continue-on-error: true
runs-on: ubuntu-22.04
timeout-minutes: 20
env:
BUNDLE_SIZE_THRESHOLD_PERCENT: 2
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/sdk-bundle-size-check
id: compare
with:
edition: ee
base-ref-commit: ${{ github.event.pull_request.base.sha }}
threshold-percent: ${{ env.BUNDLE_SIZE_THRESHOLD_PERCENT }}
- name: Comment on PR if bundle size has increased
uses: ./.github/actions/notify-pull-request
if: success() && steps.compare.outputs.status == 'increased'
with:
remove-previous-comment: true
add-new-comment: true
message: "⚠️ Embedding SDK bundle size increased by ${{ steps.compare.outputs.size_change_percent }}% compared to the ${{ github.base_ref }} branch (threshold: ${{ env.BUNDLE_SIZE_THRESHOLD_PERCENT }}%). Please contact Embedding team."
include-log: false
- name: Send notification to Slack via Zapier
if: success() && steps.compare.outputs.status == 'increased'
shell: bash
env:
ZAPIER_WEBHOOK_URL: ${{ secrets.EMBEDDING_ZAPIER_WEBHOOK_URL }}
SIZE_CHANGE_PERCENT: ${{ steps.compare.outputs.size_change_percent }}
BUNDLE_SIZE_THRESHOLD_PERCENT: ${{ env.BUNDLE_SIZE_THRESHOLD_PERCENT }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_URL: ${{ github.event.pull_request.html_url }}
run: |
curl -s -X POST "$ZAPIER_WEBHOOK_URL" \
-d "⚠️ Embedding SDK bundle size increased by ${SIZE_CHANGE_PERCENT}% (threshold: ${BUNDLE_SIZE_THRESHOLD_PERCENT}%)
PR #${PR_NUMBER} ${PR_TITLE} (${PR_URL})"
- name: Comment on PR if bundle size has decreased
uses: ./.github/actions/notify-pull-request
if: success() && steps.compare.outputs.status == 'decreased'
with:
remove-previous-comment: true
add-new-comment: true
message: "🎉 Embedding SDK bundle size decreased by ${{ steps.compare.outputs.size_change_percent }}% compared to the ${{ github.base_ref }} branch. Nice work!"
include-log: false
embedding-sdk-tests-result:
needs:
- embedding-sdk-bundle-size-check
- embedding-sdk-cli-snippets-type-check
- embedding-sdk-docs-snippets-type-check
- embedding-sdk-api-documentation-generation-validation

View File

@@ -2,9 +2,6 @@ name: Mage Tests
on:
pull_request:
paths:
- "mage/**"
- "bb.edn"
jobs:
# run mage tests even if the rest of be isnt needed

View File

@@ -30,7 +30,7 @@ jobs:
needs: check-label
if: needs.check-label.outputs.should-run == 'true'
runs-on: ubuntu-22.04
timeout-minutes: 60
timeout-minutes: 40
env:
CI: "true"
DRIVERS: postgres
@@ -81,7 +81,7 @@ jobs:
needs: check-label
if: needs.check-label.outputs.should-run == 'true'
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 40
env:
CI: "true"
DRIVERS: postgres

View File

@@ -0,0 +1,78 @@
name: Module Boundaries Stats
on:
workflow_dispatch:
schedule:
- cron: 40 1 * * 5 # every Friday at 1:40
jobs:
module-boundaries-stats:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --frozen-lockfile
- name: get module boundary stats
id: get-stats
run: |
NAMED_MODULES=$(node --input-type=module -e "
import { elements } from './frontend/lint/module-boundaries.mjs';
const named = elements.filter(e => !['shared/other', 'app/misc', 'other'].includes(e.type));
")
NAMED_DIRS=$(node --input-type=module -e "
import { elements } from './frontend/lint/module-boundaries.mjs';
const dirs = new Set();
for (const el of elements) {
const m = el.pattern.match(/^frontend\/src\/metabase\/(\w[\w-]*)\//)
if (m) dirs.add(m[1]);
}
")
TOTAL_DIRS=$(ls -d frontend/src/metabase/*/ | xargs -I{} basename {} | wc -l | tr -d ' ')
NAMED_COUNT=$(echo "$NAMED_DIRS" | grep -c . || true)
UNMODULED=$(($TOTAL_DIRS - $NAMED_COUNT))
VIOLATIONS=$(bun run module-boundaries 2>&1 | grep -o '✖ [0-9]* problems' | grep -o '[0-9]*' || echo "0")
echo "$NAMED_MODULES modules, $UNMODULED unmoduled folders, $VIOLATIONS violations"
echo "NAMED_MODULES=$NAMED_MODULES" >> $GITHUB_OUTPUT
echo "UNMODULED=$UNMODULED" >> $GITHUB_OUTPUT
echo "VIOLATIONS=$VIOLATIONS" >> $GITHUB_OUTPUT
- name: Upload stats
uses: actions/github-script@v7
env:
NAMED_MODULES: ${{ steps.get-stats.outputs.NAMED_MODULES }}
UNMODULED: ${{ steps.get-stats.outputs.UNMODULED }}
VIOLATIONS: ${{ steps.get-stats.outputs.VIOLATIONS }}
API_KEY: ${{ secrets.STATS_API_KEY }}
with:
script: | # js
const { uploadCsvToMb } = require('${{ github.workspace }}/.github/scripts/csv-to-mb.js');
console.log("NAMED_MODULES", Number(process.env.NAMED_MODULES));
console.log("UNMODULED", Number(process.env.UNMODULED));
console.log("VIOLATIONS", Number(process.env.VIOLATIONS));
const data = [
{
'Date': new Date().toISOString().slice(0, 10), // YYYY-MM-DD
'Modules': Number(process.env.NAMED_MODULES),
'Unmoduled folders': Number(process.env.UNMODULED),
'Violations': Number(process.env.VIOLATIONS),
}
];
uploadCsvToMb({
baseUrl: 'https://stats.metabase.com',
tableId: 75146,
jsonData: data,
mode: 'append',
}).then(() => {
console.log('Data uploaded successfully');
}).catch((error) => {
console.error('Error uploading data:', error);
process.exit(1);
});

View File

@@ -22,10 +22,8 @@ jobs:
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
# fetch full git history to allow fetching the latest base branch commit and comparing changes
fetch-depth: 0
- name: Setup Node.js 22
uses: actions/setup-node@v4
with:
node-version: 22.x
- name: Prepare Node + Bun environment
uses: ./.github/actions/prepare-node-bun
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Generate version.properties

44
.github/workflows/openapi.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: Validate OpenAPI
on:
push:
branches:
- 'master'
- 'release-**'
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
cancel-in-progress: true
jobs:
files-changed:
name: Check which files changed
runs-on: ubuntu-22.04
timeout-minutes: 3
outputs:
openapi: ${{ steps.changes.outputs.openapi }}
steps:
- uses: actions/checkout@v4
- name: Test which files changed
uses: dorny/paths-filter@v3.0.0
id: changes
with:
token: ${{ github.token }}
filters: .github/file-paths.yaml
validate-openapi:
needs: files-changed
if: needs.files-changed.outputs.openapi == 'true'
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Generate OpenAPI spec
run: bun run generate-openapi
- name: Validate OpenAPI spec
run: bun run lint-openapi

View File

@@ -5,8 +5,8 @@ on:
types: [ closed, unlabeled ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: false
jobs:
pr-env:

View File

@@ -0,0 +1,29 @@
name: PR Env Destroy (Manual)
on:
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to destroy environment for'
required: true
type: string
self_hosting:
description: 'Self-hosting stack'
type: boolean
default: false
jobs:
approve:
runs-on: ubuntu-latest
environment: pr-env-admin
steps:
- name: Approved
run: echo "Destroying PR environment for PR #${{ inputs.pr_number }}"
destroy:
needs: approve
uses: ./.github/workflows/pr-env-destroy.yml
secrets: inherit
with:
self_hosting: ${{ inputs.self_hosting }}
pr_number: ${{ inputs.pr_number }}

View File

@@ -9,6 +9,13 @@ on:
required: false
type: boolean
default: false
pr_number:
description: 'PR number (required for manual dispatch)'
required: false
type: string
env:
PR_NUMBER: ${{ inputs.pr_number || github.event.number }}
jobs:
destroy_pr:
@@ -62,8 +69,8 @@ jobs:
- name: Destroy PR Review ENV
if: ${{ !inputs.self_hosting }}
run: |
kubectl delete metabase -n hosting-pr${{ github.event.number }} hosting-pr${{ github.event.number }}
kubectl delete ns hosting-pr${{ github.event.number }}
kubectl delete metabase -n hosting-pr${{ env.PR_NUMBER }} hosting-pr${{ env.PR_NUMBER }}
kubectl delete ns hosting-pr${{ env.PR_NUMBER }}
- name: Setup Helm
if: ${{ inputs.self_hosting }}
uses: azure/setup-helm@v4
@@ -72,8 +79,8 @@ jobs:
- name: Destroy PR Review ENV (self-hosting)
if: ${{ inputs.self_hosting }}
run: |
helm uninstall hosting-pr${{ github.event.number }} --namespace hosting-pr${{ github.event.number }} || true
kubectl delete ns hosting-pr${{ github.event.number }} || true
helm uninstall hosting-pr${{ env.PR_NUMBER }} --namespace hosting-pr${{ env.PR_NUMBER }} || true
kubectl delete ns hosting-pr${{ env.PR_NUMBER }} || true
- name: Setup psql client
run: |
sudo apt-get update
@@ -85,4 +92,4 @@ jobs:
-h ${{ secrets.PR_ENV_DB_HOST }} \
-U ${{ secrets.PR_ENV_DB_USER }} \
-d ${{ secrets.PR_ENV_DB_NAME }} \
-c "DROP DATABASE IF EXISTS hosting_pr${{ github.event.number }};"
-c "DROP DATABASE IF EXISTS hosting_pr${{ env.PR_NUMBER }};"

266
.github/workflows/repro-bot.yml vendored Normal file
View File

@@ -0,0 +1,266 @@
name: "Repro Bot"
on:
issues:
types:
- labeled
workflow_dispatch:
inputs:
issue_number:
description: "Issue number to reproduce"
required: true
type: string
repro_bot_branch:
description: "Branch of repro-bot to use"
required: false
default: "main"
type: string
jobs:
repro-bot:
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: read
issues: read
id-token: write
if: |
(github.event_name == 'push') ||
(github.event_name == 'issues' && github.event.label.name == '.Run Repro Bot' && github.event.issue.state == 'open') ||
(github.event_name == 'workflow_dispatch')
steps:
- name: Get issue number
id: issue
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "number=${{ github.event.inputs.issue_number }}" >> $GITHUB_OUTPUT
elif [ "${{ github.event_name }}" = "push" ]; then
echo "number=70292" >> $GITHUB_OUTPUT
else
echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
fi
- uses: actions/checkout@v4
- uses: ./.github/actions/prepare-backend
- uses: ./.github/actions/prepare-frontend
- name: Checkout repro-bot
uses: actions/checkout@v6
with:
repository: metabase/repro-bot
ref: ${{ github.event.inputs.repro_bot_branch || 'main' }}
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
path: repro-bot
- name: Pre-clone metabase bare git repo
run: git clone --bare .git repro-bot/git/metabase
- name: Capture Node.js path for MCP subprocesses
run: echo "NODE_BIN_DIR=$(dirname $(which node))" >> $GITHUB_ENV
- name: Install playwright-cli
run: |
npm install -g @playwright/cli@latest
playwright-cli install --skills
- name: Install bbin and clj-nrepl-eval
run: |
# Install babashka
curl -sLO https://raw.githubusercontent.com/babashka/babashka/master/install
chmod +x install
sudo ./install
rm install
# Install bbin
mkdir -p ~/.bbin/bin
curl -sL https://raw.githubusercontent.com/babashka/bbin/main/bbin > ~/.bbin/bin/bbin
chmod +x ~/.bbin/bin/bbin
export PATH="$HOME/.bbin/bin:$PATH"
export BBIN_HOME="$HOME/.bbin"
# Install clj-nrepl-eval via bbin
bbin install https://github.com/bhauman/clojure-mcp-light.git \
--tag v0.2.1 \
--as clj-nrepl-eval \
--main-opts '["-m" "clojure-mcp-light.nrepl-eval"]'
# Add to PATH for the rest of the workflow
echo "$HOME/.bbin/bin" >> $GITHUB_PATH
# Verify
clj-nrepl-eval --help | head -3
- name: Register MCP servers
env:
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY_REPRO_BOT }}
run: |
UV_PATH="$(which uv)"
jq -n \
--arg uv "$UV_PATH" \
--arg key "$LINEAR_API_KEY" \
'{
mcpServers: {
"metabase-repro": {
command: $uv,
args: ["run", "--directory", "mcp", "--with", "fastmcp", "python", "-m", "metabase_repro"]
},
"linear-server": {
url: "https://mcp.linear.app/mcp",
type: "http",
headers: {
Authorization: ("Bearer " + $key)
}
}
}
}' > ~/.claude.json
- name: Generate GitHub App Token
uses: actions/create-github-app-token@v2
id: claude-app-token
with:
app-id: ${{ secrets.METABASE_BOT_APP_ID }}
private-key: ${{ secrets.METABASE_BOT_APP_PRIVATE_KEY }}
- name: Run repro-bot with Claude Code
id: claude
continue-on-error: true
uses: anthropics/claude-code-action@v1
env:
CLAUDE_CODE_DEBUG_LOGS_DIR: /tmp/claude-debug-logs
CLAUDE_WORKING_DIR: ${{ github.workspace }}/repro-bot
MB_PREMIUM_EMBEDDING_TOKEN: ${{ secrets.PR_ENV_MB_PREMIUM_EMBEDDING_TOKEN }}
GITHUB_ACTIONS_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
timeout-minutes: 30
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_REPRO_BOT }}
github_token: ${{ steps.claude-app-token.outputs.token }}
prompt: |
Read and follow the instructions in .claude/commands/repro.md to investigate this GitHub issue:
https://github.com/${{ github.repository }}/issues/${{ steps.issue.outputs.number }}
## CI Environment Notes
- The bare git repo has been pre-cloned at git/metabase. The master worktree still needs setup via setup_master_worktree().
- The GITHUB_ACTIONS_RUN_URL environment variable is set. If any phase fails catastrophically, follow the CI error handling in repro.md to post the error to Linear with the action run URL.
- Docker is available for appdb (Postgres/MySQL/MariaDB) if needed.
- Java and Python/uv are pre-installed.
claude_args: >-
--allowedTools
"Glob,
Grep,
Read,
ToolSearch,
Skill(*),
WebSearch,
WebFetch(domain:docs.metabase.com),
WebFetch(domain:metabase.com),
WebFetch(domain:www.metabase.com),
Edit(//./**),
Edit(repro/**),
Edit(/tmp/**),
Write(//./**),
Write(repro/**),
Write(/tmp/**),
Bash(base64 *),
Bash(cat *),
Bash(cd *),
Bash(clj-nrepl-eval *),
Bash(clojure *),
Bash(cp *),
Bash(diff *),
Bash(docker *),
Bash(echo *),
Bash(find *),
Bash(gh api repos/metabase/metabase/*),
Bash(gh issue list * --repo metabase/metabase *),
Bash(gh issue view * --repo metabase/metabase *),
Bash(gh release *),
Bash(gh search *),
Bash(git *),
Bash(grep *),
Bash(head *),
Bash(jar *),
Bash(jq *),
Bash(kill *),
Bash(ls *),
Bash(mkdir *),
Bash(mv *),
Bash(npx playwright-cli:*),
Bash(pgrep *),
Bash(playwright-cli:*),
Bash(printf *),
Bash(ps *),
Bash(psql *),
Bash(PGPASSWORD=*),
Bash(python -m mcp.metabase_repro *),
Bash(python3 *),
Bash(sed *),
Bash(sort *),
Bash(strings *),
Bash(tail *),
Bash(tee *),
Bash(touch *),
Bash(unzip *),
Bash(wc *),
Bash(which *),
Bash(xargs *),
mcp__metabase-repro__api_call,
mcp__metabase-repro__browser_snapshot,
mcp__metabase-repro__check_setup,
mcp__metabase-repro__check_setup_token,
mcp__metabase-repro__cleanup,
mcp__metabase-repro__create_env,
mcp__metabase-repro__get_session_token,
mcp__metabase-repro__get_status,
mcp__metabase-repro__jar_download,
mcp__metabase-repro__repl_eval,
mcp__metabase-repro__reset_metabase,
mcp__metabase-repro__run_test,
mcp__metabase-repro__save_test_patch,
mcp__metabase-repro__setup_master_worktree,
mcp__metabase-repro__setup_worktree,
mcp__metabase-repro__start_appdb,
mcp__metabase-repro__start_metabase,
mcp__metabase-repro__stop_appdb_tool,
mcp__metabase-repro__stop_metabase,
mcp__linear-server__create_attachment,
mcp__linear-server__get_issue,
mcp__linear-server__list_comments,
mcp__linear-server__list_issues,
mcp__linear-server__list_issue_labels,
mcp__linear-server__save_comment,
mcp__linear-server__save_issue"
--disallowedTools "Bash(git push*),Bash(rm -rf /*)"
- name: Verify repro-bot completed successfully
run: |
if [ "${{ steps.claude.outcome }}" = "failure" ]; then
echo "::error::Claude Code crashed or exited with an error"
exit 1
fi
EXEC_FILE="${{ steps.claude.outputs.execution_file }}"
if [ -z "$EXEC_FILE" ] || [ ! -f "$EXEC_FILE" ]; then
echo "::error::No execution file found"
exit 1
fi
if ! grep -q "mcp__linear-server__" "$EXEC_FILE"; then
echo "::error::Claude did not post a comment on Linear"
exit 1
fi
echo "Repro-bot completed successfully and posted to Linear"
- name: Upload repro-bot artifacts
uses: actions/upload-artifact@v7
if: always()
with:
name: repro-bot-${{ steps.issue.outputs.number }}
if-no-files-found: ignore
path: |
repro/*/SUGGESTIONS.md
repro/*/*.log
repro/*/*.patch
repro/*/playwright/**
${{ steps.claude.outputs.execution_file }}

View File

@@ -40,11 +40,19 @@ jobs:
const BRANCH = process.env.BRANCH_NAME;
if (ATTEMPT <= MAX_ATTEMPTS) {
github.rest.actions.reRunWorkflowFailedJobs({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: ${{ github.event.workflow_run.id }},
});
try {
await github.rest.actions.reRunWorkflowFailedJobs({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: ${{ github.event.workflow_run.id }},
});
} catch (error) {
if (error.status === 403) {
console.log(`Skipping re-run: ${error.message}`);
} else {
throw error;
}
}
} else if (!BRANCH.includes('backport-')) {
// notify slack of repeated failure
const { WebClient } = require('@slack/web-api');

View File

@@ -0,0 +1,63 @@
name: Reset Cloud Test Dataset
on:
workflow_dispatch:
inputs:
driver:
description: 'Driver to reset dataset for'
required: true
default: 'snowflake'
type: choice
options:
- snowflake
- bigquery-cloud-sdk
- redshift
- databricks
dataset-name:
description: 'Dataset name to drop (e.g., test-data)'
required: true
default: 'test-data'
type: string
jobs:
reset-dataset:
runs-on: ubuntu-latest
timeout-minutes: 10
env:
CI: 'true'
DRIVERS: ${{ inputs.driver }}
# Snowflake
MB_SNOWFLAKE_TEST_USER: METABASE CI
MB_SNOWFLAKE_TEST_ACCOUNT: ${{ secrets.MB_SNOWFLAKE_TEST_ACCOUNT }}
MB_SNOWFLAKE_TEST_PASSWORD: ${{ secrets.MB_SNOWFLAKE_TEST_PASSWORD }}
MB_SNOWFLAKE_TEST_PRIVATE_KEY: ${{ secrets.MB_SNOWFLAKE_TEST_PRIVATE_KEY }}
MB_SNOWFLAKE_TEST_WAREHOUSE: ${{ secrets.MB_SNOWFLAKE_TEST_WAREHOUSE }}
# BigQuery
MB_BIGQUERY_TEST_PROJECT_ID: ${{ secrets.BIGQUERY_TEST_PROJECT_ID }}
MB_BIGQUERY_TEST_CLIENT_ID: ${{ secrets.MB_BIGQUERY_TEST_CLIENT_ID }}
MB_BIGQUERY_TEST_CLIENT_SECRET: ${{ secrets.MB_BIGQUERY_TEST_CLIENT_SECRET }}
MB_BIGQUERY_TEST_ACCESS_TOKEN: ${{ secrets.MB_BIGQUERY_TEST_ACCESS_TOKEN }}
MB_BIGQUERY_TEST_REFRESH_TOKEN: ${{ secrets.MB_BIGQUERY_TEST_REFRESH_TOKEN }}
MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON: ${{ secrets.MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON }}
MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON_ROUTING: ${{ secrets.MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON_ROUTING }}
# Redshift
MB_REDSHIFT_TEST_USER: metabase_ci
MB_REDSHIFT_TEST_DB: testdb
MB_REDSHIFT_TEST_HOST: ${{ secrets.MB_REDSHIFT_TEST_HOST }}
MB_REDSHIFT_TEST_PASSWORD: ${{ secrets.MB_REDSHIFT_TEST_PASSWORD }}
# Databricks
MB_DATABRICKS_TEST_HOST: ${{ secrets.MB_DATABRICKS_JDBC_TEST_HOST }}
MB_DATABRICKS_TEST_HTTP_PATH: ${{ secrets.MB_DATABRICKS_JDBC_TEST_HTTP_PATH }}
MB_DATABRICKS_TEST_TOKEN: ${{ secrets.MB_DATABRICKS_JDBC_TEST_TOKEN }}
MB_DATABRICKS_TEST_CATALOG: metabase_ci
MB_DATABRICKS_TEST_CLIENT_ID: ${{ secrets.MB_DATABRICKS_JDBC_TEST_CLIENT_ID }}
MB_DATABRICKS_TEST_OAUTH_SECRET: ${{ secrets.MB_DATABRICKS_JDBC_TEST_OAUTH_SECRET }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/prepare-backend
- name: Drop dataset
run: >-
clojure -X:dev:drivers:drivers-dev:test
metabase.test.data.impl/drop-dataset!
:driver '"${{ inputs.driver }}"'
:dataset-name '"${{ inputs.dataset-name }}"'

View File

@@ -81,7 +81,7 @@ jobs:
secrets: inherit
with:
skip: ${{ needs.files-changed.outputs.frontend_all != 'true' }}
skip-lint: ${{ needs.files-changed.outputs.frontend_all != 'true' && needs.files-changed.outputs.e2e_specs != 'true' }}
skip-lint: ${{ needs.files-changed.outputs.frontend_all != 'true' && needs.files-changed.outputs.e2e_all != 'true' }}
e2e-tests:
needs: [files-changed, uberjar]
@@ -103,7 +103,8 @@ jobs:
(needs.files-changed.outputs.embedding_sdk_components == 'true' ||
needs.files-changed.outputs.frontend_sources == 'true' ||
needs.files-changed.outputs.embedding_documentation == 'true' ||
needs.files-changed.outputs.embedding_sdk_ci == 'true')
needs.files-changed.outputs.embedding_sdk_ci == 'true' ||
needs.files-changed.outputs.embedding_sdk_host_sample_apps == 'true')
uses: ./.github/workflows/embedding-sdk-package-build.yml
secrets: inherit
@@ -113,6 +114,7 @@ jobs:
uses: ./.github/workflows/embedding-sdk.yml
secrets: inherit
with:
bundle-size-check: ${{ github.event_name == 'pull_request' && needs.uberjar.result == 'success' && needs.files-changed.outputs.frontend_sources == 'true' }}
cli-snippets-type-check: ${{ needs.files-changed.outputs.frontend_sources == 'true' }}
docs-snippets-type-check: ${{ needs.files-changed.outputs.embedding_documentation == 'true' }}
documentation-validation: ${{ needs.files-changed.outputs.frontend_sources == 'true' }}

View File

@@ -15,7 +15,7 @@ jobs:
semantic-search-tests-h2:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 60
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
@@ -70,14 +70,14 @@ jobs:
semantic-search-tests-mariadb:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 60
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
version:
- name: MariaDB 10.2 with pgvector pg17
junit-name: semantic-search-tests-mariadb-10-2
mariadb-image: circleci/mariadb:10.2.23
- name: MariaDB 10.6 with pgvector pg17
junit-name: semantic-search-tests-mariadb-10-6
mariadb-image: circleci/mariadb:10.6
pgvector-image: pgvector/pgvector:pg17
- name: MariaDB Latest with pgvector pg17
junit-name: semantic-search-tests-mariadb-latest
@@ -144,14 +144,14 @@ jobs:
semantic-search-tests-mysql:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 60
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
version:
- name: MySQL 8.0 with pgvector pg17
junit-name: semantic-search-tests-mysql-8-0
mysql-image: cimg/mysql:8.0
- name: MySQL 8.4 with pgvector pg17
junit-name: semantic-search-tests-mysql-8-4
mysql-image: cimg/mysql:8.4
pgvector-image: pgvector/pgvector:pg17
- name: MySQL Latest with pgvector pg17
junit-name: semantic-search-tests-mysql-latest
@@ -218,14 +218,14 @@ jobs:
semantic-search-tests-postgres:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 60
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
version:
- name: Postgres 12.x with pgvector pg17
junit-name: semantic-search-tests-postgres-12
postgres-image: postgres:12-alpine
- name: Postgres 14.x with pgvector pg17
junit-name: semantic-search-tests-postgres-14
postgres-image: postgres:14-alpine
pgvector-image: pgvector/pgvector:pg17
- name: Postgres Latest with pgvector pg17
junit-name: semantic-search-tests-postgres-latest

View File

@@ -0,0 +1,495 @@
name: Transforms Python Stress Test
on:
workflow_dispatch:
inputs:
ref:
description: 'Branch name or commit SHA to checkout'
type: string
required: false
default: ''
driver:
description: 'Which driver(s) to test'
required: true
default: 'all'
type: choice
options:
- all
- bigquery
- clickhouse
- mongo
- mysql
- postgres
- redshift
- snowflake
- sqlserver
jobs:
be-tests-bigquery-transforms:
# inputs.driver is empty on push triggers, so treat '' as 'all'
if: ${{ inputs.driver == 'all' || inputs.driver == '' || inputs.driver == 'bigquery' }}
runs-on: ubuntu-latest
timeout-minutes: 60
env:
CI: 'true'
DRIVERS: bigquery-cloud-sdk
MB_BIGQUERY_TEST_PROJECT_ID: ${{ secrets.BIGQUERY_TEST_PROJECT_ID }}
MB_BIGQUERY_TEST_CLIENT_ID: ${{ secrets.MB_BIGQUERY_TEST_CLIENT_ID }}
MB_BIGQUERY_TEST_CLIENT_SECRET: ${{ secrets.MB_BIGQUERY_TEST_CLIENT_SECRET }}
MB_BIGQUERY_TEST_ACCESS_TOKEN: ${{ secrets.MB_BIGQUERY_TEST_ACCESS_TOKEN }}
MB_BIGQUERY_TEST_REFRESH_TOKEN: ${{ secrets.MB_BIGQUERY_TEST_REFRESH_TOKEN }}
MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON: ${{ secrets.MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON }}
MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON_ROUTING: ${{ secrets.MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON_ROUTING }}
name: BigQuery Transforms
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup python-runner
uses: ./.github/actions/setup-python-runner
with:
metabase-automation-token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
- name: Test BigQuery transforms
id: test-driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-bigquery-transforms'
test-args: >-
:only-tags [:mb/transforms-python-test]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
- name: Cleanup python-runner
if: always()
uses: ./.github/actions/cleanup-python-runner
with:
upload-logs: ${{ steps.test-driver.outcome == 'failure' }}
logs-artifact-suffix: BigQuery Transforms
be-tests-clickhouse-transforms:
if: ${{ inputs.driver == 'all' || inputs.driver == '' || inputs.driver == 'clickhouse' }}
runs-on: ubuntu-latest
timeout-minutes: 60
env:
CI: 'true'
DRIVERS: clickhouse
MB_CLICKHOUSE_TEST_HOST: localhost
MB_CLICKHOUSE_TEST_PORT: 8123
MB_CLICKHOUSE_TEST_OLD_PORT: 8124
MB_CLICKHOUSE_TEST_NGINX_PORT: 8127
name: ClickHouse Transforms
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup python-runner
uses: ./.github/actions/setup-python-runner
with:
metabase-automation-token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
- name: Add ClickHouse TLS instance to /etc/hosts
run: |
sudo echo "127.0.0.1 server.clickhouseconnect.test" | sudo tee -a /etc/hosts
- name: Start ClickHouse in Docker
uses: hoverkraft-tech/compose-action@v2.0.0
with:
compose-file: "modules/drivers/clickhouse/docker-compose.yml"
down-flags: "--volumes"
services: |
clickhouse
clickhouse_tls
clickhouse_older_version
clickhouse_cluster_node1
clickhouse_cluster_node2
nginx
- name: Test ClickHouse transforms
id: test-driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-clickhouse-transforms'
test-args: >-
:only-tags [:mb/transforms-python-test]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
- name: Cleanup python-runner
if: always()
uses: ./.github/actions/cleanup-python-runner
with:
upload-logs: ${{ steps.test-driver.outcome == 'failure' }}
logs-artifact-suffix: ClickHouse Transforms
be-tests-mongo-transforms:
if: ${{ inputs.driver == 'all' || inputs.driver == '' || inputs.driver == 'mongo' }}
runs-on: ubuntu-latest
timeout-minutes: 60
env:
CI: 'true'
DRIVERS: mongo
MB_MONGO_TEST_USER: metabase
MB_MONGO_TEST_PASSWORD: metasample123
services:
mongodb:
image: mongo:latest
ports:
- "27017:27017"
env:
MONGO_INITDB_ROOT_USERNAME: metabase
MONGO_INITDB_ROOT_PASSWORD: metasample123
name: MongoDB Transforms
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup python-runner
uses: ./.github/actions/setup-python-runner
with:
metabase-automation-token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
- name: Test MongoDB transforms
id: test-driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mongo-transforms'
test-args: >-
:only-tags [:mb/transforms-python-test]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
- name: Cleanup python-runner
if: always()
uses: ./.github/actions/cleanup-python-runner
with:
upload-logs: ${{ steps.test-driver.outcome == 'failure' }}
logs-artifact-suffix: MongoDB Transforms
be-tests-mysql-transforms:
if: ${{ inputs.driver == 'all' || inputs.driver == '' || inputs.driver == 'mysql' }}
runs-on: ubuntu-latest
timeout-minutes: 60
services:
mysql:
image: mysql:latest
ports:
- "3306:3306"
env:
MYSQL_ALLOW_EMPTY_PASSWORD: true
MYSQL_DATABASE: circle_test
env:
CI: 'true'
DRIVERS: mysql
MB_DB_TYPE: mysql
MB_DB_HOST: localhost
MB_DB_PORT: 3306
MB_DB_DBNAME: circle_test
MB_DB_USER: root
MB_MYSQL_TEST_USER: root
name: MySQL Transforms
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup python-runner
uses: ./.github/actions/setup-python-runner
with:
metabase-automation-token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
- name: Test MySQL transforms
id: test-driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mysql-transforms'
test-args: >-
:only-tags [:mb/transforms-python-test]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
- name: Cleanup python-runner
if: always()
uses: ./.github/actions/cleanup-python-runner
with:
upload-logs: ${{ steps.test-driver.outcome == 'failure' }}
logs-artifact-suffix: MySQL Transforms
be-tests-postgres-transforms:
if: ${{ inputs.driver == 'all' || inputs.driver == '' || inputs.driver == 'postgres' }}
runs-on: ubuntu-latest
timeout-minutes: 60
env:
CI: 'true'
DRIVERS: postgres
MB_DB_TYPE: postgres
MB_DB_PORT: 5432
MB_DB_HOST: localhost
MB_DB_DBNAME: mb_test
MB_DB_USER: mb_test
MB_POSTGRESQL_TEST_USER: mb_test
services:
postgres:
image: postgres:latest
ports:
- "5432:5432"
env:
POSTGRES_USER: mb_test
POSTGRES_DB: mb_test
POSTGRES_HOST_AUTH_METHOD: trust
name: Postgres Transforms
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup python-runner
uses: ./.github/actions/setup-python-runner
with:
metabase-automation-token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
- name: Test Postgres transforms
id: test-driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-postgres-transforms'
test-args: >-
:only-tags [:mb/transforms-python-test]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
- name: Cleanup python-runner
if: always()
uses: ./.github/actions/cleanup-python-runner
with:
upload-logs: ${{ steps.test-driver.outcome == 'failure' }}
logs-artifact-suffix: Postgres Transforms
be-tests-redshift-transforms:
if: ${{ inputs.driver == 'all' || inputs.driver == '' || inputs.driver == 'redshift' }}
runs-on: ubuntu-latest
timeout-minutes: 60
env:
CI: 'true'
DRIVERS: redshift
MB_REDSHIFT_TEST_USER: metabase_ci
MB_REDSHIFT_TEST_DB: testdb
MB_REDSHIFT_TEST_DB_ROUTING: dev
MB_REDSHIFT_TEST_HOST: ${{ secrets.MB_REDSHIFT_TEST_HOST }}
MB_REDSHIFT_TEST_PASSWORD: ${{ secrets.MB_REDSHIFT_TEST_PASSWORD }}
name: Redshift Transforms
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup python-runner
uses: ./.github/actions/setup-python-runner
with:
metabase-automation-token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
- name: Test Redshift transforms
id: test-driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-redshift-transforms'
test-args: >-
:only-tags [:mb/transforms-python-test]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
- name: Cleanup python-runner
if: always()
uses: ./.github/actions/cleanup-python-runner
with:
upload-logs: ${{ steps.test-driver.outcome == 'failure' }}
logs-artifact-suffix: Redshift Transforms
be-tests-snowflake-transforms:
if: ${{ inputs.driver == 'all' || inputs.driver == '' || inputs.driver == 'snowflake' }}
runs-on: ubuntu-latest
timeout-minutes: 60
env:
CI: 'true'
DRIVERS: snowflake
MB_SNOWFLAKE_TEST_USER: METABASE CI
MB_SNOWFLAKE_TEST_ACCOUNT: ${{ secrets.MB_SNOWFLAKE_TEST_ACCOUNT }}
MB_SNOWFLAKE_TEST_PASSWORD: ${{ secrets.MB_SNOWFLAKE_TEST_PASSWORD }}
MB_SNOWFLAKE_TEST_PRIVATE_KEY: ${{ secrets.MB_SNOWFLAKE_TEST_PRIVATE_KEY }}
MB_SNOWFLAKE_TEST_WAREHOUSE: ${{ secrets.MB_SNOWFLAKE_TEST_WAREHOUSE }}
MB_SNOWFLAKE_TEST_PK_USER: METABASE PK
MB_SNOWFLAKE_TEST_PK_PRIVATE_KEY: ${{ secrets.MB_SNOWFLAKE_TEST_PK_PRIVATE_KEY }}
MB_SNOWFLAKE_TEST_PK_PRIVATE_KEY_2: ${{ secrets.MB_SNOWFLAKE_TEST_PK_PRIVATE_KEY_2 }}
MB_SNOWFLAKE_TEST_PK_PUBLIC_KEY: ${{ secrets.MB_SNOWFLAKE_TEST_PK_PUBLIC_KEY }}
MB_SNOWFLAKE_TEST_PK_PUBLIC_KEY_2: ${{ secrets.MB_SNOWFLAKE_TEST_PK_PUBLIC_KEY_2 }}
MB_SNOWFLAKE_TEST_RSA_ROLE_TEST_DEFAULT_USER: RSA_ROLE_TEST_DEFAULT_USER
MB_SNOWFLAKE_TEST_RSA_ROLE_TEST_CUSTOM_USER: RSA_ROLE_TEST_CUSTOM_USER
MB_SNOWFLAKE_TEST_RSA_ROLE_TEST_ROLE: RSA_ROLE_TEST_ROLE
MB_SNOWFLAKE_TEST_RSA_ROLE_TEST_DB: RSA_ROLE_TEST_DB
name: Snowflake Transforms
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup python-runner
uses: ./.github/actions/setup-python-runner
with:
metabase-automation-token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
- name: Test Snowflake transforms
id: test-driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-snowflake-transforms'
test-args: >-
:only-tags [:mb/transforms-python-test]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
- name: Cleanup python-runner
if: always()
uses: ./.github/actions/cleanup-python-runner
with:
upload-logs: ${{ steps.test-driver.outcome == 'failure' }}
logs-artifact-suffix: Snowflake Transforms
be-tests-sqlserver-transforms:
if: ${{ inputs.driver == 'all' || inputs.driver == '' || inputs.driver == 'sqlserver' }}
runs-on: ubuntu-latest
timeout-minutes: 60
env:
CI: 'true'
DRIVERS: sqlserver
MB_SQLSERVER_TEST_HOST: localhost
MB_SQLSERVER_TEST_PASSWORD: 'P@ssw0rd'
MB_SQLSERVER_TEST_USER: SA
services:
sqlserver:
image: mcr.microsoft.com/mssql/server:2022-latest
ports:
- "1433:1433"
env:
ACCEPT_EULA: Y
SA_PASSWORD: 'P@ssw0rd'
MSSQL_MEMORY_LIMIT_MB: 1024
name: SQL Server Transforms
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup python-runner
uses: ./.github/actions/setup-python-runner
with:
metabase-automation-token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
- name: Test SQL Server transforms
id: test-driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-sqlserver-transforms'
test-args: >-
:only-tags [:mb/transforms-python-test]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
- name: Cleanup python-runner
if: always()
uses: ./.github/actions/cleanup-python-runner
with:
upload-logs: ${{ steps.test-driver.outcome == 'failure' }}
logs-artifact-suffix: SQL Server Transforms
transforms-stress-test-result:
needs:
- be-tests-bigquery-transforms
- be-tests-clickhouse-transforms
- be-tests-mongo-transforms
- be-tests-mysql-transforms
- be-tests-postgres-transforms
- be-tests-redshift-transforms
- be-tests-snowflake-transforms
- be-tests-sqlserver-transforms
runs-on: ubuntu-latest
timeout-minutes: 5
name: transforms-stress-test-result
if: always() && !cancelled()
steps:
- name: Check job status
uses: actions/github-script@v7
env:
needs: ${{ toJson(needs) }}
with:
script: |
const needs = JSON.parse(process.env.needs);
const jobs = Object.entries(needs).map(
([jobName, jobValues]) => ({
name: jobName,
result: jobValues.result
}));
if (jobs.every(job => (job.result === 'skipped' || job.result === 'success'))) {
console.log("All transforms tests have passed (or been skipped).");
process.exit(0);
}
console.log("Some transforms tests have failed:");
jobs.forEach((job) => {
if (job.result !== 'success' && job.result !== 'skipped') {
console.log(` ${job.name} - ${job.result}`);
}
});
process.exit(1);

View File

@@ -0,0 +1,50 @@
name: Crowdin Context Harvester
on:
workflow_dispatch:
schedule:
- cron: "30 6 * * 2" # Tuesday at 06:30 UTC, one hour after translation-update.yml
jobs:
harvest:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Harvest and write AI context to Crowdin
timeout-minutes: 60
env:
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
OPENAI_KEY: ${{ secrets.CROWDIN_OPENAI_API_KEY }}
run: |
npx --yes crowdin-context-harvester@latest harvest \
--project="$CROWDIN_PROJECT_ID" \
--ai="openai" \
--model="gpt-5.1" \
--croql='not (context contains "✨ AI Context")' \
--output="crowdin" \
--concurrency=10
# We only want to trigger pre-translations, not wait for them to finish running upstream.
# Weirdly, Crowdin keeps this step running as long as it runs pre-translations on their servers.
# Hence, the three minute timeout and continue-on-error.
- name: Trigger pre-translations
timeout-minutes: 3
uses: crowdin/github-action@v2
# https://crowdin.github.io/crowdin-cli/commands/crowdin-pre-translate
with:
command: "pre-translate"
command_args: >-
--method ai
--ai-prompt ${{ secrets.CROWDIN_AI_PROMPT_ID }}
--translate-untranslated-only
--token ${{ secrets.CROWDIN_TOKEN }}
--project-id ${{ secrets.CROWDIN_PROJECT_ID }}
--config=locales/crowdin.yml
--no-progress
continue-on-error: true

View File

@@ -1,70 +0,0 @@
# This allows Tusk to manually run workflows to check lint/build/tests before creating a PR.
# If you have any questions, please reach out to support@usetusk.ai.
name: Tusk Sanity Check
on:
workflow_dispatch:
inputs:
taskId:
description: "Tusk Task ID"
required: true
runType:
description: "Tusk Run Type"
required: true
runId:
description: "Tusk Run ID"
required: true
jobs:
sanity_check:
runs-on: ubuntu-latest
steps:
- name: Log inputs
run: |
echo "Tusk Task ID: ${{ github.event.inputs.taskId }}"
echo "Tusk Run Type: ${{ github.event.inputs.runType }}"
echo "Tusk Run ID: ${{ github.event.inputs.runId }}"
echo "Current Branch: ${{ github.ref }}"
echo "Repository default branch: ${{ github.event.repository.default_branch }}"
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
fetch-depth: 0
- name: Check which files changed
uses: dorny/paths-filter@v3.0.0
id: changes
with:
token: ${{ github.token }}
filters: .github/file-paths.yaml
# Similar to .github/workflows/frontend.yml
- name: Prepare front-end environment
if: steps.changes.outputs.frontend_all == 'true'
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- run: bun run build-pure:cljs
name: Compile CLJS
- name: Run lint-staged
if: steps.changes.outputs.frontend_all == 'true'
run: |
BASE_COMMIT=$(git merge-base origin/${{ github.event.repository.default_branch }} HEAD)
npx lint-staged --shell --no-stash --diff="$BASE_COMMIT...HEAD"
# You must include this step after running all auto-fixing steps
- uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "fix(${{ github.run_id }}): auto linting" # The commit message MUST start with "fix(${{ github.run_id }}):"
commit_options: "--no-verify" # No verify because we already ran it above
skip_fetch: true
skip_checkout: true
status_options: "--untracked-files=no"
add_options: "--update"
- name: Check types
if: steps.changes.outputs.frontend_all == 'true'
run: bun run type-check-pure

25
.gitignore vendored
View File

@@ -1,5 +1,6 @@
*.class
*.*.rej
*.d.css
*.iml
*.jar
*.log
@@ -16,6 +17,7 @@
.nrepl-port
.portal
.vscode
/.zed
/*.h2.db
/*.lock.db
/*.mv.db
@@ -24,6 +26,7 @@
/.env*
!/.env.example
/.envrc
/.workmux.yaml
/.lein-env
/.lein-failures
/.lein-plugins
@@ -43,6 +46,7 @@ cypress.env.json
/docs/**/package.json
/docs/**/yarn.lock
/docs/embedding/sdk/api/
/docs/embedding/eajs/snippets/
/frontend/test/snapshots/*
/e2e/snapshots/*
/e2e/tmp/*
@@ -63,6 +67,7 @@ cypress.env.json
/resources/frontend_client/app/embed.js
/resources/frontend_client/app/embedding-sdk.js
/resources/frontend_client/app/embedding-sdk.js.map
/resources/frontend_client/app/embedding-sdk/
/resources/frontend_client/tmp-*/
/resources/frontend_client/embed.html
/resources/frontend_client/embed-sdk.html
@@ -73,6 +78,7 @@ cypress.env.json
/resources/instance_analytics.zip
/resources/license-backend-third-party.txt
/resources/license-frontend-third-party.txt
/resources/metabase/config/modules.edn
/resources/namespaces.edn
/resources/sample-dataset.db.trace.db
/resources/version.properties
@@ -121,13 +127,13 @@ modules/drivers/*/.lsp/*
**/.clj-kondo/.cache
# clj-kondo: ignore all except our defined config
.clj-kondo/*
!.clj-kondo/README.md
!.clj-kondo/config.edn
!.clj-kondo/config/
!.clj-kondo/src/
!.clj-kondo/test/
!.clj-kondo/macros/
**/.clj-kondo/*
!**/.clj-kondo/README.md
!**/.clj-kondo/config.edn
!**/.clj-kondo/config/
!**/.clj-kondo/src/
!**/.clj-kondo/test/
!**/.clj-kondo/macros/
# Editor- or environment-specific local files
*.code-workspace
@@ -165,3 +171,8 @@ CLAUDE.local.md
/logs
/.clojure-mcp
# Python dependencies (pip-installed, not committed)
/resources/python-sources/sqlglot/
/resources/python-sources/sqlglot-*.dist-info/
/resources/python-sources/.lock
__pycache__/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -0,0 +1,151 @@
import {
ArrayType,
Comment,
Converter,
IntrinsicType,
LiteralType,
ReflectionFlag,
ReflectionKind,
UnionType,
} from "typedoc";
/**
* Check if a UnionType has ALL members as string LiteralTypes.
*/
function isAllStringLiteralUnion(type) {
if (!(type instanceof UnionType)) {
return false;
}
return (
type.types.length > 0 &&
type.types.every(
(t) => t instanceof LiteralType && typeof t.value === "string",
)
);
}
/**
* Extract the text content from a Comment tag's content array.
*/
function getTagText(tag) {
if (!tag.content || tag.content.length === 0) {
return "";
}
return tag.content.map((part) => part.text).join("");
}
/**
* Transform @remarks text into a human-readable sentence.
*
* "Guest embed" → "Available in Guest embed."
* "Pro/Enterprise" → "Available in Pro/Enterprise."
* "Pro/Enterprise, Guest embed" → "Available in Pro/Enterprise and Guest embed."
*/
function transformRemarks(text) {
const trimmed = text.trim();
if (trimmed.includes(",")) {
const parts = trimmed.split(",").map((s) => s.trim());
const last = parts.pop();
return `Available in ${parts.join(", ")} and ${last}.`;
}
return `Available in ${trimmed}.`;
}
/**
* A TypeDoc plugin that transforms property reflections before markdown
* rendering to produce cleaner props documentation tables.
*
* For each property on interfaces/type aliases:
* 1. Moves optional `?` markers into the description as "Optional."
* 2. Extracts @defaultValue tags into the description as "Default: `value`."
* 3. Replaces all-string-literal union types with `string` and lists possible values
* 4. Transforms @remarks tags into human-readable availability notes
*/
export function load(app) {
app.converter.on(Converter.EVENT_RESOLVE_END, (context) => {
const project = context.project;
for (const reflection of project.getReflectionsByKind(
ReflectionKind.Property,
)) {
const prefix = [];
// 1. Optional: move ? into description
if (reflection.flags.isOptional) {
prefix.push("Optional");
reflection.setFlag(ReflectionFlag.Optional, false);
}
// 2. Default value: extract @defaultValue tag
if (reflection.comment) {
const defaultTag = reflection.comment.getTag("@defaultValue");
if (defaultTag) {
const defaultText = getTagText(defaultTag).trim();
// If the value already contains backticks, use as-is
if (defaultText.includes("`")) {
prefix.push(`Default: ${defaultText}`);
} else {
prefix.push(`Default: \`${defaultText}\``);
}
reflection.comment.removeTags("@defaultValue");
}
}
// 3. Complex union types: simplify all-string-literal unions
let unionType = null;
if (isAllStringLiteralUnion(reflection.type)) {
unionType = reflection.type;
reflection.type = new IntrinsicType("string");
} else if (
reflection.type instanceof ArrayType &&
isAllStringLiteralUnion(reflection.type.elementType)
) {
unionType = reflection.type.elementType;
reflection.type = new ArrayType(new IntrinsicType("string"));
}
if (unionType) {
const values = unionType.types
.map((t) => `\`"${t.value}"\``)
.join(", ");
prefix.push(`Possible values: ${values}`);
}
// 4. Remarks: transform into availability notes
if (reflection.comment) {
const remarksTag = reflection.comment.getTag("@remarks");
if (remarksTag) {
const remarksText = getTagText(remarksTag);
if (remarksText.trim()) {
prefix.push(transformRemarks(remarksText));
}
reflection.comment.removeTags("@remarks");
}
}
// Assemble prefix and prepend to comment summary
if (prefix.length > 0) {
if (!reflection.comment) {
reflection.comment = new Comment();
}
const prefixText = ["---", ...prefix].join("<br>");
const existingSummary = reflection.comment.summary || [];
if (existingSummary.length > 0) {
reflection.comment.summary = [
...existingSummary,
{ kind: "text", text: "<br>" + prefixText },
];
} else {
reflection.comment.summary = [{ kind: "text", text: prefixText }];
}
}
}
});
}

View File

@@ -5,6 +5,7 @@ const eajsMarkdownConfig = {
plugin: [
"typedoc-plugin-mdn-links",
"typedoc-plugin-markdown",
"./typedoc-plugin-props-doc-transform.js",
"./typedoc-plugin-markdown-prepare-for-embedding.js",
],
entryPoints: [
@@ -32,7 +33,7 @@ const eajsMarkdownConfig = {
typeAliasPropertiesFormat: "table",
typeDeclarationFormat: "table",
tableColumnSettings: {
hideDefaults: false,
hideDefaults: true,
hideInherited: true,
hideModifiers: true,
hideOverrides: true,

View File

@@ -9,6 +9,7 @@
"github.vscode-pull-request-github",
"limejuly.vscode-yaml-block-highlight",
"streetsidesoftware.code-spell-checker",
"styled-components.vscode-styled-components"
"styled-components.vscode-styled-components",
"vunguyentuan.vscode-css-variables"
]
}

View File

@@ -4,5 +4,6 @@
"jake.autoDetect": "off",
"gulp.autoDetect": "off",
"npm.autoDetect": "off",
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"cssVariables.lookupFiles": ["**/*.d.css", "node_modules/@mantine/core/*.css"]
}

View File

@@ -25,6 +25,18 @@ For detailed guidance on writing and reviewing code and documentation, see the s
**Important**: When working with frontend code, read [frontend/CLAUDE.md](frontend/CLAUDE.md) for project-specific guidelines on component preferences, styling, TypeScript migration, testing requirements, and available scripts.
## Running Backend Tests
Use `./bin/test-agent` to run Clojure tests. It produces clean, plain-text output with no progress bars or ANSI codes.
```bash
./bin/test-agent :only '[metabase.foo-test]' # run a namespace
./bin/test-agent :only '[metabase.foo-test/some-test]' # run a single test
./bin/test-agent :only '[metabase.foo-test metabase.bar-test]' # multiple namespaces
```
Do not use `clj -X:dev:test` directly — its progress-bar output is hard to parse.
## Tool Preferences
If `clojure-mcp` tools are available, prefer them over shell-based alternatives for Clojure development.

View File

@@ -10,20 +10,24 @@
## Get started
The easiest way to get started with Metabase is to sign up for a free trial of [Metabase Cloud](https://store.metabase.com/checkout). You get support, backups, upgrades, an SMTP server, SSL certificate, SoC2 Type 2 security auditing, and more (plus your money goes toward improving Metabase). Check out our quick overview of [cloud vs self-hosting](https://www.metabase.com/docs/latest/cloud/cloud-vs-self-hosting). If you need to, you can always switch to [self-hosting](https://www.metabase.com/docs/latest/installation-and-operation/installing-metabase) Metabase at any time (or vice versa).
The easiest way to get started with Metabase is to sign up for a free trial of [Metabase Cloud](https://store.metabase.com/checkout).
You get expert support, backups, upgrades, an SMTP server, SSL certificate, SoC2 Type 2 security auditing, and more (plus your money goes toward improving a major open-source project). Check out our quick overview of [cloud vs self-hosting](https://www.metabase.com/docs/latest/cloud/cloud-vs-self-hosting). If you need to, you can always switch to [self-hosting](https://www.metabase.com/docs/latest/installation-and-operation/installing-metabase) Metabase at any time (or vice versa).
## Key Features
- [Set up in five minutes](https://www.metabase.com/docs/latest/configuring-metabase/setting-up-metabase) (we're not kidding).
- [Set up in five minutes](https://www.metabase.com/docs/latest/configuring-metabase/setting-up-metabase) (we're not kidding), or have us [host Metabase for you](https://www.metabase.com/cloud/) so you don't even need to think about it.
- Let anyone on your team [ask questions](https://www.metabase.com/docs/latest/questions/introduction) without knowing SQL.
- Use the [SQL editor](https://www.metabase.com/docs/latest/questions/native-editor/writing-sql) for more complex queries.
- Build handsome, interactive [dashboards](https://www.metabase.com/docs/latest/dashboards/introduction) with filters, auto-refresh, fullscreen, and custom click behavior.
- Create [models](https://www.metabase.com/learn/metabase-basics/getting-started/models) that clean up, annotate, and/or combine raw tables.
- Define canonical [segments and metrics](https://www.metabase.com/docs/latest/data-modeling/metrics) for your team to use.
- Send data to Slack or email on a schedule with [dashboard subscriptions](https://www.metabase.com/docs/latest/dashboards/subscriptions).
- Set up [alerts](https://www.metabase.com/docs/latest/questions/alerts) to have Metabase notify you when your data changes.
- [Embed charts and dashboards](https://www.metabase.com/docs/latest/embedding/introduction) in your app, or even [your entire Metabase](https://www.metabase.com/docs/latest/embedding/interactive-embedding).
- Build with the [Embedded Analytics SDK for React](https://www.metabase.com/docs/latest/embedding/sdk/introduction) to embed standalone components with custom styling that matches your app's design.
- Ask AI: [Metabot](https://www.metabase.com/docs/latest/ai/metabot) gives you answers you can trust, helps you write queries, and more. Or build your own [AI agent](https://www.metabase.com/docs/latest/ai/agent-api) to query your data.
- Build handsome, interactive [dashboards](https://www.metabase.com/docs/latest/dashboards/introduction) with filters, auto-refresh, fullscreen, custom click behavior, and more.
- Use [documents](https://www.metabase.com/docs/latest/documents/introduction) for long-form data analysis, and invite people to comment.
- [Transform](https://www.metabase.com/docs/latest/data-studio/transforms/transforms-overview) raw data into analytics-ready tables, track down broken dependencies, and define canonical metrics in Metabase's [Data Studio](https://www.metabase.com/docs/latest/data-studio/overview).
- Set up [alerts on your data](https://www.metabase.com/docs/latest/questions/alerts), or schedule [dashboard subscriptions](https://www.metabase.com/docs/latest/dashboards/subscriptions) to email, Slack, or even a webhook.
- Curate content in a [Library](https://www.metabase.com/docs/latest/data-studio/library), and [version your work with Git](https://www.metabase.com/docs/latest/installation-and-operation/remote-sync).
- [Embed Metabase in your app](https://www.metabase.com/docs/latest/embedding/introduction), with components for charts, dashboards, data browser, AI chat, and more. You can even put [an entire Metabase](https://www.metabase.com/docs/latest/embedding/interactive-embedding) in your app.
- Set granular [permissions](https://www.metabase.com/docs/latest/permissions/introduction) that work for both internal teams and embedded analytics, whether you co-locate your customer data, or give each customer their own database.
- Dark mode, content translations, and way more stuff than we can list here.
Take a [tour of Metabase](https://www.metabase.com/learn/metabase-basics/overview/tour-of-metabase).
@@ -36,57 +40,22 @@ Take a [tour of Metabase](https://www.metabase.com/learn/metabase-basics/overvie
Metabase can be run just about anywhere. Check out our [Installation Guides](https://www.metabase.com/docs/latest/installation-and-operation/installing-metabase).
## Documentation
The [Metabase handbook](https://www.metabase.com/docs/latest/).
## Contributing
## Quick Setup: Dev environment
To contribute to Metabase, see our [Developer docs](./docs/developers-guide/start.md).
In order to spin up a development environment, you need to start the front end and the backend as follows:
## Extending Metabase
### Frontend quick setup
The following command will install the JavaScript dependencies:
```bash
bun install
```
To build and run without watching changes:
```bash
bun run build
```
To build and run with hot-reload:
```bash
bun run build-hot
```
### Backend quick setup
To run the backend, you'll need to build the drivers first, and then start the backend:
```bash
./bin/build-drivers.sh
clojure -M:run
```
For a more detailed setup of a dev environment for Metabase, check out our [Developers Guide](./docs/developers-guide/start.md).
Hit our API to integrate analytics. Check out our guide, [Working with the Metabase API](https://www.metabase.com/learn/metabase-basics/administration/administration-and-operation/metabase-api).
## Internationalization
We want Metabase to be available in as many languages as possible. See which translations are available and help contribute to internationalization using our project over at [Crowdin](https://crowdin.com/project/metabase-i18n). You can also check out our [policies on translations](https://www.metabase.com/docs/latest/administration-guide/localization.html).
## Extending Metabase
Hit our Query API from JavaScript to integrate analytics. Metabase enables your application to:
- Build moderation interfaces.
- Export subsets of your users to third party marketing automation software.
- Provide a custom customer lookup application for the people in your company.
Check out our guide, [Working with the Metabase API](https://www.metabase.com/learn/metabase-basics/administration/administration-and-operation/metabase-api).
## Security Disclosure
See [SECURITY.md](./SECURITY.md) for details.

65
bb.edn
View File

@@ -162,6 +162,15 @@
(p/shell "bin/pre-lint-migrations-file.sh")
(lint-migrations-file/-main))}
new-migration
{:doc "Create a new directory-based migration file (v60+)"
:requires [[mage.new-migration :as new-migration]]
:examples [["./bin/mage new-migration 60 mq_indexes" "Create resources/migrations/060/20260905_mq_indexes.yaml"]]
:arg-schema [:tuple
[:int {:name "version" :description "Major version number (e.g., 60)"}]
[:string {:name "description" :description "Lowercase alphanumeric/underscore name (e.g., mq_indexes)"}]]
:task (task! (new-migration/new-migration parsed))}
jar-download
{:doc "Download (and optionally run) jar for a metabase version or branch"
:examples [["./bin/mage jar-download 50" "Download the latest enterprise version of release 50 to ~/path/to/my/metabase/jars"]
@@ -247,6 +256,14 @@
:requires [[mage.util :as u]]
:task (task! (u/print-tasks parsed))}
generate-cursive-resolve-as
{:doc "Generate XML configuration for Cursive's symbol resolution based on the current clj-kondo lint-as configuration."
:examples [["./bin/mage generate-cursive-resolve-as" "Generate resolve-as XML configuration"]
["./bin/mage generate-cursive-resolve-as > /Users/bgrabow/Library/Application\\ Support/JetBrains/IntelliJIdea2025.3/options/ClojureResolveSettings.xml"
"Generate resolve-as XML configuration and install it in your global editor configuration"]]
:requires [[mage.cursive-resolve-as :as cursive-resolve-as]]
:task (task! (cursive-resolve-as/do-it))}
analytics:setup
{:doc "Set up usage analytics development mode"
:examples [["./bin/mage analytics:setup" "Enable usage analytics dev mode and import content"]]
@@ -302,7 +319,7 @@
"If you see 'gh: command not found' or auth errors, run: gh auth login"))
:requires [[mage.ci-report :as ci-report]]
:task (task! (ci-report/generate-report! parsed))}
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Private Tasks:
;; These tasks are more for internal tooling, e.g. for use from git hooks etc.
@@ -334,12 +351,13 @@
"RUN"
(str "any of " (pr-str driver-triggers) " affected by cloud-driver module changes")]
["7" "Cloud driver + query-processor updated" "RUN" "query-processor module updated"]
["8" "Cloud driver, no relevant changes" "SKIP" "no relevant changes for cloud driver"]
["9"
["8" "Cloud driver + driver deps affected" "RUN" "driver module affected by shared code changes"]
["9" "Cloud driver, no relevant changes" "SKIP" "no relevant changes for cloud driver"]
["10"
(str (pr-str driver-triggers) " module deps affected")
"RUN"
(str "any of " (pr-str driver-triggers) " affected by module changes")]
["10" "Self-hosted driver, not affected" "SKIP" "driver module not affected"]])))))
["11" "Self-hosted driver, not affected" "SKIP" "driver module not affected"]])))))
:options [[nil "--git-ref REF" "The git ref to compute changed modules against"]
[nil "--is-master-or-release MASTER_OR_RELEASE"]
[nil "--pr-labels PR_LABELS" "Comma delimited labels on the pr"]
@@ -525,4 +543,41 @@
:requires [[mage.escherize.fix-whitespace :as fix-whitespace]]
:examples [["mage -escherize-fix-whitespace '**/*.clj{,c,s}'" "Removes all lines like ' ' from all clj files"]]
:arg-schema [:sequential [:string {:name "glob-pattern" :description "Glob pattern to match files"}]]
:task (task! (fix-whitespace/run-clean parsed))}}}
:task (task! (fix-whitespace/run-clean parsed))}
-edpaget-dev-env
{:doc "Stand up (or tear down) an isolated dev environment for this worktree"
:examples [["./bin/mage -edpaget-dev-env --app-db postgres" "Postgres app-db with auto ports"]
["./bin/mage -edpaget-dev-env --app-db postgres --with mysql --with mongo" "Full stack"]
["./bin/mage -edpaget-dev-env" "Minimal: just port config (H2 app-db)"]
["./bin/mage -edpaget-dev-env --down" "Tear down containers for this worktree"]
["./bin/mage -edpaget-dev-env --status" "Show running containers and ports"]]
:requires [[mage.edpaget.dev-env :as dev-env]]
:options [[nil "--app-db DB" "App database: h2 (default), postgres, mysql" :default "h2"]
[nil "--with SERVICE" "Warehouse service (repeatable): postgres, mysql, mariadb, mongo, clickhouse, ldap, maildev"
:multi true :default [] :update-fn conj]
[nil "--down" "Tear down all containers for this worktree"]
[nil "--status" "Show current env status"]
[nil "--slot SLOT" "Override port slot (0-99)" :parse-fn Integer/parseInt]]
:task (task! (dev-env/dev-env! parsed))}
-johnswanson-dev-env
{:doc "Stand up (or tear down) an isolated dev environment for this worktree. Interactive prompts when flags omitted."
:examples [["./bin/mage -johnswanson-dev-env" "Interactive TUI to configure infra (saves config)"]
["./bin/mage -johnswanson-dev-env --run" "Configure + launch backend/frontend"]
["./bin/mage -johnswanson-dev-env --run --no-frontend" "Configure + backend only"]
["./bin/mage -johnswanson-dev-env --down" "Stop everything for this worktree"]
["./bin/mage -johnswanson-dev-env --status" "Show running containers and processes"]]
:requires [[mage.johnswanson.dev-env :as jds-dev-env]]
:options [["-e" "--edition EDITION" "ee or oss (default: prompt)"]
["-t" "--token TOKEN" "Token type: all-features, starter-cloud, pro-cloud, pro-self-hosted, none"]
[nil "--app-db DB" "App database: h2, postgres, mysql, mariadb (default: prompt)"]
[nil "--with SERVICE" "Warehouse service (repeatable): postgres, mysql, mariadb, mongo, clickhouse, ldap, maildev"
:multi true :default [] :update-fn conj]
[nil "--run" "Also launch backend + frontend in background"]
[nil "--no-frontend" "Skip frontend dev server (only with --run)"]
[nil "--down" "Stop everything for this worktree"]
[nil "--status" "Show current env status"]
[nil "--slot SLOT" "Override port slot (0-99)" :parse-fn Integer/parseInt]
[nil "--fresh" "Recreate containers from scratch"]]
:task (task! (jds-dev-env/dev-env! parsed))}}}

View File

@@ -2,6 +2,7 @@
(:require
[build-drivers :as build-drivers]
[build.licenses :as license]
[build.python :as build.python]
[build.uberjar :as uberjar]
[build.version-properties :as version-properties]
[clojure.edn :as edn]
@@ -90,6 +91,8 @@
(build-licenses! edition))
:drivers (fn [{:keys [edition]}]
(build-drivers/build-drivers! edition))
:python (fn [{:keys [edition]}]
(build.python/build-python-deps! edition))
:uberjar (fn [{:keys [edition]}]
(build-uberjar! edition))))

View File

@@ -0,0 +1,38 @@
(ns build.python
"Build step to install Python dependencies (sqlglot) for GraalVM Python."
(:require
[clojure.java.io :as io]
[environ.core :as env]
[metabuild-common.core :as u]))
(set! *warn-on-reflection* true)
(def ^:private python-sources-dir
"Directory where sqlglot and sql_tools.py shim live."
(u/filename u/project-root-directory "resources" "python-sources"))
(def ^:private sqlglot-version
"Pinned sqlglot version for reproducible builds.
Read from pyproject.toml (single source of truth for both dev and build)."
(->> (io/file u/project-root-directory "resources/python-sources/pyproject.toml")
slurp
(re-find #"\"sqlglot==([^\"]+)\"")
second))
(defn build-python-deps!
"Install sqlglot to resources/python-sources for bundling into uberjar.
Uses `uv pip install`. The --no-compile flag skips .pyc generation since GraalVM Python
uses interpreted mode.
Can be skipped by setting SKIP_PYTHON_DEPS=true."
[_edition]
(if (= (env/env :skip-python-deps) "true")
(u/announce "Skipping Python dependencies (SKIP_PYTHON_DEPS=true)")
(u/step "Install Python dependencies (sqlglot)"
(u/create-directory-unless-exists! python-sources-dir)
(u/sh {:dir u/project-root-directory}
"uv" "pip" "install" "-r" (u/filename python-sources-dir "pyproject.toml")
"--target" python-sources-dir
"--no-compile")
(u/announce "sqlglot %s installed to %s" sqlglot-version python-sources-dir))))

96
bin/claude-dangerous Executable file
View File

@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# Runs Claude Code in a custom Docker container with --dangerously-skip-permissions.
# The image includes Clojure, Java, Babashka, and all tools from mise.toml.
# DB connection info is forwarded from mise.local.toml.
#
# Usage: workmux add --agent ./bin/claude-dangerous feat/my-branch
#
# Pass --build as the first arg to force a rebuild of the image.
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
IMAGE_NAME="metabase-claude-code"
DOCKERFILE="$PROJECT_ROOT/dev/docker/claude-code/Dockerfile"
# Build image if --build passed or image doesn't exist
if [[ "${1:-}" == "--build" ]]; then
shift
echo "Building $IMAGE_NAME image..."
docker build -t "$IMAGE_NAME" -f "$DOCKERFILE" "$PROJECT_ROOT"
elif ! docker image inspect "$IMAGE_NAME" &>/dev/null; then
echo "Image $IMAGE_NAME not found, building..."
docker build -t "$IMAGE_NAME" -f "$DOCKERFILE" "$PROJECT_ROOT"
fi
# On macOS, extract Claude OAuth credentials from the system keychain
# and prepare a minimal .claude.json for the container.
AUTH_TMPDIR=$(mktemp -d)
trap 'rm -rf "$AUTH_TMPDIR"' EXIT
AUTH_MOUNT_ARGS=()
if [[ "$OSTYPE" == darwin* ]]; then
KEYCHAIN_CREDS=$(security find-generic-password -s "Claude Code-credentials" -a "$USER" -w 2>/dev/null || true)
if [[ -n "$KEYCHAIN_CREDS" ]]; then
mkdir -p "$AUTH_TMPDIR/.claude"
echo "$KEYCHAIN_CREDS" > "$AUTH_TMPDIR/.claude/.credentials.json"
AUTH_MOUNT_ARGS+=(-v "$AUTH_TMPDIR/.claude/.credentials.json:/home/claude/.claude/.credentials.json")
fi
fi
# Build a minimal .claude.json with onboarding complete and project trust.
# Mounting the host's full ~/.claude.json won't work (host-specific paths).
python3 -c "
import json, os
host_config = '$HOME/.claude.json'
config = {}
if os.path.exists(host_config):
with open(host_config) as f:
full = json.load(f)
# Copy only the auth-related keys
for key in ('oauthAccount', 'customApiKeyResponses'):
if key in full:
config[key] = full[key]
config['hasCompletedOnboarding'] = True
config['bypassPermissionsModeAccepted'] = True
config['dangerouslySkipPermissions'] = True
config['autoAcceptPermissions'] = True
config['numStartups'] = 1
config['projects'] = {
'/app': {
'allowedTools': [],
'hasTrustDialogAccepted': True,
'skipConfirmationDialogs': True,
}
}
with open('$AUTH_TMPDIR/claude.json', 'w') as f:
json.dump(config, f, indent=2)
"
AUTH_MOUNT_ARGS+=(-v "$AUTH_TMPDIR/claude.json:/home/claude/.claude.json")
# Parse mise.local.toml for env vars (DB connection, ports, etc.)
ENV_ARGS=()
if [[ -f "$PROJECT_ROOT/mise.local.toml" ]]; then
while IFS='=' read -r key value; do
key="$(echo "$key" | xargs)"
value="$(echo "$value" | sed 's/^[[:space:]]*"//;s/"[[:space:]]*$//')"
if [[ -n "$key" && -n "$value" ]]; then
# Rewrite localhost to host.docker.internal so container can reach host DBs
if [[ "$key" == "MB_DB_CONNECTION_URI" ]]; then
value="${value//localhost/host.docker.internal}"
fi
ENV_ARGS+=(-e "$key=$value")
fi
done < <(grep -E '^[A-Z_]+ *= *"' "$PROJECT_ROOT/mise.local.toml")
fi
exec docker run -it --rm \
-v "$PROJECT_ROOT:/app" \
-w /app \
-v /dev/null:/app/mise.local.toml:ro \
"${AUTH_MOUNT_ARGS[@]}" \
-e ANTHROPIC_API_KEY \
"${ENV_ARGS[@]}" \
--add-host=host.docker.internal:host-gateway \
"$IMAGE_NAME" \
--dangerously-skip-permissions

11
bin/e2e-backend Executable file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
PORT="${MB_JETTY_PORT:-4000}"
if curl -sf "http://localhost:$PORT/api/health" > /dev/null 2>&1; then
echo "⚠️ Backend already running on :$PORT — reusing it. If you need a different MB_EDITION, kill it first: lsof -ti:$PORT | xargs kill"
exit 0
fi
exec node e2e/runner/start-backend.js

42
bin/e2e-stress-test Executable file
View File

@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail
RUNS="${E2E_STRESS_RUNS:-5}"
if [ $# -eq 0 ]; then
echo "Usage: bin/e2e-stress-test [cypress args...]"
echo " e.g. bin/e2e-stress-test --spec e2e/test/scenarios/organization/bookmarks-dashboard.cy.spec.js"
echo ""
echo "Set E2E_STRESS_RUNS=N to change iteration count (default: 5)"
exit 1
fi
rm -rf cypress/screenshots cypress/videos
for i in $(seq 1 "$RUNS"); do
echo ""
echo "=========================================="
echo " Run $i / $RUNS"
echo "=========================================="
echo ""
if ! CYPRESS_VIDEO=false CYPRESS_RETRIES=0 CYPRESS_GUI=false GENERATE_SNAPSHOTS=false bun test-cypress "$@"; then
echo ""
echo "=========================================="
echo " FAILED on run $i / $RUNS"
echo "=========================================="
if ls cypress/screenshots/**/*.png 1>/dev/null 2>&1; then
echo ""
echo "Screenshots:"
find cypress/screenshots -name "*.png" -print
fi
exit 1
fi
done
echo ""
echo "=========================================="
echo " All $RUNS runs passed"
echo "=========================================="

View File

@@ -18,16 +18,9 @@
This format should no longer be used for new migrations, use `id-timestamp-format-re` instead."
#"^v\d{2,}\.\d{2}-\d{3}$")
(s/def ::id (s/and string?
(s/or
;; new ids should conform this format
:id-with-timestamp ;; e.g: v49.2023-12-14T08:54:54
(s/and #(re-matches id-timestamp-format-re %))
:id-with-old-version-format ;; e.g: v49.00-008
(s/and #(re-matches id-number-format-re %)
;; the cut off id for this old format is v49.00-060
;; see #36787 for context
#(neg? (compare % "v49.00-061"))))))
;; Basic type check only — format enforcement is file-aware and happens in
;; `lint-migrations-file/require-change-set-ids-match-file-format`.
(s/def ::id string?)
(s/def ::author string?)
@@ -74,54 +67,6 @@
[]))) ; provide an empty list if dbms-val is nil
changes))))))
(def change-types-supporting-rollback
"This set was generated with a little grep and awk from the docs here:
https://docs.liquibase.com/workflows/liquibase-community/liquibase-auto-rollback.html
If a new change type is introduced that supports automatic rollback, it should be added
to this set."
#{:addCheckConstraint
:addColumn
:addDefaultValue
:addForeignKeyConstraint
:addLookupTable
:addNotNullConstraint
:addPrimaryKey
:addUniqueConstraint
:createIndex
:createSequence
:createSynonym
:createTable
:createView
:disableCheckConstraint
:disableTrigger
:dropNotNullConstraint
:enableCheckConstraint
:enableTrigger
:renameColumn
:renameSequence
:renameTable
:renameTrigger
:renameView
;; assumes all custom changes use the `def-migration` or `define-reversible-migration` in
;; [[metabase.app-db.custom-migrations]]
:customChange})
(defn- major-version
"Returns major version from id string, e.g. 44 from \"v44.00-034\""
[id-str]
(when (string? id-str)
(some-> (re-find #"\d+" id-str) Integer/parseInt)))
(defn- rollback-present-when-required?
"Ensures rollback key is present when change type doesn't support auto rollback"
[{:keys [id changes] :as change-set}]
(or
(int? id)
(< (major-version id) 45)
(some change-types-supporting-rollback (mapcat keys changes))
(contains? change-set :rollback)))
(defn- disallow-delete-cascade-with-add-column
"Returns false if addColumn changeSet uses deleteCascade. See Metabase issue #14321"
[{:keys [changes]}]
@@ -135,7 +80,6 @@
(s/def ::change-set
(s/and
rollback-present-when-required?
disallow-delete-cascade-with-add-column
(s/keys :req-un [::id ::author ::changes ::comment]
:opt-un [::preConditions])))

View File

@@ -10,7 +10,8 @@
[clojure.spec.alpha :as s]
[clojure.string :as str])
(:import
[clojure.lang ExceptionInfo]))
(clojure.lang ExceptionInfo)
(java.lang Integer)))
#_:clj-kondo/ignore
(set! *warn-on-reflection* true)
@@ -52,26 +53,58 @@
(when (seq duplicates)
(throw (validation-error "Change set IDs are not distinct." {:duplicates duplicates})))))
(defn- require-change-set-ids-in-order [change-log]
(let [ids (change-set-ids change-log)
out-of-order-ids (->> ids
(partition 2 1)
(filter (fn [[id1 id2]]
(pos? (compare id1 id2)))))]
(defn- directory-based-migration-file?
"Returns true if the file is a directory-based migration file (e.g., `060/20260905_mq_indexes.yaml`)."
[file]
(boolean (re-matches #".*\d{3}/\d{8}_[a-z0-9_]+\.yaml$" (str file))))
(when (seq out-of-order-ids)
(throw (validation-error "Change set IDs are not in order"
{:out-of-order-ids out-of-order-ids})))))
(defn- file-version
"Extracts the migration version number from a file.
For per-release files like `059_update_migrations.yaml`, parses from the filename.
For directory-based files like `060/20260106_125531.yaml`, parses from the parent directory name."
[^java.io.File file]
(if (directory-based-migration-file? file)
(parse-long (re-find #"\d+" (.getName (.getParentFile file))))
(parse-long (re-find #"\d+" (.getName file)))))
(defn- changeset-version+id
"Returns [version local-id] for a changeset.
Parses from the ID itself, which should be of the form 'v{version}.{local-id}'."
[_file changeset-id]
(let [id-str (str changeset-id)]
(when-let [[_ version local-id] (re-matches #"^v(\d+)\.(.+)$" id-str)]
[(parse-long version) local-id])))
(defn- changeset-at-or-after?
"Returns true if the changeset (identified by file context and changeset ID) is at or after
the given [threshold-version threshold-id]."
[file changeset-id threshold-version threshold-id]
(when-let [[v local-id] (changeset-version+id file changeset-id)]
(or (> v threshold-version)
(and (= v threshold-version)
(not (neg? (compare local-id threshold-id)))))))
(defn- require-change-set-ids-in-order [change-log file]
(when-not (directory-based-migration-file? file)
(let [ids (change-set-ids change-log)
out-of-order-ids (->> ids
(partition 2 1)
(filter (fn [[id1 id2]]
(pos? (compare id1 id2)))))]
(when (seq out-of-order-ids)
(throw (validation-error "Change set IDs are not in order"
{:out-of-order-ids out-of-order-ids}))))))
(defn- require-change-set-ids-in-correct-file [change-log file]
(let [file-version (parse-long (re-find #"\d+" (.getName file)))
(let [fv (file-version file)
ids (change-set-ids change-log)
wrong-file-ids (->> ids
(filter (fn [id]
(let [id-version (parse-long (re-find #"\d+" id))]
(if (= file-version 1)
(> id-version 55)
(not= file-version id-version))))))]
wrong-file-ids
(->> ids
(filter (fn [id]
(let [id-version (parse-long (re-find #"\d+" id))]
(if (= fv 1)
(> id-version 55)
(not= fv id-version))))))]
(when (seq wrong-file-ids)
(throw (validation-error "Change set IDs are in the wrong file"
{:wrong-file-ids wrong-file-ids})))))
@@ -101,18 +134,19 @@
(some #(check-change-use-types? target-types %) (get-in change-set [:changeSet :changes])))
(defn- require-no-types-in-change-log!
"Returns true if none of the changes in the change-log contain usage of any type specified in `target-types`.
`id-filter-fn` is a function that takes an ID and return true if the changeset should be checked."
"Throws if any changeset at or after [version id] uses a type in target-types.
When version and id are not provided, checks all changesets."
([target-types change-log]
(require-no-types-in-change-log! target-types change-log (constantly true)))
([target-types change-log id-filter-fn]
(require-no-types-in-change-log! target-types change-log nil nil nil))
([target-types change-log file version id]
{:pre [(set? target-types)]}
(when-let [using-types? (->> change-log
(filter (fn [change-set]
(let [id (get-in change-set [:changeSet :id])]
(and (string? id)
(id-filter-fn id)))))
(let [cs-id (get-in change-set [:changeSet :id])]
(and cs-id
(if version
(changeset-at-or-after? file cs-id version id)
true)))))
(filter #(check-change-set-use-types? target-types %))
(map #(get-in % [:changeSet :id]))
seq)]
@@ -132,17 +166,46 @@
(defn require-no-bare-boolean-types
"Ensures that no \"boolean\" type columns are added in changesets with id later than v49.00-032. From that point on,
\"${boolean.type}\" should be used instead, so that we can consistently use `BIT(1)` for Boolean columns on MySQL."
[change-log]
(require-no-types-in-change-log! #{"boolean"} change-log #(pos? (compare % "v49.00-032"))))
[change-log file]
(require-no-types-in-change-log! #{"boolean"} change-log file 49 "00-032"))
(defn require-no-datetime-type
"Ensures that no \"datetime\" or \"timestamp without time zone\".
From that point on, \"${timestamp_type}\" should be used instead, so that all of our time related columns are tz-aware."
[change-log]
[change-log file]
(require-no-types-in-change-log!
#{"datetime" "timestamp" "timestamp without time zone"}
change-log
#(pos? (compare % "v49.00-000"))))
change-log file 49 "00-000"))
(def ^:private directory-based-id-format-re
"For directory-based migration files (v60+), IDs must be prefixed with the correct version.
E.g., `v60.my-migration-name` or `v60.1`"
#"^v\d{2,}\..+$")
(defn- require-change-set-ids-match-file-format
"Enforces that changeset IDs use the correct format for the file type:
- Directory-based files (v60+): IDs must be prefixed with v{version}. (e.g., v60.anything)
- 001_update_migrations.yaml: any string ID is allowed (legacy file)
- Other per-release files: IDs must match the timestamp format"
[change-log file]
(cond
(directory-based-migration-file? file)
(let [ids (change-set-ids change-log)
bad-ids (remove #(re-matches directory-based-id-format-re %) ids)]
(when (seq bad-ids)
(throw (validation-error
(format "Directory-based migration file contains IDs without v{version}. prefix: %s"
(str/join ", " bad-ids))
{:invalid-ids (vec bad-ids)}))))
(not= (file-version file) 1)
(let [ids (change-set-ids change-log)
bad-ids (remove #(re-matches change-set.strict/id-timestamp-format-re %) ids)]
(when (seq bad-ids)
(throw (validation-error
(format "Per-release migration file contains non-timestamp ID formats: %s"
(str/join ", " bad-ids))
{:invalid-ids (vec bad-ids)}))))))
(defn require-primary-key-exists-has-table-name
"Ensures that all primaryKeyExists preconditions specify a tableName."
@@ -165,25 +228,82 @@
(throw (validation-error "Expected exactly one key." {:keys keys})))
(first keys)))
(defn- major-version
"Returns major version from id string, e.g. 44 from \"v44.00-034\".
For directory-based migrations (file path matches `NNN/`), extracts
version from the file path. Otherwise parses from the id string."
[id-str file]
(if-let [[_ file-version] (when file (re-find #"(\d{3})[/\\]" (str file)))]
(Integer/parseInt file-version)
(when (string? id-str)
(some-> (re-find #"\d+" id-str) Integer/parseInt))))
(def change-types-supporting-rollback
"This set was generated with a little grep and awk from the docs here:
https://docs.liquibase.com/workflows/liquibase-community/liquibase-auto-rollback.html
If a new change type is introduced that supports automatic rollback, it should be added
to this set."
#{:addCheckConstraint
:addColumn
:addDefaultValue
:addForeignKeyConstraint
:addLookupTable
:addNotNullConstraint
:addPrimaryKey
:addUniqueConstraint
:createIndex
:createSequence
:createSynonym
:createTable
:createView
:disableCheckConstraint
:disableTrigger
:dropNotNullConstraint
:enableCheckConstraint
:enableTrigger
:renameColumn
:renameSequence
:renameTable
:renameTrigger
:renameView
;; assumes all custom changes use the `def-migration` or `define-reversible-migration` in
;; [[metabase.app-db.custom-migrations]]
:customChange})
(defn- rollback-present-when-required?
"Ensures rollback key is present when change type doesn't support auto rollback.
`file` is used to extract version for directory-based migrations."
[{:keys [id changes] :as change-set} file]
(or
(let [v (major-version (str id) file)]
(and v (< v 45)))
(some change-types-supporting-rollback (mapcat keys changes))
(contains? change-set :rollback)))
(defn- validate-database-change-log [change-log file]
(when (string? change-log)
(throw (validation-error "Expected `:databaseChangeLog` to be a map, not a string.")))
(require-distinct-change-set-ids change-log)
(require-change-set-ids-in-correct-file change-log file)
(require-change-set-ids-in-order change-log)
(require-change-set-ids-in-order change-log file)
(require-change-set-ids-match-file-format change-log file)
(require-no-bare-blob-or-text-types change-log)
(require-no-bare-boolean-types change-log)
(require-no-datetime-type change-log)
(require-no-bare-boolean-types change-log file)
(require-no-datetime-type change-log file)
(require-primary-key-exists-has-table-name change-log)
(let [{:keys [changeSet]
:as all} (group-by only-key change-log)]
(when-not (set/subset? (set (keys all)) #{:property :objectQuotingStrategy :changeSet})
(throw (validation-error "Expected exactly one of :property, :objectQuotingStrategy, or :changeSet."
(when-not (set/subset? (set (keys all)) #{:property :objectQuotingStrategy :changeSet :logicalFilePath})
(throw (validation-error "Expected exactly one of :property, :objectQuotingStrategy, :logicalFilePath, or :changeSet."
{:keys (keys all)})))
(doseq [{:keys [changeSet]} changeSet
:when (not (s/valid? ::changeSet changeSet))]
(throw (validation-error "Invalid change set." (s/explain-data ::changeSet changeSet))))))
(doseq [{:keys [changeSet]} changeSet]
(when-not (s/valid? ::changeSet changeSet)
(throw (validation-error "Invalid change set." (s/explain-data ::changeSet changeSet))))
(when-not (rollback-present-when-required? changeSet file)
(throw (validation-error "Rollback is required but not present."
{:id (:id changeSet)}))))))
(defn- validate-migrations [migrations file]
(require-database-change-log! migrations)
@@ -195,7 +315,14 @@
(let [dir-str #?(:bb "resources/migrations" :clj "../../resources/migrations")
dir (io/file dir-str)]
(->> (file-seq dir)
(filter #(and (.isFile %) (str/ends-with? (.getName %) "_update_migrations.yaml"))))))
(filter (fn [^java.io.File f]
(and (.isFile f)
(or
;; Per-release files: 059_update_migrations.yaml
(str/ends-with? (.getName f) "_update_migrations.yaml")
;; Directory-based files: 060/20260106_125531.yaml
(directory-based-migration-file? f)))))
(sort-by str))))
(defn- migrations [file]
(assert (.exists file) (format "%s does not exist" file))
@@ -207,13 +334,22 @@
(fix-vals (yaml/parse-string
#_:clj-kondo/ignore (slurp file)))))
(defn- display-name
"Returns a human-readable name for a migration file.
For per-release files, returns just the filename.
For directory-based files, returns `parent/filename`."
[^java.io.File file]
(if (directory-based-migration-file? file)
(str (.getName (.getParentFile file)) "/" (.getName file))
(.getName file)))
(defn- validate-all []
(doseq [file (migration-files)]
(println "Validating" (.getName file) "...")
(println "Validating" (display-name file) "...")
(try
(validate-migrations (migrations file) file)
(catch ExceptionInfo e
(throw (ex-info (.getMessage e) (assoc (ex-data e) :file (.getName file)) e))))))
(throw (ex-info (.getMessage e) (assoc (ex-data e) :file (display-name file)) e))))))
(defn -main
"Entry point for Clojure CLI task `lint-migrations-file`. Run it with

View File

@@ -200,17 +200,19 @@
(validate-id "v49.2024-01-01T10:30:00"))))
(testing "invalid date components should throw an error"
(are [msg id]
(thrown-with-msg?
clojure.lang.ExceptionInfo
#"Invalid change set\."
(validate-id "v49.2024-30-01T10:30:00")
msg)
"invalid month" "v49.2024-13-01T10:30:00"
"invalid day" "v49.2024-01-32T10:30:00"
"invalid hour" "v49.2024-01-01T25:30:00"
"invalid minute" "v49.2024-01-01T10:60:00"
"invalid second" "v49.2024-01-01T10:30:60"))))
(let [validate-id-strict (fn [id]
(validate-file (io/file "049_update_migrations.yaml")
(mock-change-set :id id)))]
(are [msg id]
(thrown-with-msg?
clojure.lang.ExceptionInfo
#"non-timestamp ID formats"
(validate-id-strict id))
"invalid month" "v49.2024-13-01T10:30:00"
"invalid day" "v49.2024-01-32T10:30:00"
"invalid hour" "v49.2024-01-01T25:30:00"
"invalid minute" "v49.2024-01-01T10:60:00"
"invalid second" "v49.2024-01-01T10:30:60")))))
(deftest ^:parallel validate-id-in-file-test
(letfn [(validate-id [id file]
@@ -285,7 +287,7 @@
(testing "missing rollback key fails"
(is (thrown-with-msg?
clojure.lang.ExceptionInfo
#"Invalid change set\."
#"Rollback is required but not present\."
(validate (update (mock-change-set :id "v49.2024-01-01T10:30:00" :changes [{:sql {:sql "select 1"}}])
:changeSet dissoc :rollback)))))
(testing "nil rollback is allowed"

31
bin/test-agent Executable file
View File

@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Agent-friendly test runner for Metabase backend (Clojure) tests.
#
# Differences from a normal `clj -X:dev:test` run:
# - Uses HAWK_MODE=cli/ci so eftest.report.pretty is used instead of the
# progress-bar reporter. Failures print inline as they happen.
# - Strips ANSI escape codes from output so results are plain text.
# - Filters Maven download noise and reflection warnings.
#
# Usage:
# bin/test-agent :only '[metabase.foo-test]'
# bin/test-agent :only '[metabase.foo-test/some-test]'
# bin/test-agent # runs all tests (rarely what you want)
#
# Any extra arguments are forwarded directly to `clojure -X:dev:ee:ee-dev:test`.
# The :test alias uses port 0 (OS-assigned random port) so parallel runs never collide.
set -euo pipefail
export HAWK_MODE=cli/ci
# Filter noise and strip ANSI escape sequences
clean_output() {
grep -v '^Downloading: ' \
| grep -v '^Reflection warning,' \
| grep -v '^Excluding directory' \
| grep -v '^Looking for test namespaces in directory' \
| sed $'s/\033[\[(][0-9;]*[A-Za-z]//g; s/\033[^[]*//g'
}
exec clojure -X:dev:ee:ee-dev:test "$@" 2>&1 | clean_output

841
bun.lock

File diff suppressed because it is too large Load Diff

55
cross-version/README.md Normal file
View File

@@ -0,0 +1,55 @@
# Cross-Version Migration Testing
Tests Metabase database migrations across version upgrades and downgrades.
## Quick Start
```bash
# Test upgrade (OSS)
./test.sh --source v0.58.6 --target v0.58.7
# Test downgrade (OSS)
./test.sh --source v0.58.7 --target v0.58.6
# Test Enterprise edition (use v1.x.x)
./test.sh --source v1.58.6 --target v1.58.7
```
Edition is inferred from version prefix: `v0.x.x` = OSS, `v1.x.x` = EE.
## Requirements
- Docker
- Docker Compose v2
- Bun (for version helpers CLI)
## How It Works
### Upgrade Test
1. Start PostgreSQL container
2. Start SOURCE Metabase version, wait for healthy
3. Stop SOURCE container (PostgreSQL persists)
4. Start TARGET version - migrations run automatically
5. Verify health check passes
### Downgrade Test
1. Start PostgreSQL container
2. Start SOURCE (newer) Metabase version, wait for healthy
3. Stop SOURCE container
4. Start TARGET (older) version - expects refusal
5. Run `migrate down` command
6. Start TARGET version again
7. Verify health check passes
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `METABASE_PORT` | 3000 | Port to expose Metabase |
| `HEALTH_TIMEOUT` | 120 | Seconds to wait for health check |
## CI
The GitHub workflow (`.github/workflows/cross-version.yml`) supports:
- **Manual trigger**: Specify source/target versions
- **Nightly schedule**: Runs predefined version matrix

19
cross-version/cli.ts Normal file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bun
import {
compareVersions,
getDockerImage,
getMajorVersion,
} from "../release/src/cross-version-helpers";
const [cmd, ...args] = process.argv.slice(2);
if (cmd === "compare") {
console.log(compareVersions(args[0], args[1]));
} else if (cmd === "image") {
console.log(getDockerImage(args[0]));
} else if (cmd === "major") {
console.log(getMajorVersion(args[0]));
} else {
console.error("Usage: cli.ts <compare|image|major> [args]");
process.exit(1);
}

View File

@@ -0,0 +1,32 @@
services:
postgres:
image: postgres:15
environment:
POSTGRES_USER: metabase
POSTGRES_PASSWORD: metabase
POSTGRES_DB: metabase
healthcheck:
test: ["CMD-SHELL", "pg_isready -U metabase"]
interval: 5s
timeout: 5s
retries: 5
volumes:
- postgres_data:/var/lib/postgresql/data
metabase:
image: ${METABASE_IMAGE:-metabase/metabase:latest}
ports:
- "${METABASE_PORT:-3000}:3000"
environment:
MB_DB_TYPE: postgres
MB_DB_DBNAME: metabase
MB_DB_PORT: 5432
MB_DB_USER: metabase
MB_DB_PASS: metabase
MB_DB_HOST: postgres
depends_on:
postgres:
condition: service_healthy
volumes:
postgres_data:

383
cross-version/test.sh Executable file
View File

@@ -0,0 +1,383 @@
#!/usr/bin/env bash
#
# Cross-version migration testing for Metabase
#
# Usage:
# ./test.sh --source v0.58.6 --target v0.58.7
#
# Edition (OSS/EE) is inferred from version prefix (v0.x = OSS, v1.x = EE).
# Direction (upgrade/downgrade) is inferred from version comparison.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Defaults
SOURCE_VERSION=""
TARGET_VERSION=""
METABASE_PORT="${METABASE_PORT:-3000}"
HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-120}"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log() { echo -e "${GREEN}[cross-version]${NC} $*"; }
warn() { echo -e "${YELLOW}[cross-version]${NC} $*"; }
error() { echo -e "${RED}[cross-version]${NC} $*" >&2; }
usage() {
cat <<EOF
Usage: $0 --source VERSION --target VERSION
Options:
--source VERSION Source version (e.g., v0.58.6 for OSS, v1.58.6 for EE)
--target VERSION Target version (e.g., v0.58.7 for OSS, v1.58.7 for EE)
--help Show this help
Examples:
$0 --source v0.58.6 --target v0.58.7 # Upgrade test (OSS)
$0 --source v0.58.7 --target v0.58.6 # Downgrade test (OSS)
$0 --source v1.58.6 --target v1.58.7 # Upgrade test (EE)
EOF
exit 1
}
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--source) SOURCE_VERSION="$2"; shift 2 ;;
--target) TARGET_VERSION="$2"; shift 2 ;;
--help) usage ;;
*) error "Unknown option: $1"; usage ;;
esac
done
if [[ -z "$SOURCE_VERSION" || -z "$TARGET_VERSION" ]]; then
error "Both --source and --target are required"
usage
fi
# CLI wrapper for version helpers (uses tested TypeScript implementation)
cli() {
bun "$SCRIPT_DIR/cli.ts" "$@"
}
wait_for_health() {
local timeout="$1"
local start_time=$(date +%s)
log "Waiting for Metabase to be healthy (timeout: ${timeout}s)..."
while true; do
local current_time=$(date +%s)
local elapsed=$((current_time - start_time))
if (( elapsed >= timeout )); then
error "Health check timed out after ${timeout}s"
return 1
fi
if curl -sf "http://localhost:${METABASE_PORT}/api/health" > /dev/null 2>&1; then
local response=$(curl -sf "http://localhost:${METABASE_PORT}/api/health")
if [[ "$response" == '{"status":"ok"}' ]]; then
log "Health check passed after ${elapsed}s"
return 0
fi
fi
sleep 2
done
}
# Start Metabase with a specific image
start_metabase() {
local image="$1"
log "Starting Metabase with image: $image"
METABASE_IMAGE="$image" METABASE_PORT="$METABASE_PORT" \
docker compose up -d --quiet-pull
}
# Stop Metabase (but keep postgres)
stop_metabase() {
log "Stopping Metabase container..."
docker compose stop metabase
docker compose rm -f metabase
}
# Get edition prefix from version (0 for OSS, 1 for EE)
get_edition_prefix() {
local version="$1"
if [[ "$version" == "HEAD" ]]; then
echo "1" # HEAD is EE
elif [[ "$version" =~ ^v([01])\. ]]; then
echo "${BASH_REMATCH[1]}"
else
error "Cannot determine edition from version: $version"
return 1
fi
}
# https://www.metabase.com/docs/latest/installation-and-operation/upgrading-metabase#using-the-migrate-down-command
# Run a single migrate down step (rolls back exactly one major version)
# This function is idempotent - it takes a version string and handles everything internally.
migrate_down_step() {
local version="$1"
local image is_head="false"
# Resolve image from version
image=$(cli image "$version") || return 1
[[ "$version" == "HEAD" ]] && is_head="true"
log "Running 'migrate down' for version: $version (image: $image)"
local output
local exit_code=0
# The HEAD Docker image has version "vUNKNOWN" in its version.properties,
# which causes `migrate down` to fail with a NullPointerException when trying
# to determine the current major version. To work around this, we extract the
# JAR, replace version.properties with the upcoming version, and mount the
# modified JAR.
#
# HEAD is the next major version (CURRENT_VERSION + 1).
#
# See DEV-1636 for a potential backend fix that would make this unnecessary.
if [[ "$is_head" == "true" ]]; then
if [[ -z "$CURRENT_VERSION" ]]; then
error "CURRENT_VERSION must be set when testing HEAD downgrades"
return 1
fi
local next_version=$((CURRENT_VERSION + 1))
local upcoming_version="v1.${next_version}.0"
local work_dir="/tmp/metabase-head-jar"
local modified_jar="$work_dir/metabase.jar"
log "HEAD JAR needs patching (version.properties says 'vUNKNOWN', setting to $upcoming_version)"
rm -rf "$work_dir"
mkdir -p "$work_dir"
# Extract JAR from container (copy to host with proper permissions)
docker create --name metabase-jar-extract "$image" >/dev/null
docker cp metabase-jar-extract:/app/metabase.jar "$modified_jar"
docker rm metabase-jar-extract >/dev/null
# Create version.properties with upcoming version and update the JAR
echo "tag=$upcoming_version" > "$work_dir/version.properties"
echo "hash=HEAD" >> "$work_dir/version.properties"
echo "date=$(date +%Y-%m-%d)" >> "$work_dir/version.properties"
(cd "$work_dir" && zip -u metabase.jar version.properties >/dev/null)
log "HEAD JAR patched - proceeding with migrate down"
output=$(METABASE_IMAGE="$image" docker compose run --rm --quiet-pull \
-v "$modified_jar:/app/metabase.jar:ro" \
metabase "migrate down" 2>&1) || exit_code=$?
else
output=$(METABASE_IMAGE="$image" docker compose run --rm --quiet-pull metabase "migrate down" 2>&1) || exit_code=$?
fi
# Always print output for visibility
echo "$output"
# Check for errors - Metabase returns 0 even on partial rollback failures
if [[ $exit_code -ne 0 ]]; then
error "migrate down failed with exit code $exit_code"
return 1
fi
if echo "$output" | grep -q "ERROR.*liquibase\|RollbackFailedException\|Command failed with exception"; then
error "migrate down encountered errors (check logs above)"
return 1
fi
}
# Cascading migrate down: rolls back multiple major versions one at a time
# Metabase only supports rolling back one major version per `migrate down` call,
# so we need to iterate through intermediate versions.
cascading_migrate_down() {
local source_version="$1"
local target_version="$2"
local source_major target_major edition_prefix
source_major=$(cli major "$source_version") || return 1
target_major=$(cli major "$target_version") || return 1
edition_prefix=$(get_edition_prefix "$source_version") || return 1
local steps=$((source_major - target_major))
log "Cascading migrate down: v$source_major → v$target_major ($steps step(s))"
# Build version sequence: source version first, then intermediate rolling versions
local versions=("$source_version")
local current=$((source_major - 1))
while (( current > target_major )); do
versions+=("v${edition_prefix}.${current}.x")
((current--))
done
# Simple loop - each step handles its own image resolution
local step=1
for version in "${versions[@]}"; do
log "--------------------------------------------"
log "Cascade step $step/$steps: $version"
log "--------------------------------------------"
if ! migrate_down_step "$version"; then
error "Migrate down failed at step $step ($version)"
return 1
fi
((step++))
done
log "--------------------------------------------"
log "Cascading migrate down complete"
}
# Check if Metabase refuses to start (downgrade detection)
check_downgrade_refused() {
local timeout=60
local start_time=$(date +%s)
log "Checking if Metabase detects downgrade and refuses to start..."
while true; do
local current_time=$(date +%s)
local elapsed=$((current_time - start_time))
if (( elapsed >= timeout )); then
error "Timed out waiting for downgrade detection"
return 1
fi
# Check container logs for downgrade error
local logs=$(docker compose logs metabase 2>&1 || true)
if echo "$logs" | grep -qi "migrate.*down\|downgrade\|database.*newer"; then
log "Metabase correctly detected downgrade scenario"
return 0
fi
local status=$(docker compose ps metabase --format '{{.Status}}' 2>/dev/null || echo "")
if [[ "$status" =~ [Ee]xit ]]; then
log "Metabase container exited (expected for downgrade)"
return 0
fi
sleep 2
done
}
cleanup() {
log "Cleaning up..."
docker compose down -v --remove-orphans 2>/dev/null || true
}
check_image_exists() {
local image="$1"
if ! docker manifest inspect "$image" > /dev/null 2>&1; then
error "Docker image not found: $image"
exit 1
fi
}
main() {
local direction source_image target_image
direction=$(cli compare "$SOURCE_VERSION" "$TARGET_VERSION") || exit 1
source_image=$(cli image "$SOURCE_VERSION") || exit 1
target_image=$(cli image "$TARGET_VERSION") || exit 1
if [[ "$direction" == "same" ]]; then
error "Source and target versions are the same"
exit 1
fi
log "Checking if Docker images exist..."
check_image_exists "$source_image"
check_image_exists "$target_image"
log "============================================"
log "Cross-Version Migration Test"
log "============================================"
log "Direction: $direction"
log "Source: $source_image"
log "Target: $target_image"
log "============================================"
# Ensure clean state
cleanup
trap cleanup EXIT
log ""
log "Step 1: Starting SOURCE version ($SOURCE_VERSION)..."
start_metabase "$source_image"
if ! wait_for_health "$HEALTH_TIMEOUT"; then
error "❌ SOURCE version ($SOURCE_VERSION) failed health check"
docker compose logs metabase
exit 1
fi
log "✅ SOURCE version ($SOURCE_VERSION) is healthy"
log ""
log "Step 2: Stopping SOURCE version ($SOURCE_VERSION)..."
stop_metabase
log ""
log "Step 3: Starting TARGET version ($TARGET_VERSION)..."
start_metabase "$target_image"
if [[ "$direction" == "upgrade" ]]; then
# Upgrade: should migrate automatically and become healthy
if ! wait_for_health "$HEALTH_TIMEOUT"; then
error "❌ TARGET version ($TARGET_VERSION) failed health check after upgrade"
docker compose logs metabase
exit 1
fi
log "✅ UPGRADE successful - TARGET version ($TARGET_VERSION) is healthy"
else
# Downgrade: should refuse to start, then we run migrate down
if ! check_downgrade_refused; then
error "❌ TARGET version ($TARGET_VERSION) did not properly detect downgrade"
docker compose logs metabase
exit 1
fi
# Stop the failed container
stop_metabase
log ""
log "============================================"
log "Step 4: Rolling back database ($SOURCE_VERSION → $TARGET_VERSION)..."
log "============================================"
if ! cascading_migrate_down "$SOURCE_VERSION" "$TARGET_VERSION"; then
error "❌ migrate down failed"
exit 1
fi
# Try starting again
log ""
log "Step 5: Starting TARGET version ($TARGET_VERSION) after migrate down..."
start_metabase "$target_image"
if ! wait_for_health "$HEALTH_TIMEOUT"; then
error "❌ TARGET version ($TARGET_VERSION) failed health check after migrate down"
docker compose logs metabase
exit 1
fi
log "✅ DOWNGRADE successful - TARGET version ($TARGET_VERSION) is healthy after migrate down"
fi
log ""
log "============================================"
log "✅ TEST PASSED"
log "============================================"
}
main

View File

@@ -47,13 +47,15 @@
com.github.seancorfield/honeysql {:mvn/version "2.7.1350"} ; Honey SQL 2. SQL generation from Clojure data maps
com.github.seancorfield/next.jdbc {:mvn/version "1.3.1070"} ; Talk to JDBC DBs
com.github.steffan-westcott/clj-otel-api {:mvn/version "0.2.8"} ; Telemetry library
com.github.steffan-westcott/clj-otel-exporter-otlp {:mvn/version "0.2.8"} ; OTLP span exporter (gRPC and HTTP)
com.github.steffan-westcott/clj-otel-sdk {:mvn/version "0.2.8"} ; OpenTelemetry SDK for programmatic configuration
com.github.vertical-blank/sql-formatter {:mvn/version "2.0.5"} ; Java SQL formatting library https://github.com/vertical-blank/sql-formatter
com.google.guava/guava {:mvn/version "33.5.0-jre"} ; dep for BigQuery, Spark, and GA. Require here rather than letting different dep versions stomp on each other — see comments on #9697
com.h2database/h2 ^:antq/exclude ; https://metaboat.slack.com/archives/CKZEMT1MJ/p1727191010259979
{:mvn/version "2.1.214"} ; embedded SQL database
com.ibm.icu/icu4j {:mvn/version "78.1"} ; Used to transliterate Unicode characters into Latin characters
com.knuddels/jtokkit {:mvn/version "1.1.0"} ; Java tokenizer library for OpenAI models, used for token counting in semantic search
com.mchange/c3p0 {:mvn/version "0.11.2"} ; DB connection pool
com.mchange/c3p0 {:mvn/version "0.12.0"} ; DB connection pool
com.snowplowanalytics/snowplow-java-tracker ^:antq/exclude ; 2+ depend on apache httpclient 5.x, but saml and clj-http depend on 4.x
{:mvn/version "1.0.1"} ; Snowplow analytics
com.sun.mail/jakarta.mail ^:antq/exclude ; 2+ changes imports from javax.mail to jakarta.mail
@@ -80,11 +82,11 @@
hiccup/hiccup {:mvn/version "2.0.0"} ; HTML templating
inflections/inflections {:mvn/version "0.15.0"} ; Clojure/Script library used for prularizing words
instaparse/instaparse {:mvn/version "1.5.0"} ; Make your own parser
io.github.camsaul/toucan2 {:mvn/version "1.0.570"}
io.github.camsaul/toucan2 {:mvn/version "1.0.571"}
io.github.eerohele/pp {#_#_:git/tag "2024-11-13.77" ; super fast pretty-printing library
:git/sha "ffc4edc482c057cd9412c62f018a41e9140c618b"
:git/url "https://github.com/eerohele/pp"}
io.github.metabase/macaw {:mvn/version "0.2.33"} ; Parse native SQL queries
io.github.metabase/macaw {:mvn/version "0.2.36"} ; Parse native SQL queries
io.netty/netty-codec-http {:mvn/version "4.2.9.Final"} ; override of transitive dep from aws s3
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"} ; prometheus jvm collector
junegunn/grouper {:mvn/version "0.1.1"} ; Batch processing helper
@@ -111,14 +113,17 @@
net.sf.cssbox/cssbox {:mvn/version "5.0.2" ; HTML / CSS rendering
:exclusions [org.slf4j/slf4j-api
junit/junit]}
net.thisptr/jackson-jq {:mvn/version "1.6.0"} ; Java implementation of the JQ json query language
org.apache.commons/commons-compress {:mvn/version "1.28.0"} ; compression utils
org.apache.commons/commons-lang3 {:mvn/version "3.19.0"} ; helper methods for working with java.lang stuff
org.apache.logging.log4j/log4j-1.2-api {:mvn/version "2.25.3"} ; apache logging framework
org.apache.logging.log4j/log4j-api {:mvn/version "2.25.3"} ; add compatibility with log4j 1.2
org.apache.logging.log4j/log4j-core {:mvn/version "2.25.3"} ; apache logging framework
org.apache.logging.log4j/log4j-jcl {:mvn/version "2.25.3"} ; allows the commons-logging API to work with log4j 2
org.apache.logging.log4j/log4j-jul {:mvn/version "2.25.3"} ; java.util.logging (JUL) -> Log4j2 adapter
net.thisptr/jackson-jq {:mvn/version "1.6.0"} ; Java implementation of the JQ json query language
org.apache.commons/commons-compress {:mvn/version "1.28.0"} ; compression utils
org.apache.commons/commons-lang3 {:mvn/version "3.19.0"} ; helper methods for working with java.lang stuff
org.apache.httpcomponents.client5/httpclient5 {:mvn/version "5.4.4"} ; pinned: ClickHouse JDBC 0.9.7 needs httpcore5 5.3.4+; must pair with httpclient5 5.4.x
org.apache.httpcomponents.core5/httpcore5 {:mvn/version "5.3.4"} ; pinned: ClickHouse JDBC 0.9.7 requires URIBuilder.optimize() added in 5.3
org.apache.httpcomponents.core5/httpcore5-h2 {:mvn/version "5.3.4"} ; pinned: must match httpcore5 version
org.apache.logging.log4j/log4j-1.2-api {:mvn/version "2.25.3"} ; apache logging framework
org.apache.logging.log4j/log4j-api {:mvn/version "2.25.3"} ; add compatibility with log4j 1.2
org.apache.logging.log4j/log4j-core {:mvn/version "2.25.3"} ; apache logging framework
org.apache.logging.log4j/log4j-jcl {:mvn/version "2.25.3"} ; allows the commons-logging API to work with log4j 2
org.apache.logging.log4j/log4j-jul {:mvn/version "2.25.3"} ; java.util.logging (JUL) -> Log4j2 adapter
org.apache.logging.log4j/log4j-layout-template-json
{:mvn/version "2.25.2"} ; allows the custom json logging format
org.apache.logging.log4j/log4j-slf4j2-impl
@@ -157,14 +162,16 @@
org.clojure/tools.namespace {:mvn/version "1.5.0"}
org.clojure/tools.reader {:mvn/version "1.5.2"}
org.clojure/tools.trace {:mvn/version "0.8.0"} ; function tracing
org.eclipse.jetty/jetty-server {:mvn/version "12.1.3"}
org.eclipse.jetty/jetty-unixdomain-server {:mvn/version "12.1.3"}
org.eclipse.jetty.ee9/jetty-ee9-servlet {:mvn/version "12.1.3"}
org.eclipse.jetty.ee9.websocket/jetty-ee9-websocket-jetty-server {:mvn/version "12.1.3"}
org.eclipse.jetty/jetty-server {:mvn/version "12.1.7"}
org.eclipse.jetty/jetty-unixdomain-server {:mvn/version "12.1.7"}
org.eclipse.jetty.ee9/jetty-ee9-servlet {:mvn/version "12.1.7"}
org.eclipse.jetty.ee9.websocket/jetty-ee9-websocket-jetty-server {:mvn/version "12.1.7"}
org.eclipse.jgit/org.eclipse.jgit {:mvn/version "7.3.0.202506031305-r"}; Java implementation of Git version control system
org.flatland/ordered {:mvn/version "1.15.12"} ; ordered maps & sets
org.graalvm.polyglot/js-community {:mvn/version "25.0.1" :extension "pom"} ; JavaScript engine
org.graalvm.polyglot/polyglot {:mvn/version "25.0.1"} ; GraalVM platform, required by JS engine
org.graalvm.polyglot/python {:mvn/version "25.0.1"
:extension "pom"}
org.jsoup/jsoup {:mvn/version "1.21.2"} ; required by hickory
org.liquibase/liquibase-core {:mvn/version "4.33.0" ; migration management (Java lib)
:exclusions [ch.qos.logback/logback-classic]}
@@ -325,9 +332,9 @@
"-Dmb.jetty.join=false"
"-Dmb.field.filter.operators.enabled=true"
"-Dmb.api.key=test-api-key"
;; Different port from normal `:dev` so you can run tests on a different server.
;; TODO -- figure out how to do a random port like in the old project.clj?
"-Dmb.jetty.port=3001"]}
;; Port 0 tells the OS to assign a random free port, allowing parallel test runs.
;; The actual port is read from the running Jetty server via server.instance/server-port.
"-Dmb.jetty.port=0"]}
;; run the dev server with
;; clojure -M:run
@@ -443,9 +450,10 @@
com.lambdaisland/glogi {:mvn/version "1.3.169"}
io.github.metabase/hawk {:mvn/version "1.0.13"}
org.clojars.mmb90/cljs-cache {:mvn/version "0.1.4"}
org.clojure/clojurescript {:mvn/version "1.12.134"}
refactor-nrepl/refactor-nrepl {:mvn/version "3.11.0"}
;; keep it in sync with package.json
thheller/shadow-cljs {:mvn/version "3.2.1"}}}
thheller/shadow-cljs {:mvn/version "3.3.5"}}}
;; for local dev -- include the drivers locally with :dev:drivers
:drivers
@@ -615,6 +623,9 @@
:implicit-dependencies
;; too many false positives for now
:unused-ret-vals
;; hard to clear on for lib.util.match macros, and disabling the linter just there
;; doesn't seem to work
:unused-meta-on-macro
;; Kondo lints this for us anyway, and this isn't as easy to configure.
:wrong-arity
;; Kondo lints this for us anyway, and this isn't as easy to configure.
@@ -626,6 +637,29 @@
;; and a simple reproduction repo can be found here https://github.com/metabase/snowplow-eastwood-issue
:exclude-namespaces [metabase.analytics.snowplow]}}
;; Test counterpart of :eastwood. Starts with :reflection only; add more linters as we fix other eastwood
;; issues in tests.
;;
;; clojure -X:dev:ee:ee-dev:drivers:drivers-dev:test:eastwood/test
:eastwood/test
{:exec-fn metabase.linters.eastwood/eastwood
:exec-args {:source-paths ["test"
"enterprise/backend/test"]
:linters [:reflection]
:exclude-namespaces
[;; byte/1 dispatch value trips up Eastwood's reader
metabase.test.http-client
;; Cookie/.getName qualified method trips up Eastwood's analyzer
metabase.server.middleware.settings-cache-test
;; .cljc syntax error during Eastwood's analyze+eval phase
metabase.lib.query-test
;; "count not supported on this type: Symbol" in Eastwood's analyzer
metabase.query-processor.parameters.temporal-units-test
;; Syntax error compiling fn* in Eastwood's analyzer
metabase.sql-tools.macaw.references-test
;; Method too large for Eastwood's analyzer
metabase.sql-tools.sqlglot.references-test]}}
;; clojure -T:whitespace-linter
:whitespace-linter
{:deps {com.github.camsaul/whitespace-linter {:sha "e35bc252ccf5cc74f7d543ef95ad8a3e5131f25b"}}
@@ -757,7 +791,7 @@
;;
;; the various steps available are:
;;
;; :version :translations :frontend :licenses :drivers :uberjar
;; :version :translations :frontend :licenses :drivers :python :uberjar
:build/all
{:exec-fn build/build-cli}

View File

@@ -0,0 +1,38 @@
FROM ubuntu:noble
RUN apt-get update && apt-get install -y --no-install-recommends \
curl git rlwrap bash ca-certificates gnupg ripgrep sudo \
&& rm -rf /var/lib/apt/lists/*
# Create non-root user (--dangerously-skip-permissions refuses to run as root)
RUN useradd -m -s /bin/bash claude
RUN echo 'claude ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
USER claude
# Install mise
RUN curl https://mise.run | sh
ENV PATH="/home/claude/.local/bin:$PATH"
# Copy project mise.toml to install the same tool versions
COPY --chown=claude:claude mise.toml /home/claude/.config/mise/config.toml
# Trust the mise config files so there's no interactive prompt
RUN mise trust /home/claude/.config/mise/config.toml
# Install all tools defined in mise.toml
RUN mise install
# Put mise shims on PATH
RUN echo 'eval "$(mise activate bash)"' >> /home/claude/.bashrc
ENV PATH="/home/claude/.local/share/mise/shims:$PATH"
# Auto-accept mise trust prompts for project configs mounted at runtime
ENV MISE_YES=1
# Create .claude directory so Claude Code can write session data
RUN mkdir -p /home/claude/.claude
WORKDIR /app
ENTRYPOINT ["claude"]

113
dev/src/dev/api/preview.clj Normal file
View File

@@ -0,0 +1,113 @@
(ns dev.api.preview
"Dev-only endpoints for previewing rendered pulse cards and dashboards.
These were previously at `/api/pulse/preview_*` but have been moved here
since they are only useful for development and testing."
(:require
[hiccup.core :refer [html]]
[hiccup.page :refer [html5]]
[metabase.api.common :as api]
[metabase.api.macros :as api.macros]
[metabase.api.routes.common]
[metabase.channel.render.core :as channel.render]
[metabase.channel.urls :as urls]
[metabase.query-processor :as qp]
[metabase.query-processor.middleware.permissions :as qp.perms]
[metabase.util.malli.schema :as ms])
(:import
(java.io ByteArrayInputStream)))
(set! *warn-on-reflection* true)
(defn- pulse-card-query-results
{:arglists '([card])}
[{query :dataset_query, card-id :id}]
(binding [qp.perms/*card-id* card-id]
(qp/process-query
(qp/userland-query
(assoc query
:middleware {:process-viz-settings? true
:js-int-to-string? false})
{:executed-by api/*current-user-id*
:context :pulse
:card-id card-id}))))
(api.macros/defendpoint :get "/preview-card/:id"
"Get HTML rendering of a Card with `id`."
[{:keys [id]} :- [:map
[:id ms/PositiveInt]]]
(let [card (api/read-check :model/Card id)
result (pulse-card-query-results card)]
{:status 200
:body (html5
[:html
[:body {:style "margin: 0;"}
(channel.render/render-pulse-card-for-display (channel.render/defaulted-timezone card)
card
result
{:channel.render/include-title? true, :channel.render/include-buttons? true})]])}))
(api.macros/defendpoint :get "/preview-dashboard/:id"
"Get HTML rendering of a Dashboard with `id`.
This endpoint relies on a custom middleware defined in `metabase.channel.render.core/style-tag-nonce-middleware` to
allow the style tag to render properly, given our Content Security Policy setup."
[{:keys [id]} :- [:map
[:id ms/PositiveInt]]]
(api/read-check :model/Dashboard id)
{:status 200
:headers {"Content-Type" "text/html"}
:body (channel.render/style-tag-from-inline-styles
(html5
[:head
[:meta {:charset "utf-8"}]
[:link {:nonce "%NONCE%" ;; this will be str/replaced by 'style-tag-nonce-middleware
:rel "stylesheet"
:href "https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap"}]]
[:body [:h2 (format "Backend Artifacts Preview for Dashboard %s" id)]
(channel.render/render-dashboard-to-html id)]))})
(api.macros/defendpoint :get "/preview-card-info/:id"
"Get JSON object containing HTML rendering of a Card with `id` and other information."
[{:keys [id]} :- [:map
[:id ms/PositiveInt]]]
(let [card (api/read-check :model/Card id)
result (pulse-card-query-results card)
data (:data result)
card-type (channel.render/detect-pulse-chart-type card nil data)
card-html (html (channel.render/render-pulse-card-for-display (channel.render/defaulted-timezone card)
card
result
{:channel.render/include-title? true}))]
{:id id
:pulse_card_type card-type
:pulse_card_html card-html
:pulse_card_name (:name card)
:pulse_card_url (urls/card-url (:id card))
:row_count (:row_count result)
:col_count (count (:cols (:data result)))}))
(def ^:private preview-card-width 400)
(api.macros/defendpoint :get "/preview-card-png/:id"
"Get PNG rendering of a Card with `id`. Optionally specify `width` as a query parameter."
[{:keys [id]} :- [:map
[:id ms/PositiveInt]]
{:keys [width]} :- [:map
[:width {:optional true} [:maybe ms/PositiveInt]]]]
(let [card (api/read-check :model/Card id)
result (pulse-card-query-results card)
width (or width preview-card-width)
ba (channel.render/render-pulse-card-to-png (channel.render/defaulted-timezone card)
card
result
width
{:channel.render/include-title? true})]
{:status 200, :headers {"Content-Type" "image/png"}, :body (ByteArrayInputStream. ba)}))
(def ^:private ^{:arglists '([handler])} style-nonce-middleware
(metabase.api.routes.common/wrap-middleware-for-open-api-spec-generation
(partial channel.render/style-tag-nonce-middleware "/dev/preview/preview-dashboard")))
(def ^{:arglists '([request respond raise])} routes
"`/dev/preview` endpoints."
(api.macros/ns-handler *ns* style-nonce-middleware))

View File

@@ -1,12 +1,15 @@
(ns dev.api.routes
(:require [dev.api.prototype]
(:require [dev.api.preview]
[dev.api.prototype]
[metabase.api.util.handlers :as handlers]))
(comment
dev.api.preview/keep-me
dev.api.prototype/keep-me)
(def ^:private dev-routes-map
{"/prototype" 'dev.api.prototype})
{"/preview" 'dev.api.preview
"/prototype" 'dev.api.prototype})
(def ^{:arglists '([request respond raise])} routes
;; This map will be merged at the top level, so /dev/prototype is available.

Some files were not shown because too many files have changed in this diff Show More