Compare commits

..

333 Commits

Author SHA1 Message Date
Eric Normand
b584b35cd9 Better tests 2025-12-01 15:19:51 -06:00
Eric Normand
a36cda7ffd Merge branch 'master' into representations 2025-12-01 15:19:00 -06:00
Eric Normand
151f143233 Whitespace 2025-12-01 13:42:15 -06:00
Eric Normand
e440239b79 Move question test 2025-12-01 13:40:24 -06:00
Eric Normand
0f41ae3925 Remove stray whitespace 2025-12-01 13:35:25 -06:00
Eric Normand
d4344c308e Use representations build from clojars 2025-12-01 13:33:17 -06:00
Eric Normand
ae23f598f0 Remove special representations workflow 2025-11-25 15:01:42 -06:00
Eric Normand
2d6624319a Test collection tree import/export 2025-11-25 09:43:28 -06:00
Eric Normand
f51192d3db Collection tests! 2025-11-24 14:33:43 -06:00
Eric Normand
23754f9e95 Tests pass, kondo passes, cljfmt done 2025-11-24 14:18:16 -06:00
Eric Normand
ae65686d30 Fix tests 2025-11-21 15:49:57 -06:00
Eric Normand
f26f6a18cd core test pass 2025-11-21 15:25:43 -06:00
Eric Normand
4b33138799 Better database tests 2025-11-21 15:22:03 -06:00
Eric Normand
d3bad99de1 Whitespace 2025-11-21 15:21:51 -06:00
Eric Normand
a576039ae0 Remove persist! 2025-11-21 14:48:18 -06:00
Eric Normand
619e2fa17e Remove non-existent imports 2025-11-21 14:39:47 -06:00
Eric Normand
9116c09afe Get rid of frontend changes 2025-11-21 14:38:58 -06:00
Eric Normand
47b0340836 Merge branch 'master' into representations 2025-11-21 14:23:32 -06:00
Eric Normand
eda624973b Remove old stuff and simplify core 2025-11-21 14:21:48 -06:00
Eric Normand
0c0634dbc1 Module deps for kondo 2025-11-21 13:54:13 -06:00
Eric Normand
8a1139118f Remove transform-specific import 2025-11-21 13:53:57 -06:00
Eric Normand
8bb448b8fc People keep moving namespaces 2025-11-21 13:53:46 -06:00
Eric Normand
99e16ef825 Fixups after merge 2025-11-21 13:53:25 -06:00
Eric Normand
8b44711794 Don't throw if not found 2025-11-21 13:52:57 -06:00
Eric Normand
c35be5ea97 Remove unused transforms export 2025-11-21 13:52:43 -06:00
Eric Normand
04f1e58c3a Delete some unused files. 2025-11-21 13:50:55 -06:00
Eric Normand
20657a3b0b Remove dependencies "integration" (unused) 2025-11-19 11:34:10 -06:00
Eric Normand
174a118a6c Claude fixed the kondo warnings 2025-11-19 11:23:53 -06:00
Eric Normand
4d6693adae Typo 2025-11-19 10:27:24 -06:00
Eric Normand
91d2191c93 Merge branch 'master' into representations 2025-11-19 10:17:57 -06:00
Riley Thompson
86dcae0d73 clean up pulse ui 2025-11-13 19:07:01 -05:00
Riley Thompson
ea48c27d72 use refs with pulses 2025-11-13 19:02:23 -05:00
Riley Thompson
13cb16df11 initial pulse impl 2025-11-13 16:57:35 -05:00
Riley Thompson
89e585ac23 finish timeline tests 2025-11-13 15:30:05 -05:00
Riley Thompson
7c8078bd09 Merge branch 'master' into representations 2025-11-10 18:13:21 -05:00
Riley Thompson
8e37784337 update timelines 2025-11-10 17:52:06 -05:00
Riley Thompson
b0443a4805 timeline impl 2025-11-10 17:13:20 -05:00
Eric Normand
ddcc13edd1 Simplify portable query visitor 2025-11-07 16:53:26 -06:00
Eric Normand
38a5d7b604 Simplify portable query visitor protocol 2025-11-07 16:49:36 -06:00
Eric Normand
938600d341 Fix mbql tests 2025-11-07 11:17:18 -06:00
Eric Normand
1469864d71 Question tests pass again 2025-11-07 11:07:20 -06:00
Eric Normand
777980d11e Forgot the new file 2025-11-07 10:40:52 -06:00
Eric Normand
0a63009b9e lib.portable and representations integration 2025-11-07 10:37:56 -06:00
Eric Normand
609a58ad6d Tiny cleanup 2025-11-04 15:48:39 -06:00
Eric Normand
e202541ac8 Don't double ref: 2025-11-04 15:27:12 -06:00
Eric Normand
7e54e25766 Better 2025-11-04 15:09:27 -06:00
Eric Normand
9b903edd5e document 2025-11-04 14:33:50 -06:00
Eric Normand
ea1cdf4917 Checkpoint 2025-11-04 10:35:49 -06:00
Eric Normand
c58e26c8e2 Add resolve to export 2025-11-03 17:28:03 -06:00
Timothy Dean
99d1bd8cdf Use lookup/lookup-database-id in more places 2025-11-03 14:42:52 -07:00
Timothy Dean
57999716ce Playing with model metadata 2025-11-03 14:05:31 -07:00
Eric Normand
57f64aded5 Hook rename refs into api 2025-11-03 13:34:16 -06:00
Eric Normand
8d204c712e Whitespace 2025-11-03 13:28:25 -06:00
Eric Normand
2cbece56c3 Nice set of name tests with a munge-name implementation 2025-11-03 13:01:15 -06:00
Timothy Dean
c8c515462a Fix bad var reference 2025-11-03 10:53:40 -07:00
Timothy Dean
4ed9e32676 Delete unused and stale yml files 2025-10-31 16:48:46 -06:00
Timothy Dean
3a02474632 Delete bin/* scripts 2025-10-31 16:44:03 -06:00
Timothy Dean
20875923d7 Delete example-manifest.yml 2025-10-31 16:36:36 -06:00
Timothy Dean
2b2cd76b2f Delete ./representation babashka stuff 2025-10-31 16:35:28 -06:00
Eric Normand
6a78c448cf Oops bad merge 2025-10-31 16:24:46 -05:00
Eric Normand
81fa467312 Export ref renaming 2025-10-31 16:18:40 -05:00
Eric Normand
7b604076bc move more stuff for order-reprsentations 2025-10-31 16:18:00 -05:00
Timothy Dean
8f8b2b762a Bugfixes in insert! and update! 2025-10-31 14:24:33 -06:00
Timothy Dean
db2d21ef20 Fix v0.common-test errors 2025-10-31 14:19:49 -06:00
Timothy Dean
c2a163701b Step 3, multimethods gone and circular dependencies resolved. 2025-10-31 14:16:53 -06:00
Timothy Dean
2a9df86ff0 Step 2 of cleanup, removing import dependency from v0.* representation nses 2025-10-31 13:45:31 -06:00
Timothy Dean
b2502af083 Step 1 of a series of big cleanup commits 2025-10-31 13:35:06 -06:00
Eric Normand
4cd66a1b6e move order-representations into v0-common 2025-10-31 11:37:48 -05:00
Eric Normand
eb9a0acb6f Oops 2025-10-31 10:22:09 -05:00
Timothy Dean
bc6f659963 m. Remove ref: initial from workflows importing metabase/representations 2025-10-30 16:09:09 -06:00
Timothy Dean
93fe76a5c6 More temporary stuff, getting verify-i18n-files running 2025-10-30 16:06:35 -06:00
Timothy Dean
993672c878 Merge remote-tracking branch 'origin/master' into representations 2025-10-30 15:58:00 -06:00
Timothy Dean
2421a24f31 More temporary boilerplate 2025-10-30 15:57:41 -06:00
Timothy Dean
2688cf4cfb Build artifact with metabase/representations 2025-10-30 15:53:20 -06:00
Timothy Dean
4ac312e62c Missed a file 2025-10-30 15:50:16 -06:00
Timothy Dean
32a67d3633 Experimental. Delete copy of metabase/representations, use PAT to check out private repo 2025-10-30 15:46:02 -06:00
Eric Normand
e343d5ebbe Default db. Fix insert/update. Default to name if display_name is not avilable. 2025-10-30 14:49:36 -05:00
Eric Normand
49020716b5 Multify insert! and update! 2025-10-30 13:38:03 -05:00
Timothy Dean
e406c0c35c Merge remote-tracking branch 'origin/master' into representations 2025-10-29 17:10:05 -06:00
Timothy Dean
3cd8c2f2cd Fix core-test 2025-10-29 17:02:35 -06:00
Timothy Dean
59f53c16db The big switchover to metabase/representations for schemas 2025-10-29 16:57:55 -06:00
Timothy Dean
cac18c4db2 Copy in metabase/representations 2025-10-29 16:56:34 -06:00
Timothy Dean
dde1cd6b95 Copy from metabase/representations 2025-10-29 15:19:08 -06:00
Timothy Dean
e4b0d120e5 Copy 2b8f5d6 from metabase/representations 2025-10-29 14:31:00 -06:00
Eric Normand
56fac7fa11 Transforms order and better export/import 2025-10-29 15:23:27 -05:00
Eric Normand
6826b70279 Import test 2025-10-29 14:01:13 -05:00
Eric Normand
dae088f934 Databases maintain key order 2025-10-29 12:37:59 -05:00
Eric Normand
983bf8b002 Maintain model key order 2025-10-29 12:35:22 -05:00
Eric Normand
709a494ad4 :ref->:name in export 2025-10-29 12:32:20 -05:00
Eric Normand
29a5c1d804 Metric maintains order of keys 2025-10-29 12:32:05 -05:00
Eric Normand
d2a4f43bfa Maintain order of question keys 2025-10-29 12:31:54 -05:00
Eric Normand
9f04d4525c Docstring 2025-10-29 12:17:00 -05:00
Timothy Dean
6a98142d10 name -> display_name, ref -> name 2025-10-29 11:16:04 -06:00
Eric Normand
6e075b3fee MBQL5 fields appear to work 2025-10-29 12:04:58 -05:00
Eric Normand
f4f2fae807 Also use database: in referred to databases 2025-10-29 11:57:25 -05:00
Eric Normand
4b1783d724 ALways use query key 2025-10-29 11:57:02 -05:00
Eric Normand
9bb863e36f Reduce tables on export 2025-10-29 11:00:19 -05:00
Eric Normand
18b558507c Reduce database tables on export 2025-10-29 10:52:54 -05:00
Eric Normand
60af228ce8 Don't convert to markdown for now 2025-10-29 10:52:38 -05:00
Eric Normand
4ebe289c9e Represetnations button on database admin screen 2025-10-29 10:27:37 -05:00
Eric Normand
b96378dd1d Try deps free version 2025-10-29 09:57:35 -05:00
Eric Normand
80a1ee7544 Change to tiptap 2025-10-29 09:51:35 -05:00
Eric Normand
190b660861 Refs for documents 2025-10-28 16:39:25 -05:00
Eric Normand
69a0182908 Can you handle the whitespace? 2025-10-28 16:13:20 -05:00
Eric Normand
096c943179 Better document export 2025-10-28 15:53:53 -05:00
Eric Normand
3250dbbe3b Document prosemirror to markdown 2025-10-28 15:53:53 -05:00
Timothy Dean
eb27ad00a2 Copy changes from metabase/representations 2025-10-28 12:03:41 -06:00
Timothy Dean
e5d50e6e41 Add :local/root to deps.edn for copy of metabase/representations 2025-10-28 11:33:39 -06:00
Timothy Dean
e876cc59ff Copy in metabase/representations 2025-10-28 11:19:52 -06:00
Timothy Dean
04f22bce1c Revert "Attempt to bring in metabase/representations as submodule"
This reverts commit 38edca362b.
2025-10-28 11:12:19 -06:00
Timothy Dean
fe38c6dbfb Revert "Maybe github CI can use submodules: true"
This reverts commit ab8d8e1a85.
2025-10-28 11:12:05 -06:00
Timothy Dean
ab8d8e1a85 Maybe github CI can use submodules: true 2025-10-28 11:06:40 -06:00
Timothy Dean
38edca362b Attempt to bring in metabase/representations as submodule 2025-10-28 11:00:11 -06:00
Eric Normand
481d5c7dd7 Fix card question 2025-10-28 11:47:55 -05:00
Eric Normand
bd407a7a23 Use native-only-query? 2025-10-28 11:17:34 -05:00
Eric Normand
81d235e1f9 Docstring 2025-10-28 10:35:48 -05:00
Eric Normand
a6e8e815e1 Correct env var problem 2025-10-28 10:34:21 -05:00
Eric Normand
20bc080cbe Source cards and native queris 2025-10-28 10:34:21 -05:00
Timothy Dean
9489190f9b Merge remote-tracking branch 'origin/master' into representations 2025-10-28 09:21:47 -06:00
Timothy Dean
481b92c289 Remove whitespace 2025-10-28 09:21:03 -06:00
Timothy Dean
adcc8e3e5c Merge remote-tracking branch 'origin/master' into representations 2025-10-28 09:17:36 -06:00
Eric Normand
d9fff0a2e7 Documents and collections representations 2025-10-27 18:38:41 -05:00
Eric Normand
ebe192cc51 Yes, transforms too. 2025-10-27 18:10:05 -05:00
Eric Normand
fbe0f1a83b Better UI, more cards 2025-10-27 18:10:05 -05:00
Timothy Dean
5f100521aa More use of lookup-database-id 2025-10-27 15:59:37 -06:00
Timothy Dean
dcc2e5b777 disable test, add comments 2025-10-27 14:58:26 -06:00
Timothy Dean
3152844182 Transforms code is stale and broken 2025-10-27 14:52:32 -06:00
Timothy Dean
6cd4d53aaa Fix typo in snippet-test and add uses of with-toucan-defaults 2025-10-27 14:42:53 -06:00
Timothy Dean
de157d98f1 Use v0-common/map-entity-index in more places 2025-10-27 14:37:16 -06:00
Timothy Dean
64a914bb6c Getting tests up to date 2025-10-27 14:22:59 -06:00
Timothy Dean
b392c931ef Delete stale test 2025-10-27 14:13:27 -06:00
Timothy Dean
57dce27409 Fix bug in new lookup-id impl for maps, update common-test 2025-10-27 14:10:05 -06:00
Timothy Dean
7bbf5f082e Update stale reference to old with-toucan-defaults 2025-10-27 14:00:25 -06:00
Timothy Dean
6a90502eaa Fix CI issue: create lookup/lookup-database-id 2025-10-27 13:51:02 -06:00
Timothy Dean
be07b8fe0f Remove metabase/representations dependency so CI can run 2025-10-27 13:41:19 -06:00
Timothy Dean
802765a6b4 Fix with-toucan-defaults, add resolve-database-id, get more tests passing 2025-10-27 13:40:28 -06:00
Eric Normand
8e4d85d37b Get a modal. It works. Could be better. 2025-10-27 14:12:20 -05:00
Timothy Dean
ae17a6f937 Fix compilation error in representations.export 2025-10-27 11:41:01 -06:00
Timothy Dean
cafc5540c0 Add metabase/representations dependency 2025-10-27 11:41:01 -06:00
Eric Normand
ccc6c717d2 Missing yamls 2025-10-27 12:26:17 -05:00
Timothy Dean
944cefd0b5 Merge remote-tracking branch 'origin/master' into representations 2025-10-27 11:05:48 -06:00
Eric Normand
8bd2e87737 WIP document 2025-10-24 14:25:59 -05:00
Eric Normand
9927a4abcb Add with-toucan-defaults 2025-10-23 14:37:20 -05:00
Eric Normand
b087de7b13 Guard against infinite loops in order-representations 2025-10-23 12:23:37 -05:00
Eric Normand
3760109621 Robust tests against faulty cycle detection 2025-10-22 16:59:44 -05:00
Eric Normand
c44514f19e Cleanup and test 2025-10-22 16:50:44 -05:00
Eric Normand
d197607055 Add a first implementation of a RefIndex 2025-10-22 15:32:37 -05:00
Eric Normand
5b4b835a45 Move representation-type to common 2025-10-22 15:17:09 -05:00
Eric Normand
9cb6173c3f Cycle detection in order-representations 2025-10-22 14:44:28 -05:00
Eric Normand
04fb2f80b8 Cleanup 2025-10-22 12:47:34 -05:00
Eric Normand
434e4c77b4 Whitespace 2025-10-22 12:35:46 -05:00
Eric Normand
fa86a2f729 Convert type->model to multimethod 2025-10-22 12:35:22 -05:00
Eric Normand
3e8cc7d241 Add docstrings in lookup 2025-10-22 12:01:12 -05:00
Eric Normand
9672377eff Add some todos for migration in comments 2025-10-22 12:00:52 -05:00
Eric Normand
2d25ff6801 More testing and polishing 2025-10-20 15:10:03 -05:00
Eric Normand
23c32a9679 More lookup functions 2025-10-20 13:37:11 -05:00
Eric Normand
192a3f8cab Good tests for exporting based on types 2025-10-20 13:21:01 -05:00
Eric Normand
d48e2e167f Test lookup and type conversion 2025-10-20 13:07:07 -05:00
Eric Normand
0fc449bb3f First pass at better import (WIP) 2025-10-14 13:55:33 +02:00
Eric Normand
0ccfcf9368 At leasts the tests pass 2025-10-10 23:06:49 -05:00
Eric Normand
f920a0ad6b Work with lib in transforms??? 2025-10-10 22:59:52 -05:00
Eric Normand
ad03eb5e71 Fix transform tests 2025-10-10 22:54:28 -05:00
Eric Normand
968ab66658 Get lib questions working (with tests) 2025-10-10 22:41:56 -05:00
Timothy Dean
531c0a2ec0 Tweaking mbql.clj 2025-10-10 13:46:38 -06:00
Timothy Dean
361b089ae6 Fix nesting bug in mbql.clj, re-enable a models test 2025-10-10 13:13:50 -06:00
Timothy Dean
f868deee6b Models and their metadata working much better 2025-10-10 12:55:18 -06:00
Timothy Dean
e24dd51f77 Land bugfixes in flow we're not using any longer, but it's related to the collections.yml ticket 2025-10-10 12:54:41 -06:00
Eric Normand
a8c3bf86bf Missed a reference 2025-10-10 12:31:11 -05:00
Eric Normand
f77ee2c518 Oops cyclical dep 2025-10-10 12:27:42 -05:00
Eric Normand
126768e0b4 Cleanup mbql stuff 2025-10-10 12:27:08 -05:00
Riley Thompson
a0088a70e2 Merge branch 'representations' of https://github.com/metabase/metabase into representations 2025-10-10 13:24:43 -04:00
Riley Thompson
64860019bc add snippet import tests 2025-10-10 13:24:33 -04:00
Eric Normand
b899d8cd97 Update test 2025-10-10 11:23:54 -05:00
Eric Normand
fc0f79e339 Export only schemas and tables that are referred to 2025-10-10 11:22:55 -05:00
Riley Thompson
60050841a2 snippet export test 2025-10-09 18:35:32 -04:00
Eric Normand
bc8d0d0423 Metrics import/export tests 2025-10-09 15:02:19 -05:00
Riley Thompson
75de8e0572 remove static serve code from 3312f91 and 89bf939 2025-10-09 15:21:57 -04:00
Riley Thompson
9b1198ce7b remove frontend code 2025-10-09 14:50:58 -04:00
Riley Thompson
f1cb9c64fd use refs directory for db representations 2025-10-09 11:43:48 -04:00
Riley Thompson
80af50a637 replace content with sql in snippet representation 2025-10-09 11:29:22 -04:00
Riley Thompson
0ac69e8f89 Merge branch 'representations' of https://github.com/metabase/metabase into representations 2025-10-08 16:33:14 -04:00
Riley Thompson
2b3a76cf5b use lib native to build template tags 2025-10-08 16:33:09 -04:00
Eric Normand
a4acab9eff Dependency check API and beginnings of import 2025-10-08 13:32:22 -05:00
Riley Thompson
8dd09eaf20 Merge branch 'representations' of https://github.com/metabase/metabase into representations 2025-10-08 14:06:53 -04:00
Riley Thompson
085b060c4c add template tags to sql snippetes yaml->toucan 2025-10-08 14:06:43 -04:00
Timothy Dean
fb34fc1654 babashka import: manually fix a few issues, but the code organization is pretty bad 2025-10-08 11:00:46 -06:00
Riley Thompson
ca9f37c05f replace versioned :type key with separate :version and :type keys 2025-10-08 12:16:47 -04:00
Timothy Dean
2ebdcb6139 Init commit for representation.bb 2025-10-07 19:00:23 -06:00
Riley Thompson
1300e4fe2d Merge branch 'representations' of https://github.com/metabase/metabase into representations 2025-10-07 18:30:55 -04:00
Riley Thompson
ee5c71754a sql snippet representations 2025-10-07 18:30:49 -04:00
Eric Normand
e0790dda10 Structural change for better recursion 2025-10-07 16:21:41 -05:00
Eric Normand
ca30b7d361 Export entire collection (API) 2025-10-07 16:18:08 -05:00
Eric Normand
6697c05014 Can check question dependencies 2025-10-07 15:39:54 -05:00
Eric Normand
56e70074d0 Merge branch 'representations' of github.com:metabase/metabase into representations 2025-10-07 12:15:01 -05:00
Eric Normand
7e0b5e85cd Don't use ref/ namespace 2025-10-07 12:14:34 -05:00
Timothy Dean
e7154969aa Delete a ton of code and experimental feature work. 2025-10-07 10:46:25 -06:00
Timothy Dean
471e76be64 Whitespace 2025-10-06 13:01:42 -06:00
Timothy Dean
25908d5f96 Fix core-test 2025-10-06 12:53:41 -06:00
Timothy Dean
42f5d6cc13 Merge remote-tracking branch 'origin/master' into representations 2025-10-06 12:44:46 -06:00
Timothy Dean
e3a9edcb7a Fix question-test 2025-10-06 12:41:56 -06:00
Timothy Dean
cf1c863b2b Fix YAML dumper-options, filenames, misc. 2025-10-06 12:19:26 -06:00
Timothy Dean
f7db036c73 Add latest export examples 2025-10-06 12:18:09 -06:00
Timothy Dean
49cb9edfa7 Fix topological sort for base64-encoded mbql 2025-10-06 11:10:36 -06:00
Timothy Dean
0fd583ee05 Non-cleaned-up, but new tests and metadata overrides for models. Mix of hand and vibe code, cleanup needed 2025-10-04 00:57:43 -06:00
Timothy Dean
a33fee2435 MBQL separated as base64-encoded blob 2025-10-03 23:33:53 -06:00
Timothy Dean
de107e5639 Add forgotten file. 2025-10-03 17:20:59 -06:00
Timothy Dean
339ce9b24c export-import-with-refs-test 2025-10-03 17:12:09 -06:00
Timothy Dean
07b6dcd9ea export-with-refs and export-with-ids 2025-10-03 16:56:57 -06:00
Timothy Dean
560a3b67f0 Cleanup, about to refactor to allow bimodal exports 2025-10-03 15:39:08 -06:00
Eric Normand
d8ae013339 Ooops! 2025-10-03 15:58:36 -05:00
Eric Normand
ad06c09272 Transforms work! 2025-10-03 15:35:38 -05:00
Timothy Dean
b0d38931d0 Some fixes, mostly around models 2025-10-03 12:15:50 -06:00
Eric Normand
30f997054b Make question tests pass 2025-10-01 16:51:44 -05:00
Eric Normand
bc73878e2a Fix kondo and remove bad docstring 2025-10-01 14:07:41 -05:00
Eric Normand
2bdaa1ca68 Make export-set take a collection of reps 2025-10-01 14:07:23 -05:00
Eric Normand
949633567d Whitespace in cond 2025-10-01 14:06:57 -05:00
Eric Normand
ee68edecae Fix question test 2025-10-01 14:06:45 -05:00
Eric Normand
6b055d5133 Use u/remove-nils instead of custom one 2025-10-01 13:36:55 -05:00
Timothy Dean
c39e42692c Fix circular dependencies 2025-09-30 23:55:11 -06:00
Timothy Dean
34dc6533b2 Cleaning up a little, and resolving 104 kondo warnings 2025-09-30 22:52:08 -06:00
Timothy Dean
46bf926ee3 Get import/export buttons working more nicely (for database.yml and question.yml only) 2025-09-30 16:13:12 -06:00
Riley Thompson
54389ff03a fix collection script 2025-09-30 17:10:01 -04:00
Riley Thompson
13a76e983e Merge branch 'master' into representations 2025-09-30 17:04:15 -04:00
Riley Thompson
34b84f3151 download and upload transform represenations 2025-09-30 17:02:04 -04:00
Timothy Dean
d65e9c8ec4 Merge duplicated work, get batch-imports working for at least questions and databases.
Eric and I duplicated some work. This merges our efforts. This also builds in ref patching on import and export and provides an `import-dir!` method that #worksonmymachine.

Co-authored-by: Eric Normand <ericwnormand@gmail.com>
2025-09-29 14:23:59 -06:00
Timothy Dean
29263dad18 Fix circular dependency, clean up some code 2025-09-26 15:15:13 -06:00
Eric Normand
b70431b8f6 Match existing database by name or do nothing. 2025-09-26 12:47:46 -05:00
Eric Normand
57c7b6e15c Database api and better yaml format 2025-09-26 11:45:03 -05:00
Eric Normand
9b0a44d385 Update database export
Env vars for passwords and other secure details
2025-09-26 11:41:56 -05:00
Eric Normand
c0bec3f32e Remove question 2025-09-26 11:41:38 -05:00
Eric Normand
2510bae3c2 Tests 2025-09-26 11:09:46 -05:00
Eric Normand
1dad0e5c1e Import ordering 2025-09-25 15:25:27 -05:00
Eric Normand
a9be96bbd2 Merge branch 'representations' of github.com:metabase/metabase into representations 2025-09-25 13:23:18 -05:00
Eric Normand
1625842d80 Whitespace 2025-09-25 13:21:02 -05:00
Eric Normand
9af2a858cb Export databases. 2025-09-25 13:19:18 -05:00
Eric Normand
942fcbd2d0 Remove prints 2025-09-25 13:17:56 -05:00
Timothy Dean
67566c475b m. 2025-09-24 17:58:23 -06:00
Timothy Dean
e1bf8c6582 Basic database export functionality 2025-09-24 17:56:06 -06:00
Timothy Dean
069d773c94 Make collection-8 test_resources dir mirroring /collection/8-serialization-v0 2025-09-24 15:47:25 -06:00
Eric Normand
82029ecfbb We're not ready for import-export 2025-09-24 14:26:24 -05:00
Riley Thompson
7070fac7a6 add frontend 2025-09-24 14:15:42 -04:00
Timothy Dean
2653b9d303 Merge remote-tracking branch 'origin/master' into representations 2025-09-24 11:39:14 -06:00
Riley Thompson
addf6fda01 export and import collection representation endpoints 2025-09-24 13:00:51 -04:00
Timothy Dean
4a2651851b representations.core: remove unnecessary decoder code 2025-09-24 10:34:09 -06:00
Timothy Dean
2f80bab68f Fix transform schema 2025-09-24 10:21:03 -06:00
Timothy Dean
a136a66af7 Just delete run-trigger 2025-09-23 17:02:05 -06:00
Timothy Dean
aab38f089a Transform improvements 2025-09-23 16:51:09 -06:00
Eric Normand
8dd1b8c4cc import/export tests for questions 2025-09-23 16:02:37 -05:00
Eric Normand
d8e3b30a83 Add transforms export 2025-09-23 14:35:42 -05:00
Eric Normand
6b9a0c0277 Add roundtrip tests 2025-09-23 14:31:12 -05:00
Timothy Dean
774cc91a4e Transforms schema tweaks, ingestion. Persist method not well tested 2025-09-23 10:22:30 -06:00
Eric Normand
0d185aa777 Export metrics 2025-09-23 10:10:22 -05:00
Eric Normand
4434205bc0 Add model test 2025-09-23 09:34:19 -05:00
Timothy Dean
9fc8a2724d Add validate-representation script 2025-09-22 14:05:04 -06:00
Timothy Dean
94a5098f74 Add load-representation script 2025-09-22 13:44:14 -06:00
Timothy Dean
5c788efbe1 Questions and models from serialization-v0 collection 2025-09-22 12:47:58 -06:00
Eric Normand
dc9bbc95d5 More question tests 2025-09-22 11:39:47 -05:00
Eric Normand
29e408bf72 Merge branch 'master' into representations 2025-09-22 10:06:21 -05:00
Timothy Dean
9398a2f092 Push experimental model yml file 2025-09-20 01:45:06 -05:00
Timothy Dean
bed66ab743 Fix yarn dev 2025-09-20 01:39:08 -05:00
Timothy Dean
ed931e2404 Playing with model metadata 2025-09-20 01:38:13 -05:00
Timothy Dean
afa3d6d70c Fix the /load endpoint 2025-09-20 01:37:15 -05:00
dan sutton
d616d269f9 fixup tests
need db to run db tests. and validation checks against schema
2025-09-19 17:36:31 -05:00
Eric Normand
ea1ef9c283 Comment 2025-09-19 16:57:54 -05:00
Eric Normand
639fcf2a3e Add question tests 2025-09-19 16:54:31 -05:00
dan sutton
89bf939860 referencing questions in static assets
```
❯ tree /tmp/behold
/tmp/behold
├── c-1-card196164.card.yml
├── c-2-card196324.card.yml
├── sub-collection
│   └── c-3-card12345.card.yml
└── with-references
    ├── question-122.yml
    └── question-125.yml

3 directories, 5 files
```

the with-references directory is as follows:

```yaml
name: kondo_lints_question
type: question
ref: question-122
mbql_query:
  type: query
  database: pg
  query:
    source-table:
    - pg
    - public
    - kondo_lints
    fields:
    - - field
      - - pg
        - public
        - kondo_lints
        - id
      - base-type: type/Integer
    - - field
      - - pg
        - public
        - kondo_lints
        - filename
      - base-type: type/Text
    - - field
      - - pg
        - public
        - kondo_lints
        - line
      - base-type: type/Integer
    - - field
      - - pg
        - public
        - kondo_lints
        - status
      - base-type: type/Text
    - - field
      - - pg
        - public
        - kondo_lints
        - error
      - base-type: type/Text
    - - field
      - - pg
        - public
        - kondo_lints
        - category
      - base-type: type/Text
database: pg
```

and 125:

```yaml
name: kondo_lints_question, Filtered by Status is error
type: question
ref: question-125
mbql_query:
  database: pg
  type: query
  query:
    source-table:
      ref: question-122
    filter:
    - =
    - - field
      - status
      - base-type: type/Text
    - error
database: pg
```

It _references_ another static, non-app-db question

```clojure
core=> (:dataset_query (fetch :question -5))
{:database 3,
 :type "query",
 :query {:source-table "card__-4", ;; <----- card -4
         :filter ["="
                  ["field" "status" {:base-type "type/Text"}]
                  "error"]}}
core=> (->> (qp/process-query (:dataset_query (fetch :question -5)))
            :data :rows (take 2))
([12933
  "shared/src/metabase/mbql/normalize.cljc"
  594
  "error"
  "Recur can only be used in tail position."
  "shared/src/metabase/mbql/normalize.cljc"]
 [12934
  "shared/src/metabase/mbql/normalize.cljc"
  594
  "error"
  "Unresolved symbol"
  "shared/src/metabase/mbql/normalize.cljc"])
```
2025-09-19 16:38:05 -05:00
Eric Normand
c635152b92 Fixes 2025-09-19 15:50:30 -05:00
Eric Normand
7e8d562358 Move export functions 2025-09-19 14:25:21 -05:00
Eric Normand
320aab30bc whitespace 2025-09-19 14:25:16 -05:00
Eric Normand
968cf8c336 Add a tiny test 2025-09-19 13:37:15 -05:00
Eric Normand
f4f22cb733 Revert all disablings 2025-09-19 13:33:30 -05:00
Timothy Dean
48239abdd3 Add an MBQL model.yml file 2025-09-19 12:24:33 -06:00
Eric Normand
2c40e5f217 kick ci 2025-09-19 13:22:56 -05:00
Eric Normand
38550465eb disable frontend tests? 2025-09-19 13:20:19 -05:00
Eric Normand
8f73866c87 Disable frontend tests? 2025-09-19 13:19:20 -05:00
Eric Normand
d1625d7384 Skip frontend? 2025-09-19 13:18:11 -05:00
Eric Normand
e22c93ed4c try skip 2025-09-19 13:11:23 -05:00
Eric Normand
a229d08410 remove stuff from run-tests.yml 2025-09-19 13:07:49 -05:00
Eric Normand
35aec6c7cb Revert "Delete e2e tests files"
This reverts commit 58c4abdd10.
2025-09-19 13:05:51 -05:00
Eric Normand
58c4abdd10 Delete e2e tests files 2025-09-19 13:04:02 -05:00
Eric Normand
08e9272440 Get representation tests to run???? 2025-09-19 12:51:38 -05:00
Eric Normand
5c9303e6e3 Attempt to disable tests 2025-09-19 12:49:47 -05:00
Eric Normand
554c6388b3 Remove more tests 2025-09-19 12:41:13 -05:00
Eric Normand
f21d0de52e Disable backend test runs 2025-09-19 12:11:19 -05:00
Eric Normand
8dc61e4eb0 Disable more tests 2025-09-19 12:08:10 -05:00
Timothy Dean
270fcf4299 Reorganize code again based on huddle discussion 2025-09-19 11:06:43 -06:00
Eric Normand
77cfe40954 kick ci 2025-09-19 11:58:19 -05:00
Eric Normand
6c4f743f38 Attempt: Disable driver tests and add representation tests runner 2025-09-19 11:56:39 -05:00
Eric Normand
65a5a67624 wip 2025-09-19 11:06:15 -05:00
Timothy Dean
abaac8c3f7 m. Rename 2025-09-19 09:20:07 -06:00
Timothy Dean
e1406025a9 Provide a wrapper for yaml->toucan operations 2025-09-19 09:18:41 -06:00
dan sutton
3312f915ee Static serving
given

```
❯ tree /tmp/swaperoo
/tmp/swaperoo
├── c-1-card196164.card.yml
├── c-2-card196324.card.yml
└── sub-collection
    └── c-3-card12345.card.yml

2 directories, 3 files
```

the running

```
clj -M:dev:ee:ee-dev:drivers:drivers-dev:socket -m metabase.core.bootstrap static --directory /tmp/swaperoo
```

Will serve the assets from /tmp/swaperoo.
It does this by loading them in MEMORY and assigning them sequential
negative ids. These are not loaded into the app-db.

```
❯ tree /tmp/swaperoo
/tmp/swaperoo  (collection -1: swaperoo)
├── c-1-card196164.card.yml (card -1)
├── c-2-card196324.card.yml (card -2)
└── sub-collection (collection -2: sub-collection)
    └── c-3-card12345.card.yml (card -3)

```
2025-09-19 09:32:02 -05:00
Timothy Dean
3809c909f0 Create an invalid example yml file 2025-09-18 16:44:09 -06:00
Timothy Dean
388f208bb2 Tweak model.yml 2025-09-18 16:28:12 -06:00
Eric Normand
55ef1e8392 card->question 2025-09-18 17:14:56 -05:00
Eric Normand
576535eaf7 dataset->model 2025-09-18 17:09:20 -05:00
Timothy Dean
78e3b5d282 Wire up model ingestion 2025-09-18 15:50:51 -06:00
Timothy Dean
ff96dabfcf Slightly improved /validate endpoint 2025-09-18 15:35:02 -06:00
Timothy Dean
3dece4143a WIP model ingestion, not wired up yet 2025-09-18 15:28:52 -06:00
Eric Normand
092674a6b8 string instead of keyword 2025-09-18 16:22:21 -05:00
dan sutton
4d46a3beb4 clean up representation -> card in memory representation 2025-09-18 16:21:33 -05:00
Eric Normand
edccda93bf Add bb script to export a collection 2025-09-18 16:18:05 -05:00
Timothy Dean
2d10da8267 Stop hardcoding :native on question ingestion 2025-09-18 15:11:16 -06:00
Timothy Dean
64d22caf90 Move creator-id and entity-id determination into representation->question-data 2025-09-18 15:00:24 -06:00
Eric Normand
c09d983f2c Add collection endpoint 2025-09-18 15:50:49 -05:00
Eric Normand
503c35172d Merge branch 'master' into representations 2025-09-18 15:02:12 -05:00
Eric Normand
57906d9ddc kick ci 2025-09-18 14:59:28 -05:00
Eric Normand
0e2fdcdfef Merge branch 'representations' of github.com:metabase/metabase into representations 2025-09-18 13:55:55 -05:00
Eric Normand
83f6814775 kick ci 2025-09-18 13:55:37 -05:00
Timothy Dean
1bb21b1ad7 Add /validate endpoint to API 2025-09-18 11:46:51 -06:00
Timothy Dean
fb62d65095 m. 2025-09-18 11:37:10 -06:00
Timothy Dean
ad1423767a Better example model.yml 2025-09-18 11:36:14 -06:00
Timothy Dean
ef661cc1d1 Add example model yml with some schema 2025-09-18 11:29:51 -06:00
Timothy Dean
be445608ce Create representations.ingestion.common 2025-09-18 11:23:48 -06:00
Eric Normand
075ee44a6c Forgot this file 2025-09-18 12:05:25 -05:00
Eric Normand
407e6dd824 Export models
Note: I copied the export for questions, including the logic and schema. I don't know if there's something else in
models (metadata?) that I am not capturing.
2025-09-18 12:04:35 -05:00
Eric Normand
5b86da8b8b Export mbql questions 2025-09-18 11:01:06 -05:00
Eric Normand
39d9982ae4 API for exporting a native query question.
GET /api/ee/representation/question/92
2025-09-18 10:38:10 -05:00
Timothy Dean
530b985bc4 tweak commented out dev utils 2025-09-17 17:35:24 -06:00
Timothy Dean
ab8afe34b1 Delete 'card' as a named entity, reorganize namespaces as cleanup 2025-09-17 17:31:45 -06:00
Eric Normand
26b4d85352 API route
curl -X POST -H "Content-Type: text/yaml" -H "X-API-Key: mb_5Qj3t1s5NYcXNq1jB3wIebP3rAhLcmmgkSBp8YTvXOU=" --data-binary @test_resources/representations/v0/monthly-revenue.question.yml localhost:3002/api/ee/representation/load/5
2025-09-17 17:30:44 -05:00
Timothy Dean
558a2f91b2 On card ingestion, update existing card if ref matches 2025-09-17 16:24:56 -06:00
Timothy Dean
6b9381b859 Tweak collection schema 2025-09-17 16:15:48 -06:00
Timothy Dean
0cfa725615 Hook up card ingestion 2025-09-17 15:50:36 -06:00
Timothy Dean
7d74867058 Ingest one example question.yml file and insert it into the app-db 2025-09-17 15:42:20 -06:00
Timothy Dean
bf3a3e9e00 WIP: ingest & validate yml against schemas 2025-09-17 12:30:48 -06:00
dan sutton
bcca5c2329 first pass at writing representations
Interesting things:
- using serdes mbql export. this puts nano-ids, so we walk over each card, get mapping of nano-id to id, then replace
with refs. So the ingestion will need to do the opposite.

  ;; do we like entity type in filename?
  ;; walk over all, identity which depend on other refs. so first, get all refs
  ;; then walk over each dataset_query, looking for any refs that are reffed
  ;; then do topological sort, inserting cards without deps and keeping a map of ref to inserted id
  ;; update existing refs to ids (note source-tables are "card__<id>"
  ;; insert and continue

```clojure
core=> (write-em "/tmp" 8)
.
..nil
```

Very dump to write dots but this is exploratory, baby.

example:

(note, rows in yaml are actually commented out but git would remove them)
```yaml
❯ tree custom
custom
├── c-115-card1150883.card.yml
├── c-116-card1151398.card.yml
└── c-117-card1150868.card.yml

1 directory, 3 files

/tmp on ☁️  metabase-query
❯ cat custom/*
name: Orders and weird column
version: 1-card
type: card
ref: c-115-card1150883
dataset_query:
  database: Sample Database
  type: query
  query:
    source-table:
    - Sample Database
    - PUBLIC
    - ORDERS
    expressions:
      sum-of-weird:
      - +
      - - field
        - - Sample Database
          - PUBLIC
          - ORDERS
          - TAX
        - base-type: type/Float
      - - field
        - - Sample Database
          - PUBLIC
          - ORDERS
          - TOTAL
        - base-type: type/Float

 [1 1 14 37.648145389078365 2.07 39.718145389078366 nil "2025-02-11T21:40:27.892Z" 2 41.788145389078366 "Hudson Borer" "Awesome Concrete Shoes"]
 [2 1 123 110.93145648834248 6.1 117.03145648834247 nil "2024-05-15T08:04:04.58Z" 3 123.13145648834247 "Hudson Borer" "Mediocre Wooden Bench"]
 [3 1 105 52.723521442619514 2.9 49.206842233769756 6.416679208849759 "2025-12-06T22:22:48.544Z" 2 52.106842233769754 "Hudson Borer" "Fantastic Wool Shirt"]
 [4 1 94 109.21864156655383 6.01 115.22864156655383 nil "2025-08-22T16:30:42.392Z" 6 121.23864156655384 "Hudson Borer" "Awesome Bronze Plate"]
 [5 1 132 127.88197029833711 7.03 134.9119702983371 nil "2024-10-10T03:34:47.309Z" 5 141.9419702983371 "Hudson Borer" "Sleek Steel Table"]
 [6 1 60 29.80214751859149 1.64 31.44214751859149 nil "2025-11-06T16:38:50.134Z" 3 33.08214751859149 "Hudson Borer" "Rustic Paper Car"]
 [7 1 55 95.77128575934437 5.27 101.04128575934436 nil "2024-09-11T11:22:26.521Z" 5 106.31128575934436 "Hudson Borer" "Mediocre Rubber Shoes"]
 [8 1 65 68.22769726470014 3.75 63.32374433449318 8.653952930206964 "2025-06-17T02:37:41.693Z" 7 67.07374433449317 "Hudson Borer" "Ergonomic Silk Table"]
 [9 1 184 77.3982748679465 4.26 78.06353271268733 3.594742155259162 "2023-05-03T16:00:54.923Z" 3 82.32353271268734 "Hudson Borer" "Practical Granite Plate"]
 [10 1 6 97.43621265344382 5.36 102.79621265344382 nil "2026-01-17T01:44:37.233Z" 2 108.15621265344382 "Hudson Borer" "Small Marble Hat"]
name: Orders and weird column, Filtered by sum-of-weird is greater than 10
version: 1-card
type: card
ref: c-116-card1151398
dataset_query:
  database: Sample Database
  type: query
  query:
    source-table: c-115-card1150883
    filter:
    - '>'
    - - field
      - sum-of-weird
      - base-type: type/Float
    - 10

 [1 1 14 37.648145389078365 2.07 39.718145389078366 nil "2025-02-11T21:40:27.892Z" 2 41.788145389078366 "Hudson Borer" "Awesome Concrete Shoes"]
 [2 1 123 110.93145648834248 6.1 117.03145648834247 nil "2024-05-15T08:04:04.58Z" 3 123.13145648834247 "Hudson Borer" "Mediocre Wooden Bench"]
 [3 1 105 52.723521442619514 2.9 49.206842233769756 6.416679208849759 "2025-12-06T22:22:48.544Z" 2 52.106842233769754 "Hudson Borer" "Fantastic Wool Shirt"]
 [4 1 94 109.21864156655383 6.01 115.22864156655383 nil "2025-08-22T16:30:42.392Z" 6 121.23864156655384 "Hudson Borer" "Awesome Bronze Plate"]
 [5 1 132 127.88197029833711 7.03 134.9119702983371 nil "2024-10-10T03:34:47.309Z" 5 141.9419702983371 "Hudson Borer" "Sleek Steel Table"]
 [6 1 60 29.80214751859149 1.64 31.44214751859149 nil "2025-11-06T16:38:50.134Z" 3 33.08214751859149 "Hudson Borer" "Rustic Paper Car"]
 [7 1 55 95.77128575934437 5.27 101.04128575934436 nil "2024-09-11T11:22:26.521Z" 5 106.31128575934436 "Hudson Borer" "Mediocre Rubber Shoes"]
 [8 1 65 68.22769726470014 3.75 63.32374433449318 8.653952930206964 "2025-06-17T02:37:41.693Z" 7 67.07374433449317 "Hudson Borer" "Ergonomic Silk Table"]
 [9 1 184 77.3982748679465 4.26 78.06353271268733 3.594742155259162 "2023-05-03T16:00:54.923Z" 3 82.32353271268734 "Hudson Borer" "Practical Granite Plate"]
 [10 1 6 97.43621265344382 5.36 102.79621265344382 nil "2026-01-17T01:44:37.233Z" 2 108.15621265344382 "Hudson Borer" "Small Marble Hat"]
name: 5 products
version: 1-card
type: card
ref: c-117-card1150868
dataset_query:
  database: Sample Database
  type: native
  native:
    template-tags:
      category_filter:
        widget-type: string/=
        display-name: Category Filter
        type: dimension
        name: category_filter
        id: b5cca8e2-d397-4195-9957-1f69fda66d95
        dimension:
        - field
        - - Sample Database
          - PUBLIC
          - PRODUCTS
          - CATEGORY
        - null
    query: select * from products where {{category_filter}} limit 5

 [1 "1018947080336" "Rustic Paper Wallet" "Gizmo" "Swaniawski, Casper and Hilll" 29.463261130679875 4.6 "2023-07-19T19:44:56.582Z"]
 [2 "7663515285824" "Small Marble Shoes" "Doohickey" "Balistreri-Ankunding" 70.07989613071763 0.0 "2025-04-11T08:49:35.932Z"]
 [3 "4966277046676" "Synergistic Granite Chair" "Doohickey" "Murray, Watsica and Wunsch" 35.388744881539054 4.0 "2024-09-08T22:03:20.239Z"]
 [4 "4134502155718" "Enormous Aluminum Shirt" "Doohickey" "Regan Bradtke and Sons" 73.99178100854834 3.0 "2024-03-06T02:53:09.937Z"]
 [5 "5499736705597" "Enormous Marble Wallet" "Gadget" "Price, Schultz and Daniel" 82.7450976850356 4.0 "2022-10-03T01:47:39.147Z"]
```
2025-09-16 16:49:54 -05:00
Timothy Dean
25c142a7aa v0 scaffolding collection.yml 2025-09-16 15:43:43 -06:00
Timothy Dean
2d53706de7 v0 database scaffolding 2025-09-16 15:36:14 -06:00
Timothy Dean
ae6f82e0ee Scaffolding: clj schemas 2025-09-16 15:16:19 -06:00
Timothy Dean
219685e26f Scaffolding 2025-09-16 15:01:02 -06:00
Timothy Dean
1f892946c9 Scaffolding 2025-09-16 14:53:43 -06:00
Timothy Dean
cdacf75b02 Init commit 2025-09-16 13:59:53 -06:00
10284 changed files with 608185 additions and 1106088 deletions

View File

@@ -1,12 +0,0 @@
Generate a CI failure report for PR $PR_NUM_OR_URL (or current branch if no argument given).
Run `./bin/mage ci-report $PR_NUM_OR_URL` to generate the report, then:
1. **Summarize the failures** - What tests failed and in which jobs?
2. **Categorize** - Are these real failures, flaky tests, or infrastructure issues?
3. **Recommend next steps**:
- If flaky: suggest rerunning with `gh run rerun <run-id> --failed -R metabase/metabase`
- If real: identify the root cause and suggest fixes
- If infrastructure: note the issue type (timeout, OOM, network)
Focus on actionable insights. If all checks are passing, confirm that briefly.

View File

@@ -1 +0,0 @@
Run mutation testing for $ARGUMENTS

View File

@@ -12,7 +12,14 @@
## Testing
Use `./bin/test-agent` to run backend tests. See the top-level CLAUDE.md for usage.
- **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.
## Code Readability
@@ -22,11 +29,6 @@ Use `./bin/test-agent` to run backend tests. See the top-level CLAUDE.md for usa
## REPL Usage
> **Note:** If you have `clojure-mcp` tools available (check for tools like `clojure_eval`),
> **always prefer those over `./bin/mage -repl`**. The MCP tools provide better integration,
> richer feedback, and avoid shell escaping issues. Only use `./bin/mage -repl` as a fallback
> when clojure-mcp is not available.
- **Evaluating Clojure Code:** `./bin/mage -repl '<code>'`
- See "Sending Code to the REPL" section for more details

View File

@@ -29,7 +29,7 @@ This guide covers Clojure and ClojureScript coding conventions for Metabase. See
**Comments:**
- `TODO` format: `;; TODO (Name YYYY-MM-DD) -- description`
- `TODO` format: `;; TODO (Name M/D/YY) -- description`
## Code Organization
@@ -42,7 +42,7 @@ This guide covers Clojure and ClojureScript coding conventions for Metabase. See
- Break up functions > 20 lines
- Lines ≤ 120 characters
- No blank non-comment lines within definition forms (except pairwise `let`/`cond`)
- No blank lines within definition forms (except pairwise `let`/`cond`)
## Style Conventions

View File

@@ -1,21 +1,21 @@
## Linting and Formatting
- **Lint:** `bun run lint-eslint-pure`
- **Lint:** `yarn lint-eslint-pure`
- Run ESLint on the codebase
- **Format:** `bun run prettier`
- **Format:** `yarn prettier`
- Format code using Prettier
- **Type Check:** `bun run type-check-pure`
- **Type Check:** `yarn type-check-pure`
- Run TypeScript type checking
## Testing
### JavaScript/TypeScript Tests
- **Test a specific file:** `bun run test-unit-keep-cljs path/to/file.unit.spec.js`
- **Test by pattern:** `bun run test-unit-keep-cljs -t "pattern"`
- **Test a specific file:** `yarn test-unit-keep-cljs path/to/file.unit.spec.js`
- **Test by pattern:** `yarn test-unit-keep-cljs -t "pattern"`
- Runs tests matching the given pattern
### ClojureScript Tests
- **Test ClojureScript:** `bun run test-cljs`
- **Test ClojureScript:** `yarn test-cljs`
- Run ClojureScript tests

View File

@@ -1,420 +0,0 @@
---
name: add-malli-schemas
description: Efficiently add Malli schemas to API endpoints in the Metabase codebase with proper patterns, validation timing, and error handling
---
# Add Malli Schemas to API Endpoints
This skill helps you efficiently and uniformly add Malli schemas to API endpoints in the Metabase codebase.
## Reference Files (Best Examples)
- `src/metabase/warehouses/api.clj` - Most comprehensive schemas, custom error messages
- `src/metabase/api_keys/api.clj` - Excellent response schemas
- `src/metabase/collections/api.clj` - Great named schema patterns
- `src/metabase/timeline/api/timeline.clj` - Clean, simple examples
## Quick Checklist
When adding Malli schemas to an endpoint:
- [ ] Route params have schemas
- [ ] Query params have schemas with `:optional true` and `:default` where appropriate
- [ ] Request body has a schema (for POST/PUT)
- [ ] Response schema is defined (using `:-` after route string)
- [ ] Use existing schema types from `ms` namespace when possible
- [ ] Consider creating named schemas for reusable or complex types
- [ ] Add contextual error messages for validation failures
## Basic Structure
### Complete Endpoint Example
```clojure
(mr/def ::Color [:enum "red" "blue" "green"])
(mr/def ::ResponseSchema
[:map
[:id pos-int?]
[:name string?]
[:color ::Color]
[:created_at ms/TemporalString]])
(api.macros/defendpoint :post "/:name" :- ::ResponseSchema
"Create a resource with a given name."
[;; Route Params:
{:keys [name]} :- [:map [:name ms/NonBlankString]]
;; Query Params:
{:keys [include archived]} :- [:map
[:include {:optional true} [:maybe [:= "details"]]]
[:archived {:default false} [:maybe ms/BooleanValue]]]
;; Body Params:
{:keys [color]} :- [:map [:color ::Color]]
]
;; endpoint implementation, ex:
{:id 99
:name (str "mr or mrs " name)
:color ({"red" "blue" "blue" "green" "green" "red"} color)
:created_at (t/format (t/formatter "yyyy-MM-dd'T'HH:mm:ssXXX") (t/zoned-date-time))}
)
```
## Common Schema Patterns
1. Route Params (the 5 in `api/user/id/5`)
2. Query Params (the sort+asc pair in `api/users?sort=asc`)
3. Body Params (the contents of a request body. Almost always decoded from json into edn)
4. The Raw Request map
Of the 4 arguments, deprioritize usage of the raw request unless necessary.
### Route Params
Always required, typically just a map with an ID:
```clojure
[{:keys [id]} :- [:map [:id ms/PositiveInt]]]
```
For multiple route params:
```clojure
[{:keys [id field-id]} :- [:map
[:id ms/PositiveInt]
[:field-id ms/PositiveInt]]]
```
### Query Params
Add properties for `{:optional true ...}` and `:default` values:
```clojure
{:keys [archived include limit offset]} :- [:map
[:archived {:default false} [:maybe ms/BooleanValue]]
[:include {:optional true} [:maybe [:= "tables"]]]
[:limit {:optional true} [:maybe ms/PositiveInt]]
[:offset {:optional true} [:maybe ms/PositiveInt]]]
```
### Request Body (POST/PUT)
```clojure
{:keys [name description parent_id]} :- [:map
[:name ms/NonBlankString]
[:description {:optional true} [:maybe ms/NonBlankString]]
[:parent_id {:optional true} [:maybe ms/PositiveInt]]]
```
### Response Schemas
#### Simple inline response:
```clojure
(api.macros/defendpoint :get "/:id" :- [:map
[:id pos-int?]
[:name string?]]
"Get a thing"
...)
```
#### Named schema for reuse:
```clojure
(mr/def ::Thing
[:map
[:id pos-int?]
[:name string?]
[:description [:maybe string?]]])
(api.macros/defendpoint :get "/:id" :- ::Thing
"Get a thing"
...)
(api.macros/defendpoint :get "/" :- [:sequential ::Thing]
"Get all things"
...)
```
## Common Schema Types
### From `metabase.util.malli.schema` (aliased as `ms`)
Prefer the schemas in the ms/* namespace, since they work better with our api infrastructure.
For example use `ms/PositiveInt` instead of `pos-int?`.
```clojure
ms/PositiveInt ;; Positive integer
ms/NonBlankString ;; Non-empty string
ms/BooleanValue ;; String "true"/"false" or boolean
ms/MaybeBooleanValue ;; BooleanValue or nil
ms/TemporalString ;; ISO-8601 date/time string (for REQUEST params only!)
ms/Map ;; Any map
ms/JSONString ;; JSON-encoded string
ms/PositiveNum ;; Positive number
ms/IntGreaterThanOrEqualToZero ;; 0 or positive
```
**IMPORTANT:** For response schemas, use `:any` for temporal fields, not `ms/TemporalString`!
Response schemas validate BEFORE JSON serialization, so they see Java Time objects.
### Built-in Malli Types
```clojure
:string ;; Any string
:boolean ;; true/false
:int ;; Any integer
:keyword ;; Clojure keyword
pos-int? ;; Positive integer predicate
[:maybe X] ;; X or nil
[:enum "a" "b" "c"] ;; One of these values
[:or X Y] ;; Schema that satisfies X or Y
[:and X Y] ;; Schema that satisfies X and Y
[:sequential X] ;; Sequential of Xs
[:set X] ;; Set of Xs
[:map-of K V] ;; Map with keys w/ schema K and values w/ schema V
[:tuple X Y Z] ;; Fixed-length tuple of schemas X Y Z
```
Avoid using sequence schemas unless completely necessary.
## Step-by-Step: Adding Schemas to an Endpoint
### Example: Adding return schema to `GET /api/field/:id/related`
**Before:**
```clojure
(api.macros/defendpoint :get "/:id/related"
"Return related entities."
[{:keys [id]} :- [:map [:id ms/PositiveInt]]]
(-> (t2/select-one :model/Field :id id) api/read-check xrays/related))
```
**Step 1:** Check what the function returns (look at `xrays/related`)
**Step 2:** Define response schema based on return type:
```clojure
(mr/def ::RelatedEntity
[:map
[:tables [:sequential [:map [:id pos-int?] [:name string?]]]]
[:fields [:sequential [:map [:id pos-int?] [:name string?]]]]])
```
**Step 3:** Add response schema to endpoint:
```clojure
(api.macros/defendpoint :get "/:id/related" :- ::RelatedEntity
"Return related entities."
[{:keys [id]} :- [:map [:id ms/PositiveInt]]]
(-> (t2/select-one :model/Field :id id) api/read-check xrays/related))
```
## Advanced Patterns
### Custom Error Messages
```clojure
(def DBEngineString
"Schema for a valid database engine name."
(mu/with-api-error-message
[:and
ms/NonBlankString
[:fn
{:error/message "Valid database engine"}
#(u/ignore-exceptions (driver/the-driver %))]]
(deferred-tru "value must be a valid database engine.")))
```
### Enum with Documentation
```clojure
(def PinnedState
(into [:enum {:error/message "pinned state must be 'all', 'is_pinned', or 'is_not_pinned'"}]
#{"all" "is_pinned" "is_not_pinned"}))
```
### Complex Nested Response
```clojure
(mr/def ::DashboardQuestionCandidate
[:map
[:id ms/PositiveInt]
[:name ms/NonBlankString]
[:description [:maybe string?]]
[:sole_dashboard_info
[:map
[:id ms/PositiveInt]
[:name ms/NonBlankString]
[:description [:maybe string?]]]]])
(mr/def ::DashboardQuestionCandidatesResponse
[:map
[:data [:sequential ::DashboardQuestionCandidate]]
[:total ms/PositiveInt]])
```
### Paginated Response Pattern
```clojure
(mr/def ::PaginatedResponse
[:map
[:data [:sequential ::Item]]
[:total integer?]
[:limit {:optional true} [:maybe integer?]]
[:offset {:optional true} [:maybe integer?]]])
```
## Common Pitfalls
### Don't: Forget `:maybe` for nullable fields
```clojure
[:description ms/NonBlankString] ;; WRONG - fails if nil
[:description [:maybe ms/NonBlankString]] ;; RIGHT - allows nil
```
### Don't: Forget `:optional true` for optional query params
```clojure
[:limit ms/PositiveInt] ;; WRONG - required but shouldn't be
[:limit {:optional true} [:maybe ms/PositiveInt]] ;; RIGHT
```
### Don't: Forget `:default` values for known params
```clojure
[:limit ms/PositiveInt] ;; WRONG - required but shouldn't be
[:limit {:optional true :default 0} [:maybe ms/PositiveInt]] ;; RIGHT
```
### Don't: Mix up route params, query params, and body
```clojure
;; WRONG - all in one map
[{:keys [id name archived]} :- [:map ...]]
;; RIGHT - separate destructuring
[{:keys [id]} :- [:map [:id ms/PositiveInt]]
{:keys [archived]} :- [:map [:archived {:default false} ms/BooleanValue]]
{:keys [name]} :- [:map [:name ms/NonBlankString]]]
```
### Don't: Use `ms/TemporalString` for Java Time objects in response schemas
```clojure
;; WRONG - Java Time objects aren't strings yet
[:date_joined ms/TemporalString]
;; RIGHT - schemas validate BEFORE JSON serialization
[:date_joined :any] ;; Java Time object, serialized to string by middleware
[:last_login [:maybe :any]] ;; Java Time object or nil
```
**Why:** Response schemas validate the internal Clojure data structures BEFORE they are serialized to JSON. Java Time objects like `OffsetDateTime` get converted to ISO-8601 strings by the JSON middleware, so the schema needs to accept the raw Java objects.
### Don't: Use `[:sequential X]` when the data is actually a set
```clojure
;; WRONG - group_ids is actually a set
[:group_ids {:optional true} [:sequential pos-int?]]
;; RIGHT - matches the actual data structure
[:group_ids {:optional true} [:maybe [:set pos-int?]]]
```
**Why:** Toucan hydration methods often return sets. The JSON middleware will serialize sets to arrays, but the schema validates before serialization.
### Don't: Create anonymous schemas for reused structures
Use `mr/def` for schemas used in multiple places:
```clojure
(mr/def ::User
[:map
[:id pos-int?]
[:email string?]
[:name string?]])
```
## Finding Return Types
1. **Look at the function being called**
```clojure
(api.macros/defendpoint :get "/:id"
[{:keys [id]}]
(t2/select-one :model/Field :id id)) ;; Returns a Field instance
```
2. **Check Toucan models for structure**
Look in `src/metabase/*/models/*.clj` for model definitions.
3. **Use clojure-mcp or REPL to inspect**
```bash
./bin/mage -repl '(require '\''metabase.xrays.core) (doc metabase.xrays.core/related)'
```
4. **Check tests**
Tests often show the expected response structure.
## Understanding Schema Validation Timing
**CRITICAL CONCEPT:** Schemas validate at different points in the request/response lifecycle:
### Request Parameter Schemas (Query/Body/Route)
- Validate AFTER JSON parsing
- Data is already deserialized (strings, numbers, booleans)
- Use `ms/TemporalString` for date/time inputs
- Use `ms/BooleanValue` for boolean query params
### Response Schemas
- Validate BEFORE JSON serialization
- Data is still in Clojure format (Java Time objects, sets, keywords)
- Use `:any` for Java Time objects
- Use `[:set X]` for sets
- Use `[:enum :keyword]` for keyword enums
### Serialization Flow
```
Request: JSON string → Parse → Coerce → Handler
Response: Handler → Schema Check → Encode → Serialize → JSON string
```
## Workflow Summary
1. **Read the endpoint** - understand what it does
2. **Identify params** - route, query, body
3. **Add parameter schemas** - use existing types from `ms`
4. **Determine return type** - check the implementation
5. **Define response schema** - inline or named with `mr/def`
6. **Test** - ensure the endpoint works and validates correctly
## Testing Your Schemas
After adding schemas, verify:
1. **Valid requests work** - test with correct data
2. **Invalid requests fail gracefully** - test with wrong types
3. **Optional params work** - test with/without optional params
4. **Error messages are clear** - check validation error responses
## Tips
- **Start simple** - begin with basic types, refine later
- **Reuse schemas** - if you see the same structure twice, make it a named schema
- **Be specific** - use `ms/PositiveInt` instead of `pos-int?`
- **Document intent** - add docstrings to named schemas
- **Follow conventions** - look at similar endpoints in the same namespace
- **Check the actual data** - use REPL to inspect what's actually returned before serialization
## Additional Resources
- [Malli Documentation](https://github.com/metosin/malli)
- Metabase Malli utilities: `src/metabase/util/malli/schema.clj`
- Metabase schema registry: `src/metabase/util/malli/registry.clj`

View File

@@ -1,357 +0,0 @@
--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

@@ -1,504 +0,0 @@
---
name: analytics-events
description: Add product analytics events to track user interactions in the Metabase frontend
allowed-tools: Read, Write, Edit, Grep, Glob
---
# Frontend Analytics Events Skill
This skill helps you add product analytics (Snowplow) events to track user interactions in the Metabase frontend codebase.
## Quick Reference
Analytics events in Metabase use Snowplow with typed event schemas. All events must be defined in TypeScript types before use.
**Key Files:**
- `frontend/src/metabase-types/analytics/event.ts` - Event type definitions
- `frontend/src/metabase-types/analytics/schema.ts` - Schema registry
- `frontend/src/metabase/lib/analytics.ts` - Core tracking functions
- Feature-specific `analytics.ts` files - Tracking function wrappers
## Quick Checklist
When adding a new analytics event:
- [ ] Define event type in `frontend/src/metabase-types/analytics/event.ts`
- [ ] Add event to appropriate union type (e.g., `DataStudioEvent`, `SimpleEvent`)
- [ ] Create tracking function in feature's `analytics.ts` file
- [ ] Import and call tracking function at the interaction point
- [ ] Use `trackSimpleEvent()` for basic events (most common)
## Event Schema Types
### 1. Simple Events (Most Common)
Use `SimpleEventSchema` for straightforward tracking. It supports these standard fields:
```typescript
type SimpleEventSchema = {
event: string; // Required: Event name (snake_case)
target_id?: number | null; // Optional: ID of affected entity
triggered_from?: string | null; // Optional: UI location/context
duration_ms?: number | null; // Optional: Duration in milliseconds
result?: string | null; // Optional: Outcome (e.g., "success", "failure")
event_detail?: string | null; // Optional: Additional detail/variant
};
```
**When to use:** 90% of events fit this schema. Use for clicks, opens, closes, creates, deletes, etc.
### 2. Custom Schemas (legacy, no events are being added)
Consider adding new event schema only in very special cases.
**Examples:** `DashboardEventSchema`, `CleanupEventSchema`, `QuestionEventSchema`
## Step-by-Step: Adding a Simple Event
### Example: Track when a user applies filters in a table picker
#### Step 1: Define Event Types
Add event type definitions to `frontend/src/metabase-types/analytics/event.ts`:
```typescript
export type DataStudioTablePickerFiltersAppliedEvent = ValidateEvent<{
event: "data_studio_table_picker_filters_applied";
}>;
export type DataStudioTablePickerFiltersClearedEvent = ValidateEvent<{
event: "data_studio_table_picker_filters_cleared";
}>;
```
#### Step 2: Add to Union Type
Find or create the appropriate union type and add your events:
```typescript
export type DataStudioEvent =
| DataStudioLibraryCreatedEvent
| DataStudioTablePublishedEvent
| DataStudioGlossaryCreatedEvent
| DataStudioGlossaryEditedEvent
| DataStudioGlossaryDeletedEvent
| DataStudioTablePickerFiltersAppliedEvent // <- Add here
| DataStudioTablePickerFiltersClearedEvent; // <- Add here
```
#### Step 3: Create Tracking Functions
In your feature's `analytics.ts` file (e.g., `enterprise/frontend/src/metabase-enterprise/data-studio/analytics.ts`):
```typescript
import { trackSimpleEvent } from "metabase/lib/analytics";
export const trackDataStudioTablePickerFiltersApplied = () => {
trackSimpleEvent({
event: "data_studio_table_picker_filters_applied",
});
};
export const trackDataStudioTablePickerFiltersCleared = () => {
trackSimpleEvent({
event: "data_studio_table_picker_filters_cleared",
});
};
```
#### Step 4: Use in Components
Import and call the tracking function at the interaction point:
```typescript
import {
trackDataStudioTablePickerFiltersApplied,
trackDataStudioTablePickerFiltersCleared,
} from "metabase-enterprise/data-studio/analytics";
function FilterPopover({ filters, onSubmit }) {
const handleReset = () => {
trackDataStudioTablePickerFiltersCleared(); // <- Track here
onSubmit(emptyFilters);
};
return (
<form
onSubmit={(event) => {
event.preventDefault();
trackDataStudioTablePickerFiltersApplied(); // <- Track here
onSubmit(form);
}}
>
{/* form content */}
</form>
);
}
```
## Using SimpleEventSchema Fields
### Example: Event with target_id
```typescript
// Type definition
export type DataStudioLibraryCreatedEvent = ValidateEvent<{
event: "data_studio_library_created";
target_id: number | null;
}>;
// Tracking function
export const trackDataStudioLibraryCreated = (id: CollectionId) => {
trackSimpleEvent({
event: "data_studio_library_created",
target_id: Number(id),
});
};
// Usage
trackDataStudioLibraryCreated(newLibrary.id);
```
### Example: Event with triggered_from
```typescript
// Type definition
export type NewButtonClickedEvent = ValidateEvent<{
event: "new_button_clicked";
triggered_from: "app-bar" | "empty-collection";
}>;
// Tracking function
export const trackNewButtonClicked = (location: "app-bar" | "empty-collection") => {
trackSimpleEvent({
event: "new_button_clicked",
triggered_from: location,
});
};
// Usage
<Button onClick={() => {
trackNewButtonClicked("app-bar");
handleCreate();
}}>
New
</Button>
```
### Example: Event with event_detail
```typescript
// Type definition
export type MetadataEditEvent = ValidateEvent<{
event: "metadata_edited";
event_detail: "type_casting" | "semantic_type_change" | "visibility_change";
triggered_from: "admin" | "data_studio";
}>;
// Tracking function
export const trackMetadataChange = (
detail: "type_casting" | "semantic_type_change" | "visibility_change",
location: "admin" | "data_studio"
) => {
trackSimpleEvent({
event: "metadata_edited",
event_detail: detail,
triggered_from: location,
});
};
// Usage
trackMetadataChange("semantic_type_change", "data_studio");
```
### Example: Event with result and duration
```typescript
// Type definition
export type MoveToTrashEvent = ValidateEvent<{
event: "moved-to-trash";
target_id: number | null;
triggered_from: "collection" | "detail_page" | "cleanup_modal";
duration_ms: number | null;
result: "success" | "failure";
event_detail: "question" | "model" | "metric" | "dashboard";
}>;
// Tracking function
export const trackMoveToTrash = (params: {
targetId: number | null;
triggeredFrom: "collection" | "detail_page" | "cleanup_modal";
durationMs: number | null;
result: "success" | "failure";
itemType: "question" | "model" | "metric" | "dashboard";
}) => {
trackSimpleEvent({
event: "moved-to-trash",
target_id: params.targetId,
triggered_from: params.triggeredFrom,
duration_ms: params.durationMs,
result: params.result,
event_detail: params.itemType,
});
};
// Usage with timing
const startTime = Date.now();
try {
await moveToTrash(item);
trackMoveToTrash({
targetId: item.id,
triggeredFrom: "collection",
durationMs: Date.now() - startTime,
result: "success",
itemType: "question",
});
} catch (error) {
trackMoveToTrash({
targetId: item.id,
triggeredFrom: "collection",
durationMs: Date.now() - startTime,
result: "failure",
itemType: "question",
});
}
```
## Naming Conventions
### Event Names (snake_case)
```typescript
// Good
"data_studio_library_created"
"table_picker_filters_applied"
"metabot_chat_opened"
// Bad
"DataStudioLibraryCreated" // Wrong case
"tablePickerFiltersApplied" // Wrong case
"filters-applied" // Use underscore, not hyphen
```
### Event Type Names (PascalCase with "Event" suffix)
```typescript
// Good
DataStudioLibraryCreatedEvent
TablePickerFiltersAppliedEvent
MetabotChatOpenedEvent
// Bad
dataStudioLibraryCreated // Wrong case
DataStudioLibraryCreated // Missing "Event" suffix
```
### Tracking Function Names (camelCase with "track" prefix)
```typescript
// Good
trackDataStudioLibraryCreated
trackTablePickerFiltersApplied
trackMetabotChatOpened
// Bad
DataStudioLibraryCreated // Missing "track" prefix
track_library_created // Wrong case
logLibraryCreated // Use "track" prefix
```
## Common Patterns
### Pattern 1: Feature-Specific Union Types
Group related events together:
```typescript
export type DataStudioEvent =
| DataStudioLibraryCreatedEvent
| DataStudioTablePublishedEvent
| DataStudioGlossaryCreatedEvent;
export type MetabotEvent =
| MetabotChatOpenedEvent
| MetabotRequestSentEvent
| MetabotFixQueryClickedEvent;
// Then add to SimpleEvent union
export type SimpleEvent =
| /* other events */
| DataStudioEvent
| MetabotEvent
| /* more events */;
```
### Pattern 2: Conditional Tracking
Track different events based on user action:
```typescript
const handleSave = async () => {
if (isNewItem) {
await createItem(data);
trackItemCreated(newItem.id);
} else {
await updateItem(id, data);
trackItemUpdated(id);
}
};
```
## Common Pitfalls
### Don't: Add custom fields to SimpleEvent
```typescript
// WRONG - SimpleEvent doesn't support custom fields
export const trackFiltersApplied = (filters: FilterState) => {
trackSimpleEvent({
event: "filters_applied",
data_layer: filters.dataLayer, // ❌ Not in SimpleEventSchema
data_source: filters.dataSource, // ❌ Not in SimpleEventSchema
with_owner: filters.hasOwner, // ❌ Not in SimpleEventSchema
});
};
// RIGHT - Use only standard SimpleEventSchema fields
export const trackFiltersApplied = () => {
trackSimpleEvent({
event: "filters_applied",
});
};
// Or use event_detail for a single variant
export const trackFilterApplied = (filterType: string) => {
trackSimpleEvent({
event: "filter_applied",
event_detail: filterType, // ✓ "data_layer", "data_source", etc.
});
};
```
### Don't: Forget to add event to union type
```typescript
// Define the event
export type NewFeatureClickedEvent = ValidateEvent<{
event: "new_feature_clicked";
}>;
// ❌ WRONG - Forgot to add to SimpleEvent union
// Event won't be recognized by TypeScript
// ✓ RIGHT - Add to appropriate union
export type SimpleEvent =
| /* other events */
| NewFeatureClickedEvent;
```
### Don't: Mix up event name formats
```typescript
// WRONG
event: "dataStudioLibraryCreated" // camelCase
event: "data-studio-library-created" // kebab-case
event: "Data_Studio_Library_Created" // Mixed case
// RIGHT
event: "data_studio_library_created" // snake_case
```
### Don't: Track PII or sensitive data
```typescript
// WRONG - Don't track user emails, names, or sensitive data
trackSimpleEvent({
event: "user_logged_in",
event_detail: user.email, // ❌ PII
});
// RIGHT - Track non-sensitive identifiers only
trackSimpleEvent({
event: "user_logged_in",
target_id: user.id, // ✓ Just the ID
});
```
### Don't: Forget to track both success and failure
```typescript
// WRONG - Only tracking success
try {
await saveData();
trackDataSaved();
} catch (error) {
// ❌ No tracking for failure case
}
// RIGHT - Track both outcomes
try {
await saveData();
trackDataSaved({ result: "success" });
} catch (error) {
trackDataSaved({ result: "failure" });
}
```
## Testing Analytics Events
While developing, you can verify events are firing:
1. **Check browser console** - When `SNOWPLOW_ENABLED=true` in dev, events are logged
2. **Use shouldLogAnalytics** - Set in `metabase/env` to see all analytics in console
3. **Check Snowplow debugger** - Browser extension for Snowplow events
Example console output:
```
[SNOWPLOW EVENT | event sent:true], data_studio_table_picker_filters_applied
```
## File Organization
### Where to put tracking functions:
```
Feature-specific analytics functions:
frontend/src/metabase/{feature}/analytics.ts
enterprise/frontend/src/metabase-enterprise/{feature}/analytics.ts
Event type definitions (all in one place):
frontend/src/metabase-types/analytics/event.ts
Core tracking utilities:
frontend/src/metabase/lib/analytics.ts
```
## Real-World Examples
See these files for reference:
- **Simple events**: `enterprise/frontend/src/metabase-enterprise/data-studio/analytics.ts`
- **Events with variants**: `frontend/src/metabase/dashboard/analytics.ts`
- **Complex events**: `frontend/src/metabase/query_builder/analytics.js`
- **Event type examples**: `frontend/src/metabase-types/analytics/event.ts`
## Workflow Summary
1. **Identify the user interaction** to track
2. **Decide on event name** (snake_case, descriptive)
3. **Define event type** in `event.ts` using `ValidateEvent`
4. **Add to union type** (create feature union if needed)
5. **Create tracking function** in feature's `analytics.ts`
6. **Import and call** at the interaction point
7. **Test** that events fire correctly
## Tips
- **Be specific** - `filters_applied` is better than `action_performed`
- **Use past tense** - `library_created` not `create_library`
- **Group related events** - Create feature-specific event union types
- **Track meaningful actions** - Not every click needs tracking
- **Consider the data** - What would you want to analyze later?
- **Stay consistent** - Follow existing naming patterns in the codebase
- **Document context** - Use `triggered_from` to track where the action happened

View File

@@ -54,14 +54,14 @@ Use this to scan through changes efficiently:
- [ ] Public vars in `src` or `enterprise/backend/src` have useful docstrings
- [ ] Docstrings use Markdown conventions
- [ ] References use `[[other-var]]` not backticks
- [ ] `TODO` comments include author and date: `;; TODO (Name 2025-01-01) -- description`
- [ ] `TODO` comments include author and date: `;; TODO (Name 1/1/25) -- description`
### Code Organization
- [ ] Everything `^:private` unless used elsewhere
- [ ] No `declare` when avoidable (public functions near end)
- [ ] Functions under 20 lines when possible
- [ ] No blank, non-comment lines within definition forms (except pairwise constructs in `let`/`cond`)
- [ ] No blank lines within definition forms (except pairwise constructs in `let`/`cond`)
- [ ] Lines ≤ 120 characters
### Tests
@@ -118,7 +118,7 @@ Quick scan for common issues:
| `update-db`, `save-model` | Missing `!` for side effects: `update-db!`, `save-model!` |
| `snake_case_var` | Should use kebab-case |
| Public var without docstring | Add docstring explaining purpose |
| `;; TODO fix this` | Missing author/date: `;; TODO (Name 2025-01-01) -- description` |
| `;; TODO fix this` | Missing author/date: `;; TODO (Name 1/1/25) -- description` |
| `(defn foo ...)` in namespace used elsewhere | Should be `(defn ^:private foo ...)` |
| Function > 20 lines | Consider breaking up into smaller functions |
| `/api/dashboards/:id` | Use singular: `/api/dashboard/:id` |

View File

@@ -5,16 +5,6 @@ description: Guide Clojure and ClojureScript development using REPL-driven workf
# Clojure Development Skill
## Tool Preference
When `clojure-mcp` tools are available (e.g., `clojure_eval`, `clojure_edit`), **always use them**
instead of shell commands like `./bin/mage -repl`. The MCP tools provide:
- Direct REPL integration without shell escaping issues
- Better error messages and feedback
- Structural Clojure editing that prevents syntax errors
Only fall back to `./bin/mage` commands when clojure-mcp is not available.
@./../_shared/development-workflow.md
@./../_shared/clojure-style-guide.md
@./../_shared/clojure-commands.md

View File

@@ -54,6 +54,8 @@ Run through the diff looking for these issues:
**Formatting:**
- [ ] Backticks on UI elements (should use **bold**)
- [ ] Backticks used as quotation marks
- [ ] Ampersands as "and" substitute (except proper nouns)
- [ ] Inconsistent list formatting
@@ -72,6 +74,7 @@ Run through the diff looking for these issues:
| Pattern | Issue |
| ----------------------------- | --------------------------------------------- |
| `Button name` or `UI element` | Should use **bold** not backticks |
| we can do X, our feature | Should be "Metabase" or "it" |
| click here, read more here | Need descriptive link text |
| easy, simple, just | Remove condescending qualifiers |
@@ -105,10 +108,13 @@ Suggested fix or explanation
**Examples:**
> **Issue 1: Formal tone**
> **Issue 1: Backticks on UI elements**
> Line 42: This uses backticks for the UI element. Use **bold** instead: **Filter** not `Filter`.
> **Issue 2: Formal tone**
> Line 15: This could be more conversational. Consider: "You can't..." instead of "You cannot..."
> **Issue 2: Vague heading**
> **Issue 3: Vague heading**
> Line 8: The heading could be more specific. Try stating the point directly: "Run migrations before upgrading" vs "Upgrade process"
### PR review mode format
@@ -139,11 +145,10 @@ When posting to GitHub (PR mode), use the **pending review workflow**:
- All comments will appear together as one cohesive review
**Comment format example:**
```
**Issue 1: Formal tone**
**Issue 1: Backticks on UI elements**
This could be more conversational. Consider: "You can't..." instead of "You cannot..."
This uses backticks for the UI element. Use **bold** instead: **Filter** not `Filter`.
```
**IMPORTANT**:

View File

@@ -40,7 +40,7 @@ allowed-tools: Read, Write, Grep, Bash, Glob
**Format:**
- Run prettier on the file after making edits: `bun run prettier --write <file-path>`
- Run prettier on the file after making edits: `yarn prettier --write <file-path>`
- This ensures consistent formatting across all documentation
### Common patterns

View File

@@ -1,260 +0,0 @@
---
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

@@ -1,78 +0,0 @@
---
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

@@ -1,114 +0,0 @@
---
name: mutation-testing
description: Run mutation testing on a Clojure namespace, generate tests to kill surviving mutations, and open draft PRs with Linear issue tracking.
---
# Mutation Testing Skill
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 engine (generates reports, runs mutations)
- `dev/src/dev/mutation_testing.clj` — orchestration, Linear API, PR helpers
## Invocation
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
```
- `--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.
## Steps
### 1. Parse arguments
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.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
(mut-test/list-teams!)
(mut-test/set-config! {:team-id "<id>"})
```
### 3. Run
```clojure
(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"})
```
Pass the opts map with `:base-branch` and/or `:project-id` if those flags were provided in the arguments.
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
;; 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>))
```
### 5. Verify the prompt (dry run)
To inspect what Claude will see without invoking it:
```clojure
(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]))))
```
## Configuration
- **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,9 +12,8 @@ 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

@@ -1,7 +1,7 @@
;; -*- comment-column: 100; -*-
{:config-paths ["macros" "src" "config"]
:linters
{;;
{ ;;
;; enabled optional linters (these are disabled by default)
;;
@@ -50,15 +50,13 @@
;; disabled linters
;;
:redundant-ignore {:level :off} ; lots of false positives, see https://github.com/clj-kondo/clj-kondo/issues/2414
:redundant-ignore {:level :off} ; lots of false positives, see https://github.com/clj-kondo/clj-kondo/issues/2414
:unexpected-recur {:level :off} ; TODO (cam): I think we just need to tell it how to handle MBQL match and we can enable this?
;;
;; globally-enabled custom linters
;;
:metabase/check-defmulti-arglists {:level :warning}
:metabase/check-def-check-not-uppercase-name {:level :warning}
:metabase/check-def-no-underscores {:level :warning}
:metabase/check-me-humanize {:level :warning}
:metabase/defsetting-must-specify-export {:level :warning}
:metabase/defsetting-namespace {:level :warning}
@@ -67,9 +65,8 @@
:metabase/missing-test-expr-requires-in-cljs {:level :warning}
:metabase/modules {:level :warning} ; more config in `.clj-kondo/config/modules/config.edn`
:metabase/namespace-name {:level :warning}
:metabase/no-jsqlparser-imports {:level :warning}
:metabase/require-shape-checker {:level :warning}
:metabase/tests-must-live-in-known-modules {:level :warning}
:metabase/test-helpers-use-non-thread-safe-functions {:level :warning} ; more config in `.clj-kondo/config/test-helpers/config.edn`
:metabase/validate-defendpoint-route-uses-kebab-case {:level :warning}
:metabase/validate-defendpoint-query-params-use-kebab-case {:level :warning}
:metabase/validate-logging {:level :warning}
@@ -180,11 +177,6 @@
metabase.notification.models/create-notification!
metabase.pulse.send/send-pulse!
metabase-enterprise.transforms-python.python-runner-test/execute!
;; Workspace isolation functions are safe in parallel tests because they operate on
;; isolated, test-specific workspaces and databases that don't affect other tests
metabase-enterprise.workspaces.isolation/destroy-workspace-isolation!
metabase-enterprise.workspaces.isolation/ensure-database-isolation!
metabase-enterprise.workspaces.test-util/create-ready-ws!
;; Safe in tests, where we typically use a private, temporary index per thread.
metabase.search.core/reindex!
metabase.util/run-count!
@@ -198,7 +190,7 @@
:metabase/disallow-hardcoded-driver-names-in-tests
;; don't include `:h2` since that will probably lead to a mountain of tests where we are specifically testing
;; middleware or compilation behavior with just H2 since it's the default.
{:drivers #{;; core drivers
{:drivers #{ ;; core drivers
#_:h2
:postgres
:mysql
@@ -226,10 +218,7 @@
;; misc config for built-in linters
;;
:invalid-arity {:skip-args [metabase.lib.util.match/match
metabase.lib.util.match/match-lite
metabase.lib.util.match/match-many
metabase.lib.util.match/replace-lite]} ; TODO (cam): can we fix these?
:invalid-arity {:skip-args [metabase.lib.util.match/match]} ; TODO (cam): can we fix these?
:refer-all {:level :warning, :exclude [clojure.test]}
:unused-referred-var {:exclude {compojure.core [GET DELETE POST PUT]}}
@@ -240,6 +229,9 @@
{:exclude
[(build.uberjar/with-duration-ms)
(cljs.test/is [=? malli=])
(clojure.core.logic/fresh)
(clojure.core.logic/matcha)
(clojure.core.logic/run)
(clojure.test/is [=? malli= qv=])
(clojure.tools.macro/macrolet)
instaparse.core/transform
@@ -248,17 +240,13 @@
(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/match-lite-recursive)
(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 [<>])
@@ -310,14 +298,9 @@
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"}
}
toucan2.tools.debug/debug {:message "Don't commit code using t2/debug"}}
:discouraged-namespace
{camel-snake-kebab.core {:message "CSK is not Turkish-safe, use the versions in metabase.util instead."}
@@ -422,13 +405,12 @@
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
{cljs.cache/defcache clojure.core/deftype
clojure.core.cache/defcache clojure.core/deftype
clojure.core.logic/defne clj-kondo.lint-as/def-catch-all
clojure.test.check.clojure-test/defspec clojure.test/deftest
clojure.test.check.properties/for-all clojure.core/let
clojurewerkz.quartzite.jobs/defjob clojure.core/defn
@@ -438,10 +420,11 @@
metabase-enterprise.content-translation.routes-test/with-new-secret-key! clojure.core/fn
metabase-enterprise.serialization.test-util/with-dbs clojure.core/fn
metabase-enterprise.serialization.test-util/with-random-dump-dir clojure.core/let
metabase-enterprise.workspaces.test-util/with-resources! clojure.core/let
metabase-enterprise.workspaces.test-util/with-workspaces! clojure.core/let
metabase-enterprise.transforms.api-test/with-transform-tags clojure.core/let
metabase-enterprise.transforms.api-test/with-worker-runs clojure.core/let
metabase-enterprise.transforms.test-util/with-transform-cleanup! clojure.core/let
metabase-enterprise.transforms.execute/with-transform-lifecycle clojure.core/let
metabase.actions.test-util/with-actions clojure.core/let
metabase.transforms.test-util/with-transform-cleanup! clojure.core/let
metabase.api.common/let-404 clojure.core/let
metabase.app-db.custom-migrations.util/with-temp-scheduler! clojure.core/fn
metabase.app-db.custom-migrations/define-migration clj-kondo.lint-as/def-catch-all
@@ -469,8 +452,8 @@
metabase.premium-features.settings/define-premium-feature clojure.core/def
metabase.pulse.dashboard-subscription-test/with-dashboard-sub-for-card clojure.core/let
metabase.pulse.send-test/with-pulse-for-card clojure.core/let
metabase.query-processor.pipeline-queries-test/pmbql-query clojure.core/->
metabase.query-processor.pipeline-queries-test/run-pmbql-query clojure.core/->
metabase.query-processor-test.pipeline-queries-test/pmbql-query clojure.core/->
metabase.query-processor-test.pipeline-queries-test/run-pmbql-query clojure.core/->
metabase.query-processor.error-type/deferror clojure.core/def
metabase.query-processor.middleware.cache.impl/with-reducible-deserialized-results clojure.core/let
metabase.query-processor.middleware.process-userland-query-test/with-query-execution! clojure.core/let
@@ -529,16 +512,7 @@
{cljs.test/deftest hooks.clojure.test/deftest
cljs.test/is hooks.clojure.test/is
cljs.test/use-fixtures hooks.clojure.test/use-fixtures
clojure.core/def hooks.clojure.core.def/lint-def
clojure.core/definterface hooks.clojure.core.def/lint-def
clojure.core/defmacro hooks.clojure.core.defmacro/lint-defmacro
clojure.core/defmulti hooks.clojure.core.defmulti/lint-defmulti
clojure.core/defn hooks.clojure.core.defn/lint-defn
clojure.core/defn- hooks.clojure.core.defn/lint-defn
clojure.core/defonce hooks.clojure.core.def/lint-def
clojure.core/defprotocol hooks.clojure.core.def/lint-def
clojure.core/defrecord hooks.clojure.core.def/lint-def
clojure.core/deftype hooks.clojure.core.def/lint-def
clojure.core/ns hooks.clojure.core.ns/lint-ns
clojure.core/require hooks.clojure.core.require/lint-require
clojure.core/requiring-resolve hooks.clojure.core.require/lint-requiring-resolve
@@ -550,10 +524,8 @@
metabase-enterprise.advanced-permissions.models.permissions.application-permissions-test/with-new-group-and-current-graph hooks.common/with-two-top-level-bindings
metabase-enterprise.audit-app.pages-test/with-temp-objects hooks.common/with-one-binding
metabase-enterprise.cache.config-test/with-temp-persist-models hooks.common/with-seven-bindings
metabase-enterprise.dependencies.api-test/with-dependents-test! hooks.common/with-two-bindings
metabase-enterprise.metabot-v3.tools.dependencies-test/with-dependent-transforms! hooks.common/with-two-bindings
metabase-enterprise.semantic-search.db.connection/with-migrate-tx hooks.common/let-one-with-optional-value
metabase-enterprise.serialization.api-test/with-serialization-test-data! hooks.common/with-three-bindings
metabase-enterprise.serialization.test-util/with-temp-dpc hooks.toucan2.tools.with-temp/with-temp
metabase-enterprise.transforms-python.drivers-test/with-test-table hooks.common/with-two-bindings
metabase.api.common.internal-test/with-jetty-server hooks.common/with-one-binding
@@ -571,7 +543,7 @@
metabase.dashboards-rest.api-test/with-simple-dashboard-with-tabs hooks.common/with-one-binding
metabase.dashboards.models.dashboard-tab-test/with-dashtab-in-personal-collection hooks.common/with-one-top-level-binding
metabase.dashboards.models.dashboard-test/with-dash-in-collection! hooks.common/with-three-bindings
metabase.driver.bigquery-cloud-sdk-test/calculate-bird-scarcity hooks.metabase.query-processor.expressions-test/calculate-bird-scarcity
metabase.driver.bigquery-cloud-sdk-test/calculate-bird-scarcity hooks.metabase.query-processor-test.expressions-test/calculate-bird-scarcity
metabase.driver.mongo-test/with-describe-table-for-sample hooks.metabase.driver.mongo-test/with-describe-table-for-sample
metabase.driver.oracle-test/with-temp-user hooks.common/let-one-with-optional-value
metabase.driver/register! hooks.metabase.driver/register!
@@ -608,11 +580,11 @@
metabase.pulse.models.pulse-test/with-dashboard-subscription-in-collection! hooks.common/with-four-bindings
metabase.pulse.models.pulse-test/with-pulse-in-collection! hooks.common/with-four-bindings
metabase.pulse.test-util/checkins-query-card hooks.metabase.test.data/$ids
metabase.queries-rest.api.card-test/with-card-param-values-fixtures hooks.common/let-one-with-optional-value
metabase.queries-rest.api.card-test/with-temp-native-card hooks.common/with-two-bindings
metabase.queries-rest.api.card-test/with-temp-native-card-with-params hooks.common/with-two-bindings
metabase.query-processor.expressions-test/calculate-bird-scarcity hooks.metabase.query-processor.expressions-test/calculate-bird-scarcity
metabase.query-processor.filter-test/count-with-filter-clause hooks.metabase.test.data/$ids
metabase.queries.api.card-test/with-card-param-values-fixtures hooks.common/let-one-with-optional-value
metabase.queries.api.card-test/with-temp-native-card hooks.common/with-two-bindings
metabase.queries.api.card-test/with-temp-native-card-with-params hooks.common/with-two-bindings
metabase.query-processor-test.expressions-test/calculate-bird-scarcity hooks.metabase.query-processor-test.expressions-test/calculate-bird-scarcity
metabase.query-processor-test.filter-test/count-with-filter-clause hooks.metabase.test.data/$ids
metabase.query-processor.middleware.cache-test/with-mock-cache! hooks.common/with-two-bindings
metabase.request.core/with-current-request hooks.common/with-used-first-arg
metabase.request.core/with-current-user hooks.common/with-used-first-arg
@@ -711,7 +683,7 @@
metabase.parameters.chain-filter-test/chain-filter-search macros.metabase.parameters.chain-filter-test/chain-filter
metabase.public-sharing-rest.api-test/with-sharing-enabled-and-temp-card-referencing! macros.metabase.public-sharing-rest.api-test/with-sharing-enabled-and-temp-card-referencing!
metabase.public-sharing-rest.api-test/with-sharing-enabled-and-temp-dashcard-referencing! macros.metabase.public-sharing-rest.api-test/with-sharing-enabled-and-temp-dashcard-referencing!
metabase.queries-rest.api.card-test/with-ordered-items macros.metabase.queries-rest.api.card-test/with-ordered-items
metabase.queries.api.card-test/with-ordered-items macros.metabase.queries.api.card-test/with-ordered-items
metabase.query-analysis.models.query-analysis-test/with-test-setup macros.metabase.query-analysis.models.query-analysis-test/with-test-setup
metabase.query-analysis.task.test-setup/with-test-setup! macros.metabase.query-analysis.task.test-setup/with-test-setup!
metabase.query-processor.streaming/streaming-response macros.metabase.query-processor.streaming/streaming-response
@@ -788,24 +760,28 @@
:config-in-ns
{test-namespaces
{:linters
{:inline-def {:level :off}
:missing-docstring {:level :off}
:private-call {:level :off}
:metabase/defsetting-must-specify-export {:level :off}
:metabase/defsetting-namespace {:level :off}
:metabase/test-helpers-use-non-thread-safe-functions {:level :warning} ; more config in `.clj-kondo/config/test-helpers/config.edn
{:inline-def {:level :off}
:missing-docstring {:level :off}
:private-call {:level :off}
:metabase/defsetting-must-specify-export {:level :off}
:metabase/defsetting-namespace {:level :off}
:discouraged-var
{metabase.driver/database-supports? {:level :off}
metabase.lib.convert/->legacy-MBQL {:level :off}
metabase.lib.core/->legacy-MBQL {:level :off}}}}
metabase.lib.core/->legacy-MBQL {:level :off}}}
:hooks
{:analyze-call
{clojure.core/defmacro hooks.clojure.core.defn/non-thread-safe-form-should-end-with-exclamation
clojure.core/defn hooks.clojure.core.defn/non-thread-safe-form-should-end-with-exclamation
clojure.core/defn- hooks.clojure.core.defn/non-thread-safe-form-should-end-with-exclamation}}}
source-namespaces
{:linters
{:metabase/discourage-millis-duration {:level :warning}
:metabase/validate-defendpoint-has-response-schema {:level :warning}
:metabase/validate-escaped-single-quotes-in-i18n {:level :warning}
:metabase/validate-string-or-str-args-to-i18n {:level :warning}
{:metabase/validate-escaped-single-quotes-in-i18n {:level :warning}
:metabase/validate-string-or-str-args-to-i18n {:level :warning}
:metabase/discourage-millis-duration {:level :warning}
:discouraged-var
{clojure.core/with-redefs {:message "Don't use with-redefs outside of tests"}
@@ -934,7 +910,6 @@
clojure.core/every? {:message "Use metabase.util.performance/every?"}
clojure.core/mapv {:message "Use metabase.util.performance/mapv"}
clojure.core/run! {:message "Use metabase.util.performance/run!"}
clojure.core/get-in {:message "Use metabase.util.performance/get-in"}
clojure.core/select-keys {:message "Use metabase.util.performance/select-keys"}
clojure.core/some {:message "Use metabase.util.performance/some"}
clojure.core/update-keys {:message "Use metabase.util.performance/update-keys"}
@@ -982,4 +957,4 @@
{:discouraged-java-method
{java.lang.System {exit
{:level :error
:message "Don't use System/exit directly. Instead use mage.util/exit and let mage handle exiting the process"}}}}}}}
:message "Don't use System/exit directly. Instead throw an ex-info with {:babashka/exit <exit-code>} (and optionally :mage/error) let the mage runner handle exiting the process"}}}}}}}

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,13 @@
(ns macros.metabase.queries.api.card-test)
(defmacro with-ordered-items
[collection model-and-name-syms & body]
`(let ~(into []
(comp (partition-all 2)
(mapcat (fn [[model binding]]
[binding model])))
model-and-name-syms)
~collection
~@(->> (partition-all 2 model-and-name-syms)
(map second))
~@body))

View File

@@ -1,13 +0,0 @@
(ns macros.metabase.queries-rest.api.card-test)
(defmacro with-ordered-items
[collection model-and-name-syms & body]
`(let ~(into []
(comp (partition-all 2)
(mapcat (fn [[model binding]]
[binding model])))
model-and-name-syms)
~collection
~@(->> (partition-all 2 model-and-name-syms)
(map second))
~@body))

View File

@@ -0,0 +1,9 @@
(ns macros.metabase.server.statistics-handler-test)
(defmacro with-server
[_ _ [connector chan-start-handle chan-finish-handle chan-done-request] & body]
`(let [~connector nil
~chan-start-handle nil
~chan-finish-handle nil
~chan-done-request nil]
~@body))

View File

@@ -1,38 +0,0 @@
(ns hooks.clojure.core
(:require
[clj-kondo.hooks-api :as hooks]
[clojure.string :as str]))
(defn- all-uppercase? [symb]
(let [s (str symb)]
(= s #_{:clj-kondo/ignore [:discouraged-var]} (str/upper-case s))))
(defn- has-underscores? [symb]
(str/includes? (str symb) "_"))
(defn- check-symbol-is-kebab-case [symbol-node]
(let [symb (hooks/sexpr symbol-node)]
(when (or (all-uppercase? symb)
(has-underscores? symb))
(hooks/reg-finding! (assoc (meta symbol-node)
:message "Use kebab-case for function and variable names; don't use special notation for constants."
:type :metabase/check-def-kebab-case)))))
(defn lint-def* [{:keys [node]}]
(let [[_def & args] (:children node)
name-symbol (some (fn [arg]
(when (and (hooks/token-node? arg)
(symbol? (hooks/sexpr arg)))
arg))
args)]
(check-symbol-is-kebab-case name-symbol)))
(defn lint-def [x]
(lint-def* x)
x)
(comment
(defn x []
(let [node (-> "(def ^:private TYPE->MODEL {\"document\" :model/Document})"
hooks/parse-string)]
(lint-def {:node node}))))

View File

@@ -1,52 +0,0 @@
(ns hooks.clojure.core.def
(:require
[clj-kondo.hooks-api :as hooks]
[clojure.string :as str]))
(defn- uppercase-name?
"Whether the symbol is clearly an uppercase name like `TYPE-CONSTANTS` or `SOME_CONSTANT` or `TYPE->MODEL`."
[s]
(and (re-find #"[A-Z0-9]+(?:[-_]>?[A-Z0-9]+)+" s)
;; ignore stuff like `->SCREAMING_SNAKE_CASE`... this is ok
(not (str/includes? s "SCREAMING_SNAKE_CASE"))))
(defn- has-underscores? [s]
(and (str/includes? s "_")
;; ignore stuff like `->snake_case` and `->SCREAMING_SNAKE_CASE`
(not (str/includes? s "snake_case"))
(not (str/includes? s "SCREAMING_SNAKE_CASE"))))
(defn- check-symbol-is-kebab-case [symbol-node]
(let [symb (hooks/sexpr symbol-node)
s (str symb)]
(cond
(uppercase-name? s)
(hooks/reg-finding!
(assoc (meta symbol-node)
:message "Use lower-case names for functions and variables; don't use special notation for constants. [:metabase/check-def-check-not-all-uppercase]"
:type :metabase/check-def-check-not-uppercase-name))
(has-underscores? s)
(hooks/reg-finding!
(assoc (meta symbol-node)
:message "Use kebab-case names for functions and variables. [:metabase/check-def-no-underscores]"
:type :metabase/check-def-no-underscores)))))
(defn lint-def* [{:keys [node]}]
(let [[_def & args] (:children node)
name-symbol (some (fn [arg]
(when (and (hooks/token-node? arg)
(symbol? (hooks/sexpr arg)))
arg))
args)]
(check-symbol-is-kebab-case name-symbol)))
(defn lint-def [x]
(lint-def* x)
x)
(comment
(defn x []
(let [node (-> "(def ^:private TYPE->MODEL {\"document\" :model/Document})"
hooks/parse-string)]
(lint-def {:node node}))))

View File

@@ -1,7 +0,0 @@
(ns hooks.clojure.core.defmacro
(:require
[hooks.clojure.core.def]))
(defn lint-defmacro [x]
(hooks.clojure.core.def/lint-def* x)
x)

View File

@@ -2,7 +2,6 @@
(:require
[clj-kondo.hooks-api :as hooks]
[clojure.string :as str]
[hooks.clojure.core.def]
[hooks.common]))
(defn- check-arglists [report-node arglists]
@@ -50,5 +49,4 @@
(defn lint-defmulti [x]
(defmulti-check-for-arglists-metadata (:node x))
(hooks.clojure.core.def/lint-def* x)
x)

View File

@@ -2,7 +2,6 @@
(:require
[clj-kondo.hooks-api :as hooks]
[clojure.string :as str]
[hooks.clojure.core.def]
[hooks.common]))
(defn- end-with-exclamation?
@@ -51,11 +50,6 @@
(non-thread-safe-form-should-end-with-exclamation* node config))
{:node node})
(defn lint-defn [x]
(non-thread-safe-form-should-end-with-exclamation x)
(hooks.clojure.core.def/lint-def* x)
x)
(comment
(require '[clj-kondo.core :as clj-kondo])
(def form (str '(defmacro a

View File

@@ -99,48 +99,12 @@
:message "Metabase namespaces should have the form metabase[-enterprise].<module>.* [:metabase/namespace-name]"
:type :metabase/namespace-name)))))))
(defn- ns-form-node->import-node [ns-form-node]
(some (fn [node]
(when (and (hooks/list-node? node)
(let [first-child (first (:children node))]
(and (hooks/keyword-node? first-child)
(= (hooks/sexpr first-child) :import))))
node))
(:children ns-form-node)))
(defn- jsqlparser-import?
"Check if a symbol or string represents a jsqlparser import."
[s]
(str/starts-with? (str s) "net.sf.jsqlparser"))
(defn- lint-jsqlparser-imports [ns-form-node]
(when-let [import-node (ns-form-node->import-node ns-form-node)]
(doseq [node (rest (:children import-node))
:when (not (contains? (hooks.common/ignored-linters node) :metabase/no-jsqlparser-imports))]
(cond
;; Handle (net.sf.jsqlparser.foo Bar Baz) form
(hooks/list-node? node)
(let [package-node (first (:children node))
package-name (when package-node (str (hooks/sexpr package-node)))]
(when (jsqlparser-import? package-name)
(hooks/reg-finding! (assoc (meta node)
:message "Don't import net.sf.jsqlparser classes directly. This code should be agnostic to the parsing library. [:metabase/no-jsqlparser-imports]"
:type :metabase/no-jsqlparser-imports))))
;; Handle net.sf.jsqlparser.foo.Bar form (single class import)
(hooks/token-node? node)
(when (jsqlparser-import? (hooks/sexpr node))
(hooks/reg-finding! (assoc (meta node)
:message "Don't import net.sf.jsqlparser classes directly. This code should be agnostic to the parsing library. [:metabase/no-jsqlparser-imports]"
:type :metabase/no-jsqlparser-imports)))))))
(defn lint-ns [x]
(doto (:node x)
lint-require-shapes
lint-requires-on-new-lines
(lint-modules (modules/config x))
lint-namespace-name
lint-jsqlparser-imports)
lint-namespace-name)
x)
(comment

View File

@@ -2,8 +2,7 @@
(:require
[clj-kondo.hooks-api :as hooks]
[clojure.string :as str]
[hooks.common]
[hooks.common.modules :as modules]))
[hooks.common]))
(defn- warn-about-disallowed-parallel-forms [form config]
(letfn [(error! [form message]
@@ -103,39 +102,7 @@
(:children node))))]
(walk node)))
(defn- test-namespace-lives-outside-of-module-system? [ns-symb]
(some (fn [prefix]
(str/starts-with? (name ns-symb) prefix))
["build-drivers."
"build."
"i18n." ; bin/i18n
"lint-migrations-file-test"
"main-test" ; bin/release-list
"metabase.deps-edn-test"
"metabase.driver."
"metabase.test."
"metabase.test.data."
"metabuild-common."])) ; utils for build scripts
(let [warned-namespaces (atom #{})]
(defn deftest-check-in-valid-module
"Check whether a `deftest` lives in a known module -- important so our system to only run a subset of tests based on
which modules have changes picks them up correctly. (DEV-1125)"
[{:keys [node], ns-symb :ns, :as input}]
(when (and ns-symb
(not (test-namespace-lives-outside-of-module-system? ns-symb))
;; only warn once per namespace
(not (contains? @warned-namespaces ns-symb)))
(swap! warned-namespaces conj ns-symb)
(let [known-modules (set (keys (:metabase/modules (modules/config input))))
current-module (modules/module ns-symb)]
(when (or (not current-module)
(not (contains? known-modules current-module)))
(hooks/reg-finding! (assoc (meta node)
:message (format "All tests must live in a known module; %s is not a known module" (pr-str current-module))
:type :metabase/tests-must-live-in-known-modules)))))))
(defn deftest [{:keys [node cljc lang config], :as input}]
(defn deftest [{:keys [node cljc lang config]}]
;; run [[deftest-check-parallel]] only once... if this is a `.cljc` file only run it for the `:clj` analysis, no point
;; in running it twice.
(when (or (not cljc)
@@ -143,8 +110,7 @@
(deftest-check-parallel node (get-in config [:linters :metabase/validate-deftest])))
(deftest-check-not-horrifically-long node (get-in config [:linters :metabase/i-like-making-cams-eyes-bleed-with-horrifically-long-tests]))
(deftest-check-no-driver-keywords node (get-in config [:linters :metabase/disallow-hardcoded-driver-names-in-tests]))
(deftest-check-in-valid-module input)
input)
{:node node})
;;; this is a hacky way to determine whether these namespaces are required in the `ns` form or not... basically `:ns`
;;; will come back as `nil` if they are not.

View File

@@ -1,6 +1,4 @@
(ns hooks.common.modules
(:require
[clojure.string :as str]))
(ns hooks.common.modules)
(defn ignored-namespace? [config ns-symb]
(some
@@ -19,19 +17,14 @@
(module 'metabase-enterprise.whatever.core) => enterprise/whatever"
[ns-symb]
{:pre [(simple-symbol? ns-symb)]}
;; treat something like `metabase.driver-test` (for a module that hasn't fully been updated to use `.core`
;; namespaces) as being in the `driver` module
(let [ns-symb (if (str/ends-with? (name ns-symb) "-test")
(symbol (str/replace (name ns-symb) #"-test$" ""))
ns-symb)]
(or (some->> (re-find #"^metabase-enterprise\.([^.]+)" (str ns-symb))
second
(symbol "enterprise"))
(some-> (re-find #"^metabase\.([^.]+)" (str ns-symb))
second
symbol))))
(or (some->> (re-find #"^metabase-enterprise\.([^.]+)" (str ns-symb))
second
(symbol "enterprise"))
(some-> (re-find #"^metabase\.([^.]+)" (str ns-symb))
second
symbol)))
(defn- module-api-namespaces
(defn module-api-namespaces
"Set of API namespace symbols for a given module. `:any` means you can use anything, there are no API namespaces for
this module (yet). If unspecified, the default is just the `<module>.core` namespace."
[config module]
@@ -51,12 +44,6 @@
(symbol (str ns-prefix ".core"))
(symbol (str ns-prefix ".init"))}))))
(defn- module-friends
[config module]
"Set of modules that are `:friends` of `module`, i.e. allowed to use *any* namespace from the module, not just the
designated [[module-api-namespaces]]."
(set (get-in config [:metabase/modules module :friends])))
(defn allowed-modules
"Set of namespace symbols that `module` is allowed to use. `:any` means it's allowed to use anything."
[config module]
@@ -67,22 +54,11 @@
(or (= allowed-modules :any)
(contains? (set allowed-modules) required-module))))
(defn- allowed-module-namespace? [config current-module ns-symb]
(defn allowed-module-namespace? [config ns-symb]
(let [module (module ns-symb)
module-api-namespaces (module-api-namespaces config module)
module-friends (module-friends config module)]
module-api-namespaces (module-api-namespaces config module)]
(or (empty? module-api-namespaces)
(contains? module-api-namespaces ns-symb)
(contains? module-friends current-module))))
(defn- rest-module? [module]
(str/ends-with? module "-rest"))
(defn- routes-module? [module]
(str/ends-with? module "-routes"))
(defn- core-module? [module]
(str/ends-with? module "core"))
(contains? module-api-namespaces ns-symb))))
(defn usage-error
"Find usage errors when a `required-namespace` is required in the `current-module`. Returns a string describing the
@@ -98,19 +74,8 @@
current-module
current-module)
(not (allowed-module-namespace? config current-module required-namespace))
(not (allowed-module-namespace? config required-namespace))
(format "Namespace %s is not an allowed external API namespace for the %s module. [:metabase/modules %s :api]"
required-namespace
required-module
required-module)
;; (for now) rest modules are allowed to use one another; `routes` is ok because it collects routes together
;; and `core` is ok because [[metabase.core.init]] might need to init some of the `-routes` modules'
;; namespaces
(and (not ((some-fn rest-module? routes-module? core-module?) current-module))
(rest-module? required-module))
(format "Do not use -rest modules (%s) in non-rest modules (%s) -- move things from %s to %s if needed"
required-module
current-module
required-module
(symbol (str/replace required-module #"-rest$" "")))))))
required-module)))))

View File

@@ -90,10 +90,6 @@
:else
(recur (conj bindings x) (conj schemas nil) (list* y z more)))))]
(validate-route! route)
(when-not result-schema
(api/reg-finding! (assoc (meta node)
:message "All REST API endpoints should have a detailed response schema"
:type :metabase/validate-defendpoint-has-response-schema)))
(let [[_route-param-schema query-param-schema _body-param-schema] schemas]
(when query-param-schema
(validate-query-params! query-param-schema)))

View File

@@ -1,16 +0,0 @@
(ns hooks.metabase.query-processor.expressions-test
(:require
[clj-kondo.hooks-api :as hooks]
[hooks.metabase.test.data :as data]))
(defn calculate-bird-scarcity
[{{[_ formula filter-clause] :children} :node}]
(let [formula* (data/replace-$id-special-tokens formula)
filter-clause* (or (some-> filter-clause data/replace-$id-special-tokens)
(hooks/token-node 'nil))
node* (hooks/list-node
(list
(hooks/token-node 'do)
formula*
filter-clause*))]
{:node node*}))

View File

@@ -0,0 +1,16 @@
(ns hooks.metabase.query-processor-test.expressions-test
(:require
[clj-kondo.hooks-api :as hooks]
[hooks.metabase.test.data :as data]))
(defn calculate-bird-scarcity
[{{[_ formula filter-clause] :children} :node}]
(let [formula* (data/replace-$id-special-tokens formula)
filter-clause* (or (some-> filter-clause data/replace-$id-special-tokens)
(hooks/token-node 'nil))
node* (hooks/list-node
(list
(hooks/token-node 'do)
formula*
filter-clause*))]
{:node node*}))

View File

@@ -55,6 +55,7 @@
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
@@ -96,6 +97,7 @@
metabot-get-prompt-templates-url
metabot-prompt-generator-token-limit
multi-setting-read-only
notebook-native-preview-shown
notebook-native-preview-sidebar-width
notification-link-base-url
num-metabot-choices
@@ -117,10 +119,10 @@
reset-token-ttl-hours
retired-setting
retry-initial-interval
retry-max-retries
retry-max-attempts
retry-max-interval-millis
retry-multiplier
retry-jitter-factor
retry-randomization-factor
saml-application-name
saml-attribute-email
saml-attribute-firstname

View File

@@ -43,17 +43,6 @@
'{:metabase/modules {api {:uses #{search}}
search {:api #{metabase.search.core}}}}))))
(deftest ^:parallel module-checker-friends-test
(is (= []
(lint-modules
'(ns metabase.search-rest.api
(:require
[metabase.search.config :as search.config]
[metabase.search.core :as search]))
'{:metabase/modules {search {:api #{metabase.search.core}
:friends #{search-rest}}
search-rest {:uses #{search}}}}))))
(deftest ^:parallel module-checker-api-namespaces-ignore-test
(is (empty? (lint-modules
'(ns metabase.api.search

View File

@@ -43,6 +43,9 @@
with-meta [[:default]]
;; library stuff. Libraries that specify their own `:style/indent` specs are included in the section after this
;; since it is generated automatically.
clojure.core.logic/fresh [[:block 1]]
clojure.core.logic/matcha [[:block 1]]
clojure.core.logic/run [[:block 2]]
clojure.core.match/match [[:block 1]]
clojure.test.check.properties/for-all [[:block 1]]
clojure.test.check.properties/for-all* [[:block 1]]
@@ -90,7 +93,6 @@
metabase-enterprise.test/with-gtaps! [[:inner 0]]
metabase-enterprise.test/with-gtaps-for-user! [[:inner 0]]
metabase-enterprise.test/with-user-attributes [[:block 2]]
metabase-enterprise.workspaces.models.workspace-log/track! [[:block 2]]
metabase.actions.actions-test/with-actions-test-data-and-actions-permissively-enabled [[:block 0]]
metabase.actions.test-util/with-actions [[:block 1]]
metabase.actions.test-util/with-actions-disabled [[:block 0]]
@@ -120,10 +122,7 @@
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]]
@@ -137,7 +136,6 @@
metabase.driver.sql-jdbc.execute.diagnostic/capturing-diagnostic-info [[:block 1]]
metabase.driver.sql.query-processor/apply-top-level-clause [[:default]]
metabase.driver/with-driver [[:block 1]]
metabase.driver/with-swapped-connection-details [[:block 2]]
metabase.embedding-rest.api.embed-test/do-response-formats [[:block 1]]
metabase.embedding-rest.api.embed-test/with-embedding-enabled-and-new-secret-key [[:block 0]]
metabase.embedding-rest.api.embed-test/with-embedding-enabled-and-temp-card-referencing [[:block 3]]
@@ -159,9 +157,8 @@
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/match-lite-recursive [[: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]]
@@ -179,23 +176,21 @@
metabase.pulse.dashboard-subscription-test/with-link-card-fixture-for-dashboard [[:block 2]]
metabase.pulse.models.pulse-test/with-dashboard-subscription-in-collection [[:block 1]]
metabase.pulse.models.pulse-test/with-pulse-in-collection [[:inner 0]]
metabase.queries-rest.api.card-test/with-cards-in-readable-collection [[:block 1]]
metabase.queries-rest.api.card-test/with-cards-in-writeable-collection [[:block 1]]
metabase.queries-rest.api.card-test/with-ordered-items [[:inner 0]]
metabase.queries-rest.api.card-test/with-temp-native-card [[:block 1]]
metabase.queries-rest.api.card-test/with-temp-native-card-with-params [[:block 1]]
metabase.queries.api.card-test/with-cards-in-readable-collection [[:block 1]]
metabase.queries.api.card-test/with-cards-in-writeable-collection [[:block 1]]
metabase.queries.api.card-test/with-ordered-items [[:inner 0]]
metabase.queries.api.card-test/with-temp-native-card [[:block 1]]
metabase.queries.api.card-test/with-temp-native-card-with-params [[:block 1]]
metabase.query-processor-test.pipeline-queries-test/pmbql-query [[:block 1]]
metabase.query-processor-test.pipeline-queries-test/run-pmbql-query [[:block 1]]
metabase.query-processor.debug/debug> [[:block 0]]
metabase.query-processor.error-type/deferror [[:block 1]]
metabase.query-processor.middleware.cache-backend.interface/with-cached-results [[:block 4]]
metabase.query-processor.middleware.process-userland-query-test/with-query-execution [[:block 1]]
metabase.query-processor.pipeline-queries-test/pmbql-query [[:block 1]]
metabase.query-processor.pipeline-queries-test/run-pmbql-query [[:block 1]]
metabase.query-processor.store/cached [[:block 1]]
metabase.query-processor.store/with-metadata-provider [[:inner 0]]
metabase.query-processor.streaming/streaming-response [[:block 1]]
metabase.query-processor.test-util/format-rows-by [[:default]]
metabase.query-processor.timeseries-test.util/test-timeseries-drivers [[:block 0]]
metabase.query-processor.timeseries-test/run-mbql-query [[:inner 0]]
metabase.request.core/as-admin [[:block 0]]
metabase.request.core/with-current-request [[:inner 0]]
metabase.request.core/with-current-user [[:inner 0]]
@@ -283,9 +278,12 @@
metabase.test/with-temp-vals-in-db [[:block 3]]
metabase.test/with-test-user [[:inner 0]]
metabase.test/with-user-in-groups [[:inner 0]]
metabase.timeseries-query-processor-test.util/test-timeseries-drivers [[:block 0]]
metabase.timeseries-query-processor-test/run-mbql-query [[:inner 0]]
metabase.upload.impl-test/with-upload-table! [[:inner 0]]
metabase.upload.types/with-currency [[:default]]
metabase.upload.types/with-parens [[:default]]
metabase.util/for-map [[:block 1]]
metabase.util.async/cancelable-thread [[:block 0]]
metabase.util.date-2/with-time-zone-same-instant [[:default]]
metabase.util.encryption-test/with-secret-key [[:block 1]]
@@ -306,7 +304,6 @@
metabase.util.queue-test/await-test-while [[:block 1]]
metabase.util/auto-retry [[:block 1]]
metabase.util/case-enum [[:block 1]]
metabase.util/for-map [[:block 1]]
metabase.util/ignore-exceptions [[:block 0]]
metabase.util/or-with [[:block 1]]
metabase.util/profile [[:block 1]]

View File

@@ -1,35 +1,23 @@
Brite
afterAll
ags
altho
Brite
cancelation
cancelations
caracteres
copyin
datas
edn
eduction
hasTable
hasTables
horizontalX
horizontalY
juxt
methodd
nd
notIn
optionA
ot
pass-thru
requestor
re-use
re-used
ser
serieses
sortings
titel
trs
trsn
tru
trun
unparseable
withs

View File

@@ -1,24 +1,3 @@
[codespell]
skip =
*.csv,
*.edn,
*.geojson,
*.json,
*.tsv,
*.unit.spec.*,
*.xml,
docs/developers-guide/drivers/driver-tests.md,
docs/developers-guide/e2e-tests.md,
docs/questions/query-builder/expressions-list.md,
docs/questions/visualizations/country-codes.md,
e2e/test/scenarios/admin/i18n/content-translation/constants.ts,
frontend/src/cljs,
frontend/src/metabase/querying/expressions/tokenizer/lezer.js,
frontend/src/metabase/query_builder/components/NativeQueryEditor/CodeMirrorEditor/language.ts,
frontend/src/metabase/static-viz/constants/char-sizes.ts,
frontend/src/metabase/visualizations/lib/mapping_codes.ts,
frontend/*/mocks/*,
modules/drivers/mongo/src/metabase/driver/mongo/operators.clj,
src/metabase/analyze/classifiers/name.clj,
src/metabase/util/currency.cljc,
skip = *.csv,*.edn,*.geojson,*.json,*.tsv,*.xml,docs/developers-guide/drivers/driver-tests.md,docs/developers-guide/e2e-tests.md,docs/questions/query-builder/expressions-list.md,docs/questions/visualizations/country-codes.md,e2e/test/scenarios/admin/i18n/content-translation/constants.ts,frontend/src/cljs,frontend/src/metabase/querying/expressions/tokenizer/lezer.js,frontend/src/metabase/static-viz/constants/char-sizes.ts,frontend/src/metabase/visualizations/lib/mapping_codes.ts,modules/drivers/mongo/src/metabase/driver/mongo/operators.clj
ignore-words = .codespellignore

View File

@@ -1,11 +1,10 @@
FROM mcr.microsoft.com/vscode/devcontainers/java:21
# Set up nodesource, install node, bun, fontconfig for static viz, rlwrap for dev ergonomics
# Set up nodesource, install node, yarn, fontconfig for static viz, rlwrap for dev ergonomics
RUN ( curl -fsSL https://deb.nodesource.com/setup_22.x | bash ) \
&& export DEBIAN_FRONTEND=noninteractive \
&& apt-get update && apt-get -y install --no-install-recommends nodejs rlwrap fontconfig \
&& npm install -g bun
&& apt-get update && apt-get -y install --no-install-recommends nodejs yarn rlwrap fontconfig
# install Clojure
RUN curl https://download.clojure.org/install/linux-install-1.12.0.1488.sh | bash

View File

@@ -61,7 +61,6 @@
(eval . (put-clojure-indent 'u/prog1 1))
(eval . (put-clojure-indent 'u/select-keys-when '(:form)))
(eval . (put-clojure-indent 'with-meta '(:form)))
(eval . (put-clojure-indent 'ws.log/track! 2))
;; these ones have to be done with `define-clojure-indent' for now because of upstream bug
;; https://github.com/clojure-emacs/clojure-mode/issues/600 once that's resolved we should use `put-clojure-indent'
;; instead. Please don't add new entries unless they don't work with `put-clojure-indent'

View File

@@ -1,163 +0,0 @@
# ==============================================================================
# 0. METABASE DEVELOPMENT ENVIRONMENT
# ==============================================================================
#
# Central configuration for local Metabase development.
# Copy to .env and uncomment/modify values as needed.
#
# Usage:
# op inject --in-file .env.example --out-file .env (populate using CLI)
# cp .env.example .env (populate manually)
#
# Hint: Auto-load environment variables with mise or direnv:
# mise - mise.toml: [settings] env_file = ".env" (https://mise.jdx.dev)
# direnv - Create .envrc with: dotenv .env (https://direnv.net)
#
# Full Metabase environment configuration reference:
# https://www.metabase.com/docs/latest/configuring-metabase/environment-variables
#
# ⚠️ Make sure the same var is not exported multiple times in different contexts.
# ==============================================================================
# 1. GLOBAL CONTEXT
# ==============================================================================
#
# Shared credentials and tokens used across multiple dev workflows.
# GitHub - For gh CLI, API access, and CI integrations
# https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens
# GITHUB_TOKEN=your-PAT
# AWS - For S3, Athena, Redshift, and other AWS services
# https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html
# AWS_ACCESS_KEY_ID=your-access-key
# AWS_SECRET_ACCESS_KEY=your-secret-key
# AWS_REGION=us-east-1
# Docker Hub - Avoid rate limits when pulling images
# https://docs.docker.com/security/for-developers/access-tokens/
# DOCKER_USERNAME=your-username
# DOCKER_PASSWORD=your-access-token
# ==============================================================================
# 2. LOCAL METABASE DEVELOPMENT
# ==============================================================================
#
# Settings for running Metabase locally. Most have sensible defaults—only
# configure what you need.
# 2.1 Application Database
# ------------------------------------------------------------------------------
# Where Metabase stores its own data (dashboards, users, settings, etc.).
# https://www.metabase.com/docs/latest/installation-and-operation/configuring-application-database
# Option A: Individual connection parameters
# MB_DB_TYPE="postgres"
# MB_DB_HOST="localhost"
# MB_DB_PORT=5432
# MB_DB_DBNAME="metabase"
# MB_DB_USER="<username>"
# MB_DB_PASS="<password>"
# Option B: Single connection URI (overrides individual params)
# MB_DB_CONNECTION_URI="jdbc:postgresql://localhost:5432/metabase?user=<username>&password=<password>"
# 2.2 Server & Integrations
# ------------------------------------------------------------------------------
# https://www.metabase.com/docs/latest/configuring-metabase/customizing-jetty-webserver
# Jetty server (defaults: localhost:3000)
# MB_JETTY_HOST="localhost"
# MB_JETTY_PORT=3000
# SMTP for testing email notifications (use with maildev/maildev container)
MB_EMAIL_SMTP_HOST="localhost"
MB_EMAIL_SMTP_PORT=1025
MB_EMAIL_SMTP_USERNAME="admin"
MB_EMAIL_SMTP_PASSWORD="admin"
# 2.3 Feature Flags & Tokens
# ------------------------------------------------------------------------------
# Use staging server for license validation (avoids hitting production)
METASTORE_DEV_SERVER_URL="https://token-check.staging.metabase.com"
# Metabase license token
# Uncomment this line only when running local dev and NEVER when running Cypress
# Tests need to start from zero, and set different tokens programmatically (on-demand)
# MB_PREMIUM_EMBEDDING_TOKEN="op://Shared/Metabase License Dev Tokens/Licenses/MB_ALL_FEATURES_TOKEN"
# Skip embedding SDK build for faster frontend compilation
# SKIP_EMBEDDING_SDK="true"
# ==============================================================================
# 3. E2E / CYPRESS TESTING
# ==============================================================================
#
# Configuration for running Cypress end-to-end tests locally.
#
# The whole environment is already orchestrated via deps.edn using :e2e alias.
# Treat it as a source of truth for JVM flags and ENVs whether you decide to run
# backend on your own, or using `bun run test-cypress` (local test runner).
#
# You don't _have to_ export any of these vars (except for the tokens below).
# Things will still work.
# 3.1. E2E Application Database (H2 required)
# ------------------------------------------------------------------------------
# Whether to run the enterprise or the open-source Metabase version
MB_EDITION="ee"
# Which file to use for the ephemeral testing application database?
# MB_DB_FILE="./e2e/tmp/cypress-test.db"
# Where does Cypress go to look for the application? (defaults: localhost:4000)
# MB_JETTY_HOST="localhost"
# MB_JETTY_PORT=4000
# Frontend dev server port (for hot reload). Must match the port used by `yarn build-hot`.
# MB_FRONTEND_DEV_PORT=8080
# 3.2 E2E Local Runner Setup
# ------------------------------------------------------------------------------
# Whether to run true e2e test suite, or SDK component tests (e2e | component)
CYPRESS_TESTING_TYPE="e2e"
# Whether to start Cypress in an interactive (open) mode with full GUI
CYPRESS_GUI="true"
# Whether to generate application database dumps (colloquially "snapshots")
# Keep this enabled unless you're experimenting, or know what you're doing.
GENERATE_SNAPSHOTS="true"
# E2E tests run the backend from source by default.
# To run tests against a JAR instead, set the JAR_PATH variable.
#
# Hint: build the JAR with ./bin/build.sh
# JAR_PATH="target/uberjar/metabase.jar"
# 3.3 E2E Tokens
# ------------------------------------------------------------------------------
#
# These make sense for Metabase core devs only
# op inject --in-file .env.example --out-file .env
CYPRESS_MB_ALL_FEATURES_TOKEN="op://Shared/Metabase License Dev Tokens/Licenses/MB_ALL_FEATURES_TOKEN"
CYPRESS_MB_STARTER_CLOUD_TOKEN="op://Shared/Metabase License Dev Tokens/Licenses/MB_STARTER_CLOUD_TOKEN"
CYPRESS_MB_PRO_CLOUD_TOKEN="op://Shared/Metabase License Dev Tokens/Licenses/MB_PRO_CLOUD_TOKEN"
CYPRESS_MB_PRO_SELF_HOSTED_TOKEN="op://Shared/Metabase License Dev Tokens/Licenses/MB_PRO_SELF_HOSTED_TOKEN"
# 3.4 E2E Misc.
# ------------------------------------------------------------------------------
# To make Node.js match the instance tz
TZ="US/Pacific"
# Export this environment variable when running tests on Apple Silicon.
# DO NOT USE Docker Desktop. Use Orbstack instead. Metabase will fail to connect
# to the dockerized Mongo QA database otherwise.
EXPERIMENTAL_DOCKER_DESKTOP_FORCE_QEMU=1

5
.eslintignore Normal file
View File

@@ -0,0 +1,5 @@
frontend/src/cljs
frontend/src/cljs_release
e2e/support/cypress_sample_database.js
e2e/support/cypress_sample_instance_data.js
e2e/embedding-sdk-host-apps

356
.eslintrc.js Normal file
View File

@@ -0,0 +1,356 @@
/* eslint-disable import/no-commonjs */
/* eslint-disable no-undef */
const path = require("path");
// `postcss-modules` lints css modules class names, but it currently crashes
// eslint on vscode. If you use webstorm or want to run the lint for the cli, you
// can use this flag to enable it. This is set to true in CI
const shouldLintCssModules =
process.env.LINT_CSS_MODULES === "true" || process.env.CI;
const plugins = ["react", "no-only-tests", "ttag", "i18next"];
if (shouldLintCssModules) {
plugins.push("postcss-modules");
}
module.exports = {
ignorePatterns: ["!.storybook"],
rules: {
strict: [2, "never"],
"no-undef": 2,
"no-var": 1,
"no-unused-vars": [
"error",
{
vars: "all",
args: "none",
varsIgnorePattern: "^_.+$",
ignoreRestSiblings: true,
},
],
"no-empty": [1, { allowEmptyCatch: true }],
// Note: adding this rule to a eslint config file in a subfolder will remove
// *not* carry over the restricted imports from parent folders, you will
// need to copy them over
"no-restricted-imports": [
"error",
{
paths: [
{
name: "moment",
message: "Moment is deprecated, please use dayjs",
},
{
name: "moment-timezone",
message: "Moment is deprecated, please use dayjs",
},
],
},
],
curly: [1, "all"],
eqeqeq: [1, "smart"],
"import/no-duplicates": ["warn", { considerQueryString: true }],
"import/no-default-export": 2,
"import/no-named-as-default": 0,
"import/no-commonjs": 1,
"import/order": [
"error",
{
"newlines-between": "always",
alphabetize: {
order: "asc",
orderImportKind: "asc",
caseInsensitive: false,
},
groups: [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
],
warnOnUnassignedImports: false,
},
],
"sort-imports": [
"error",
{
// allows this rule to work with import/order
ignoreDeclarationSort: true,
},
],
"no-console": [2, { allow: ["warn", "error", "errorBuffer"] }],
"react/no-is-mounted": 2,
"react/prefer-es6-class": 2,
"react/display-name": 1,
"react/prop-types": 2,
"react/no-did-mount-set-state": 0,
"react/no-did-update-set-state": 0,
"react/no-find-dom-node": 0,
"react/no-children-prop": 2,
"react/no-string-refs": 2,
"react/no-unescaped-entities": 2,
"react/jsx-no-target-blank": 2,
"react/jsx-key": 2,
"react/forbid-component-props": [2, { forbid: ["sx"] }],
"react-hooks/exhaustive-deps": ["warn"],
"prefer-const": [1, { destructuring: "all" }],
"no-restricted-globals": ["error", "close"],
"no-useless-escape": 0,
"no-only-tests/no-only-tests": [
"error",
{
block: [
"describe",
"it",
"context",
"test",
"tape",
"fixture",
"serial",
"Feature",
"Scenario",
"Given",
"And",
"When",
"Then",
],
},
],
complexity: ["error", { max: 55 }],
...(shouldLintCssModules
? {
"postcss-modules/no-undef-class": "error",
}
: {}),
},
globals: {
before: true,
cy: true,
Cypress: true,
},
env: {
browser: true,
es2020: true,
commonjs: true,
jest: true,
"jest/globals": true,
},
parser: "babel-eslint",
plugins,
extends: [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript",
"plugin:depend/recommended",
"plugin:storybook/recommended",
"plugin:i18next/recommended",
],
settings: {
"import/internal-regex": "^metabase/|^metabase-lib/",
"import/resolver": {
webpack: {
config: path.resolve(__dirname, "./rspack.main.config.js"),
typescript: true,
},
},
"import/ignore": ["\\.css$"],
react: {
version: "detect",
},
"postcss-modules": {
baseDir: "./frontend/src",
},
},
parserOptions: {
ecmaFeatures: {
legacyDecorators: true,
},
},
overrides: [
{
files: ["*.js", "*.jsx", "*.ts", "*.tsx"],
rules: {
"jtag-missing-key": "error",
"no-unconditional-metabase-links-render": "error",
"no-color-literals": "error",
"no-literal-metabase-strings": "error",
"no-oss-reinitialize-import": "error",
"depend/ban-dependencies": [
"error",
{
allowed: [
"underscore",
"moment",
"lodash.orderby",
"lodash.debounce",
],
},
],
},
},
{
files: [
"*.unit.spec.*",
"frontend/src/metabase/admin/**/*",
"frontend/src/metabase/setup/**/*",
"enterprise/frontend/src/metabase-enterprise/whitelabel/**/*",
"enterprise/frontend/src/metabase-enterprise/embedding/**/*",
"frontend/lint/**/*",
"*.stories.*",
"**/.storybook/*",
"stories-data.*",
"e2e/**/*",
"**/tests/*",
"release/**/*",
"rspack.config.js",
"rspack.main.config.js",
"rspack.embedding-sdk-package.config.js",
"rspack.embedding-sdk-bundle.config.js",
],
rules: {
"no-color-literals": "off",
"no-unconditional-metabase-links-render": "off",
"no-literal-metabase-strings": "off",
},
},
{
files: [
"*.unit.spec.*",
"frontend/lint/**/*",
"*.stories.*",
"stories-data.*",
"e2e/**/*",
"**/tests/*",
"release/**/*",
"rspack.main.config.js",
],
rules: {
"i18next/no-literal-string": "off",
},
},
{
extends: ["plugin:@typescript-eslint/recommended"],
files: ["*.ts", "*.tsx"],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
rules: {
"prefer-rest-params": "off",
"react/prop-types": "off", // TypeScript handles prop validation
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-this-alias": "off",
"@typescript-eslint/consistent-type-imports": [
"error",
{
fixStyle: "inline-type-imports",
},
],
"@typescript-eslint/no-import-type-side-effects": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_.+$",
varsIgnorePattern: "^_.+$",
ignoreRestSiblings: true,
destructuredArrayIgnorePattern: "^_.+$",
},
],
// This was introduced in 6.0.0
"@typescript-eslint/no-unsafe-declaration-merging": "off",
},
},
{
extends: [
"plugin:jest/recommended",
"plugin:jest-dom/recommended",
"plugin:testing-library/react",
"plugin:jest-formatting/recommended",
],
plugins: ["jest", "jest-dom", "testing-library", "jest-formatting"],
files: [
"*.unit.spec.ts",
"*.unit.spec.tsx",
"*.unit.spec.js",
"*.unit.spec.jsx",
],
rules: {
"jest/valid-title": ["error", { ignoreTypeOfDescribeName: true }],
"jest/expect-expect": [
"error",
{
assertFunctionNames: ["expect*", "assert*"],
additionalTestBlockFunctions: [],
},
],
},
},
{
// Enable jest formatting for cypress tests too, the plugin logic just works
extends: ["plugin:jest-formatting/recommended"],
files: ["*.cy.spec.ts", "*.cy.spec.js"],
},
{
files: ["frontend/src/**/*"],
rules: {
"no-restricted-syntax": [
"error",
{
selector: "Literal[value=/mb-base-color-/]",
message:
"You may not use base colors in the application, use semantic colors instead. (see colors.module.css)",
},
],
},
},
{
files: ["frontend/src/metabase/**/*"],
rules: {
"no-restricted-syntax": [
"error",
{
selector: "Literal[value=/mb-base-color-/]",
message:
"You may not use base colors in the application, use semantic colors instead. (see colors.module.css)",
},
],
},
},
{
files: ["frontend/src/metabase/query_builder/**/*"],
rules: {
"import/no-cycle": "error",
},
},
{
files: ["docs/**/snippets/**/*.{ts,tsx,js,jsx}"],
rules: {
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-var-requires": "off",
"import/no-commonjs": "off",
"import/no-default-export": "off",
"import/order": "off",
"import/no-unresolved": "off",
"no-color-literals": "off",
},
},
{
files: ["frontend/build/**/*.js"],
rules: {
"import/no-commonjs": "off",
},
},
{
files: ["**/*.stories.tsx", "**/preview.tsx"],
rules: {
"import/no-default-export": "off",
},
},
],
};

9
.gitattributes vendored
View File

@@ -1,9 +0,0 @@
# 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/*_update_migrations.yaml merge=yaml-migrations

View File

@@ -1,5 +0,0 @@
# execute this in your shell to enable custom merge driver:
# echo '\n[include]\npath=../.gitconfig' >> .git/config
[merge "yaml-migrations"]
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,30 +1,266 @@
# 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
# @metabase/devexp
.github @metabase/devexp
#
# Backend modules <=> teams that own them
#
# @metabase/cloud-ops
src/metabase/analytics/prometheus.clj @metabase/cloud-ops
# 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

23
.github/ISSUE_TEMPLATE/epic.md vendored Normal file
View File

@@ -0,0 +1,23 @@
---
name: Epic (Internal Use)
about: This issue is used to track a feature/project that may span several days
to weeks to implement. This is used by the core Metabase team to project plan
and manage work.
title: "[Epic] Title"
labels: ".Epic"
---
### Links
- [product doc](LINK_TO_PRODUCT_DOC)
- [technical design doc](LINK_TO_TECHNICAL_DESIGN_DOC)
- [testing plan](LINK_TO_TESTING_PLAN)
- feature branch: `BRANCH_NAME`
- issue links: _related issues, if any_
### Implementation Plan
```[tasklist]
#### Milestone 1
- [ ] _Add tasks here_
```

View File

@@ -7,13 +7,16 @@ assignees: ''
---
**What problem will this feature request solve?**
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like.**
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**How does this feature request impact you?**
For example, is it a minor inconvenience, or does it block important work?
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional information**
**How important is this feature to you?**
**Additional context**
Add any other context or screenshots about the feature request here.

14
.github/ISSUE_TEMPLATE/task.md vendored Normal file
View File

@@ -0,0 +1,14 @@
---
name: Task (Internal Use)
about: This issue is used to track a smaller feature/task that does not require
being broken down to milestones. This is used by the core Metabase team to
project plan and manage work.
labels: ".Task"
---
**Context**
- issue links: _related issues if any_

View File

@@ -25,4 +25,3 @@ _Upload a demo video or before/after screenshots if sensible or remove the secti
### Checklist
- [ ] Tests have been added/updated to cover changes in this PR
- [ ] If adding new Loki tests: they pass [stress testing](https://github.com/metabase/metabase/actions/workflows/loki-stress-test-flake-fix.yml)

View File

@@ -5,7 +5,7 @@ PENDING CI / PENDING REVIEW / READY _(choose one and update accordingly)_
- Reproduces #XXXXX
### How to test this manually?
- `bun run test-cypress`
- `yarn test-cypress`
- `relative/path/to/the/file`
> _(For still unfixed bug)_
- Replace `it.skip()` with `it.only()` to run the test in isolation

View File

@@ -2,16 +2,9 @@ name: Fetch artifact
description: Fetches a specific artifact and downloads it to the current directory
inputs:
commit:
description: Commit SHA for the artifact
name:
description: The name of the artifact to fetch
required: true
edition:
description: Metabase edition (ee or oss)
required: true
release:
description: Whether this is a release artifact (uses metabase-release- prefix)
required: false
default: "false"
output-name:
description: Optional custom name for the output jar file (defaults to metabase.jar)
required: false
@@ -32,44 +25,25 @@ runs:
script: | # js
const fs = require('fs');
const isRelease = "${{ inputs.release }}" === "true";
const prefix = isRelease ? "metabase-release" : "metabase";
const artifactName = `${prefix}-${{ inputs.edition }}-${{ inputs.commit }}-uberjar`;
console.log(`Looking for artifact: ${artifactName}`);
const artifacts = await github.rest.actions.listArtifactsForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
name: artifactName,
name: "${{ inputs.name }}",
per_page: 1,
});
if (!artifacts.data?.artifacts?.[0]?.id) {
core.setFailed(`No artifacts found for ${artifactName}`);
return;
throw new Error(`No artifacts found for ${{ inputs.name }}`);
}
const artifact_id = artifacts.data.artifacts[0].id;
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));
}
}
const download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: artifact_id,
archive_format: 'zip',
});
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/mb.zip`, Buffer.from(download.data));

View File

@@ -7,15 +7,9 @@ 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 }}
@@ -37,9 +31,7 @@ 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,19 +0,0 @@
name: Maximize disk space
description: Removes unused stuff from GH runner
runs:
using: "composite"
steps:
- name: Maximize build space
shell: bash
run: |
echo "-- Dotnet"
sudo rm -rf /usr/share/dotnet
echo "-- Android"
sudo rm -rf /usr/local/lib/android
echo "-- Haskell"
sudo rm -rf /opt/ghc
echo "-- CodeQL"
sudo rm -rf /opt/hostedtoolcache/CodeQL
df -h

View File

@@ -1,13 +1,14 @@
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"
@@ -16,33 +17,18 @@ 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: |
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
curl -O https://download.clojure.org/install/linux-install-${{ inputs.clojure-version }}.sh &&
sudo bash ./linux-install-${{ inputs.clojure-version }}.sh
- 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:
@@ -50,5 +36,3 @@ runs:
~/.m2
~/.gitlibs
key: ${{ steps.get-cache-keys.outputs.m2-cache-key }}
restore-keys: |
${{ steps.get-cache-keys.outputs.m2-restore-key }}

View File

@@ -3,7 +3,7 @@ description: Cypress preparation steps
inputs:
working-directory:
description: "Optional working directory for bun commands"
description: "Optional working directory for yarn commands"
required: false
default: "."
@@ -15,10 +15,11 @@ outputs:
runs:
using: "composite"
steps:
- name: Install Chrome v144
uses: browser-actions/setup-chrome@v2
- name: Install Chrome v111
uses: browser-actions/setup-chrome@v1
with:
chrome-version: 144
# https://chromium.cypress.io/linux/stable/111.0.5563.146
chrome-version: 1097615
id: setup-chrome
- name: Get cache keys
@@ -33,9 +34,9 @@ runs:
- name: Ensure that Cypress executable is ready
run: |
bun run cypress install
bun run cypress cache path
bun run cypress cache list
bun run cypress verify
yarn cypress install
yarn cypress cache path
yarn cypress cache list
yarn cypress verify
shell: bash
working-directory: ${{ inputs.working-directory }}

View File

@@ -1,5 +1,4 @@
name: Prepare front-end environment
description: "Prepare the front-end environment for the CI build"
inputs:
node-version:
description: "The version of Node.js to use, overriding .nvmrc"
@@ -11,10 +10,11 @@ inputs:
runs:
using: "composite"
steps:
- name: Prepare Node + Bun environment
uses: ./.github/actions/prepare-node-bun
- name: Prepare Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
node-version: ${{ inputs.node-version }} # this overrides, if present
node-version-file: ".nvmrc"
- name: Get cache keys
uses: ./.github/actions/get-cache-keys
@@ -27,15 +27,13 @@ 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
- name: Get yarn cache directory
id: yarn-cache-dir
run: |
BUN_CACHE_DIR=$(bun pm cache)
echo "Bun cache directory: $BUN_CACHE_DIR"
echo "dir=$BUN_CACHE_DIR" >> $GITHUB_OUTPUT
YARN_CACHE_DIR=$(yarn cache dir)
echo "Yarn cache directory: $YARN_CACHE_DIR"
echo "dir=$YARN_CACHE_DIR" >> $GITHUB_OUTPUT
shell: bash
- name: Restore node_modules cache
@@ -43,19 +41,17 @@ runs:
id: node-modules-cache
with:
path: |
${{ steps.bun-cache.outputs.dir }}
${{ steps.yarn-cache-dir.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.
# For example, we never want to cache node_modules with patches already applied so cache-generator.yml skips that step.
- name: Clean install
run: bun install --frozen-lockfile
run: yarn install --frozen-lockfile --prefer-offline
shell: bash
- name: Patch frontend dependencies
if: inputs.apply-patches == 'true'
run: bun run patch-package
run: yarn patch-package
shell: bash

View File

@@ -1,20 +0,0 @@
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

@@ -1,14 +0,0 @@
name: Prepare Release Scripts
description: Set up bun and build release scripts
runs:
using: composite
steps:
- uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.7"
token: ${{ github.token }}
- name: Build release scripts
shell: bash
run: cd ${{ github.workspace }}/release && bun install --frozen-lockfile && bun run build

View File

@@ -1,132 +0,0 @@
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

@@ -2,10 +2,9 @@ name: Prepare Uberjar Artifact
description: Prepare and upload Metabase JAR as a GitHub artifact.
inputs:
edition:
required: false
default: "ee"
description: Metabase edition (ee or oss)
name:
required: true
description: Artifact name
runs:
using: "composite"
@@ -19,7 +18,7 @@ runs:
- name: Upload JARs as artifact
uses: actions/upload-artifact@v4
with:
name: metabase-${{ inputs.edition }}-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
name: ${{ inputs.name }}
path: |
./target/uberjar/metabase.jar
./COMMIT-ID

View File

@@ -1,110 +0,0 @@
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

@@ -9,35 +9,56 @@ inputs:
build-static-viz:
required: true
default: false
logs-artifact-suffix:
required: false
description: Suffix to append to the test logs artifact name to avoid conflicts in matrix jobs
edition:
required: false
default: 'ee'
description: Edition to test (oss or ee)
outputs:
test-outcome:
description: "The outcome of the test run (success, failure, cancelled, skipped)"
value: ${{ steps.run-test.outcome }}
quarantine-skipped:
description: Whether the driver test was skipped due to quarantine
value: ${{ steps.check-driver.outputs.skip }}
runs:
using: "composite"
steps:
- name: Check if driver is ignored
id: check-driver
shell: bash
run: |
# If DRIVERS env var is not set, proceed with tests
if [ -z "$DRIVERS" ]; then
echo "No DRIVERS env var set. Proceeding with tests."
echo "skip=false" >> $GITHUB_OUTPUT
exit 0
fi
# Fetch CI config and check if driver should be skipped
curl -sSf -o ci-test-config.json https://raw.githubusercontent.com/metabase/ci-test-config/refs/heads/master/ci-test-config.json || true
# Check if the driver is in the ignored list (gracefully handle missing/invalid config)
IS_IGNORED=$(jq --arg driver "$DRIVERS" '.ignored.drivers // [] | contains([$driver])' ci-test-config.json 2>/dev/null || echo "false")
if [ "$IS_IGNORED" = "true" ]; then
echo "⚠️ Driver '$DRIVERS' is in the ignored list. Skipping tests."
echo "View the ignored list here: https://github.com/metabase/ci-test-config/blob/master/ci-test-config.json"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "Driver '$DRIVERS' is not ignored. Proceeding with tests."
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Prepare back-end environment
if: steps.check-driver.outputs.skip != 'true'
uses: ./.github/actions/prepare-backend
- name: Prepare front-end environment
if: ${{ inputs.build-static-viz == 'true' }}
if: ${{ steps.check-driver.outputs.skip != 'true' && inputs.build-static-viz == 'true' }}
uses: ./.github/actions/prepare-frontend
- name: Build static viz frontend
if: ${{ inputs.build-static-viz == 'true' }}
run: bun run build-static-viz
if: ${{ steps.check-driver.outputs.skip != 'true' && inputs.build-static-viz == 'true' }}
run: yarn build-static-viz
shell: bash
env:
MB_EDITION: ee
- name: Test database driver
run: clojure -X:dev:ci:${{ inputs.edition }}:${{ inputs.edition }}-dev:drivers:drivers-dev:test ${{ inputs.test-args }}
if: steps.check-driver.outputs.skip != 'true'
run: clojure -X:dev:ci:ee:ee-dev:drivers:drivers-dev:test ${{ inputs.test-args }}
shell: bash
id: run-test
# Trunk Quarantine controls the final state of the run in the CI!
@@ -49,7 +70,7 @@ runs:
# As per the documentation: https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#steps-context
# test.outcome gives us access to the result before `continue-on-error` overrides it and sets it to 'success'.
# We want test reports even if the test step gets cancelled (e.g. times out), hence we match anything that is not a 'success'.
if: ${{ steps.run-test.outcome != 'success' }}
if: ${{ steps.run-test.outcome != 'success' && steps.check-driver.outputs.skip != 'true' }}
with:
path: "target/junit/**/*_test.xml"
name: JUnit Test Report ${{ inputs.junit-name }}
@@ -58,10 +79,10 @@ runs:
list-tests: failed
fail-on-error: false
- name: Upload Logs on Failure
if: always() && steps.run-test.outcome != 'success'
if: always() && steps.run-test.outcome != 'success' && steps.check-driver.outputs.skip != 'true'
uses: actions/upload-artifact@v4
with:
name: ${{ format('test-logs-{0}{1}', github.job, inputs.logs-artifact-suffix && format('-{0}', inputs.logs-artifact-suffix) || '') }}
name: test-logs-${{ github.job }}
path: logs/test-log.json
retention-days: 1
# Failing to upload the artifact should never affect the outcome of the whole test run.

View File

@@ -20,9 +20,7 @@ inputs:
required: true
variant:
required: false
default: ""
previous-step-outcome:
required: true
default: ''
runs:
using: "composite"
@@ -58,5 +56,3 @@ runs:
org-slug: metabase
dry-run: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == true }} # quarantine, but do not upload results
token: ${{ inputs.trunk-api-token }}
use-cache: true
previous-step-outcome: ${{ inputs.previous-step-outcome }}

View File

@@ -1,55 +1,52 @@
# Metabase Developer Assistant Instructions
For detailed coding standards and conventions, see `CLAUDE.md` and the skills in `.claude/skills/`.
## Frontend Development
## Code Review Standards
### Repository Structure
Review focus areas: Security → Performance → Testing → Documentation (in priority order)
```
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
When reviewing code, use these emoji prefixes to categorize feedback:
- 🔒 Security concerns
- ⚡ Performance opportunities
- 🧹 Cleanup needs
- 📚 Documentation gaps
- 🚨 Blocking issues
- 💭 Clarification questions
e2e/ # Cypress end-to-end tests
frontend/test/ # Jest unit tests
```
### Security Critical Issues
### Technology Stack
- 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)
- **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
### Performance Red Flags
### Coding Standards
- 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
#### Component Preferences
### Code Quality Essentials
- Prefer `metabase/ui`components over `metabase/common/components`
- Use `.tsx` for components, `.ts` for utilities
- Functions should be focused and appropriately sized
- Use clear, descriptive naming conventions
- Ensure proper error handling throughout
- Remove dead code and unused imports
#### Styling
### Review Style
ALWAYS prefer Mantine style props,then CSS modules. DO NOT suggest styled components, they are deprecated.
- 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
#### TypeScript Migration
### Review Format
When heavily editing `.js`/`.jsx` files, create a separate PR to convert to TypeScript first, then implement changes.
- 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 Features
Language-specific rules are in `.github/instructions/*.instructions.md`.
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.

View File

@@ -26,11 +26,11 @@ frontend_ci: &frontend_ci
- ".github/workflows/frontend.yml"
frontend_configs: &frontend_configs
- "bun.lock"
- "yarn.lock"
- "**/tsconfig*.json"
- "package.json"
- "babel.config.json"
- "eslint.config.mjs"
- ".eslintrc.js"
- "postcss.config.js"
- "rspack.config.js"
- "rspack.main.config.js"
@@ -85,9 +85,7 @@ backend_ci: &backend_ci
backend_presto_kerberos:
- "**/presto_jdbc/**"
- "**/presto-jdbc/**" # the driver module has a hyphen. underscores are classpaths
- "**/presto_jdbc.clj"
- "**/presto_jdbc_test.clj"
- ".github/workflows/presto-kerberos-integration-test.yml"
backend_sources: &backend_sources
@@ -199,15 +197,3 @@ 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

@@ -1,9 +0,0 @@
---
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

@@ -1,9 +0,0 @@
---
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

@@ -1,11 +0,0 @@
---
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

@@ -1,52 +0,0 @@
// 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

@@ -1,242 +0,0 @@
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

@@ -1,41 +0,0 @@
/**
* jsonData must be in the format:
* [
* { column1: value1, column2: value2, ... },
* { column1: value3, column2: value4, ... },
* ...
* ]
*/
function jsonToCsvFormData(jsonData) {
const formData = new FormData();
const csvHeader = Object.keys(jsonData[0]).join(',') + '\n';
const csvContent = jsonData.map(row =>
Object.values(row).map(value => `"${String(value).replace(/"/g, '""')}"`).join(',')
).join('\n');
const blob = new Blob([csvHeader + csvContent], { type: 'text/csv' });
formData.append('file', blob, 'data.csv');
return formData;
}
function uploadCsvToMb({ baseUrl, tableId, jsonData, mode = 'append' }) {
const formData = jsonToCsvFormData(jsonData);
const operation = mode === 'replace' ? 'replace-csv' : 'append-csv';
return fetch(`${baseUrl}/api/table/${tableId}/${operation}`, {
method: 'POST',
headers: {
"x-api-key": process.env.API_KEY
},
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error(`Upload failed`);
}
return { success: true };
});
}
module.exports = { uploadCsvToMb };

View File

@@ -1,40 +0,0 @@
async function findLinearIssue({ githubIssueUrl, linearApiKey }) {
const response = await fetch("https://api.linear.app/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: linearApiKey,
},
body: JSON.stringify({
query: `
query {
attachmentsForURL(url: "${githubIssueUrl}") {
nodes {
issue { identifier }
}
}
}
`,
}),
});
const data = await response.json();
if (!response.ok) {
return { success: false, message: `Linear API error: ${response.status}` };
}
if (!data.data?.attachmentsForURL?.nodes?.length) {
return {
success: false,
message: `No Linear issue linked to ${githubIssueUrl}`,
};
}
return {
success: true,
issue: data.data.attachmentsForURL.nodes[0].issue.identifier,
};
}
module.exports = { findLinearIssue };

View File

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

40
.github/team.json vendored
View File

@@ -4,23 +4,21 @@
"name": "Querying",
"label": ".Team/Querying",
"members": [
"ranquild",
"romeovs",
"bshepherdson",
"metamben",
"rileythomp",
"camsual"
"wzimrin",
"ericnormand",
"galdre"
]
},
{
"name": "Graphy",
"label": ".Team/Graphy",
"name": "Drivers",
"label": ".Team/Drivers",
"members": [
"ranquild",
"romeovs",
"ericnormand",
"galdre",
"dpsutton",
"escherize",
"bgrabow"
"snoe",
"rileythomp"
]
},
{
@@ -45,6 +43,9 @@
"name": "DevEx",
"label": ".Team/DevEx",
"members": [
"camsaul",
"dpsutton",
"escherize",
"fraserdrops",
"nemanjaglumac"
]
@@ -61,8 +62,7 @@
"tsplude",
"lbrdnk",
"piranha",
"crisptrutski",
"undrfined"
"crisptrutski"
]
},
{
@@ -86,12 +86,14 @@
"eliotmetabase",
"npfitz",
"johnswanson",
"iethree",
"nvoxland",
"edpaget",
"deniskaber",
"alxnddr",
"bpander",
"andsalves",
"kevinkelleher12"
"gantoreno"
]
},
{
@@ -108,16 +110,6 @@
"uladzimirdev",
"egorgrushin"
]
},
{
"name": "Growth Eng",
"label": ".Team/GrowthEng",
"members": [
"iethree",
"bpander",
"gantoreno",
"losrebellos"
]
}
]
}

View File

@@ -15,14 +15,14 @@ jobs:
be-tests-mariadb:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 40
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
version:
- name: MariaDB 10.6
junit-name: be-tests-mariadb-10-6-ee
image: circleci/mariadb:10.6
- name: MariaDB 10.2
junit-name: be-tests-mariadb-10-2-ee
image: circleci/mariadb:10.2.23
env:
enable-ssl-tests: 'false'
- name: MariaDB Latest
@@ -68,8 +68,8 @@ jobs:
(
github.event_name == 'push' &&
(
github.ref_name == 'master' ||
startsWith(github.ref_name, 'release-')
github.ref == 'master' ||
startsWith(github.ref, 'release-')
) &&
''
) ||
@@ -100,12 +100,11 @@ 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-6-ee (0)"
# "be-tests-mariadb-10-2-ee (0)"
name: "${{ matrix.version.name }} ${{ matrix.job.name }}"
steps:
- uses: actions/checkout@v4
- name: Test ${{ matrix.version.name }}
id: test-driver
uses: ./.github/actions/test-driver
with:
build-static-viz: ${{ matrix.job.build-static-viz }}
@@ -124,19 +123,18 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
be-tests-mysql:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 40
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
version:
- name: MySQL 8.4
junit-name: be-tests-mysql-8-4-ee
image: cimg/mysql:8.4
- name: MySQL 8.0
junit-name: be-tests-mysql-8-0-ee
image: cimg/mysql:8.0
env:
enable-ssl-tests: 'false'
enable-aws-iam-tests: 'false'
@@ -184,8 +182,8 @@ jobs:
(
github.event_name == 'push' &&
(
github.ref_name == 'master' ||
startsWith(github.ref_name, 'release-')
github.ref == 'master' ||
startsWith(github.ref, 'release-')
) &&
''
) ||
@@ -224,12 +222,11 @@ 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-6-ee (0)"
# "be-tests-mariadb-10-2-ee (0)"
name: "${{ matrix.version.name }} ${{ matrix.job.name }}"
steps:
- uses: actions/checkout@v4
- name: Test ${{ matrix.version.name }}
id: test-driver
uses: ./.github/actions/test-driver
with:
build-static-viz: ${{ matrix.job.build-static-viz }}
@@ -248,19 +245,18 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
be-tests-postgres:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 40
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
version:
- name: Postgres 14.x
- name: Postgres 12.x
junit-name: postgres-ee
docker-image: postgres:14-alpine
docker-image: postgres:12-alpine
env:
enable-ssl-tests: 'false'
enable-aws-iam-tests: 'false'
@@ -301,8 +297,8 @@ jobs:
(
github.event_name == 'push' &&
(
github.ref_name == 'master' ||
startsWith(github.ref_name, 'release-')
github.ref == 'master' ||
startsWith(github.ref, 'release-')
) &&
''
) ||
@@ -340,7 +336,6 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Test ${{ matrix.version.name }} (${{ matrix.job.name }})
id: test-driver
uses: ./.github/actions/test-driver
with:
build-static-viz: ${{ matrix.job.build-static-viz }}
@@ -359,7 +354,6 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
app-db-tests-result:
needs:

View File

@@ -1,34 +1,12 @@
name: Backend-Cloverage
# Module lists are generated dynamically by listing source directories.
# The setup-oss and setup-ee jobs list folders in src/metabase/ and
# enterprise/backend/src/metabase_enterprise/ respectively, convert
# underscores to dashes (for Clojure namespace convention), and output
# as JSON arrays for the matrix jobs to consume.
#
# jq flags: -R (raw string input), -s (slurp all lines into one string),
# -c (compact single-line output for GitHub Actions).
on:
workflow_dispatch:
# 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
schedule:
- cron: '0 10 * * 1-5' # every weekday at 5am US eastern time
jobs:
setup-oss:
runs-on: ubuntu-22.04
outputs:
modules: ${{ steps.get-modules.outputs.modules }}
steps:
- uses: actions/checkout@v4
- id: get-modules
run: |
modules=$(ls -1d src/metabase/*/ | xargs -n1 basename | tr '_' '-' | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "modules=$modules" >> $GITHUB_OUTPUT
be-linter-cloverage:
needs: setup-oss
# don't run this workflow on a cron for forks
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
runs-on: ubuntu-22.04
@@ -36,7 +14,96 @@ jobs:
strategy:
fail-fast: false
matrix:
modules: ${{ fromJson(needs.setup-oss.outputs.modules) }}
modules:
- actions
- activity-feed
- analytics
- analyze
- api
- api-keys
- api-routes
- app-db
- appearance
- audit-app
- auth-provider
- batch-processing
- bookmarks
- bug-reporting
- cache
- channel
- classloader
- cloud-migration
- cmd
- collections
- config
- connection-pool
- content-translation
- content-verification
- core
- dashboards
- database-routing
- driver
- driver-api
- eid-translation
- embedding
- events
- formatter
- geojson
- glossary
- graph
- indexed-entities
- internal-stats
- legacy-mbql
- lib
- lib-be
- logger
- login-history
- model-persistence
- models
- native-query-snippets
- notification
- parameters
- permissions
- pivot
- plugins
- premium-features
- product-feedback
- public-sharing
- pulse
- queries
- query-permissions
- query-processor
- remote-sync
- request
- revisions
- sample-data
- search
- secrets
- segments
- server
- session
- settings
- setup
- sso
- startup
- store-api
- sync
- system
- task
- task-history
- testing-api
- tiles
- timeline
- types
- upload
- user-key-value
- users
- util
- version
- view-log
- warehouse-schema
- warehouses
- xrays
steps:
- uses: actions/checkout@v4
- name: Prepare front-end environment
@@ -45,7 +112,7 @@ jobs:
uses: ./.github/actions/prepare-backend
- name: Build static viz frontend
run: bun run build-static-viz
run: yarn build-static-viz
env:
MB_EDITION: ee
- name: Collect the test coverage
@@ -63,19 +130,7 @@ jobs:
files: ./target/coverage/codecov.json
flags: back-end
setup-ee:
runs-on: ubuntu-22.04
outputs:
modules: ${{ steps.get-modules.outputs.modules }}
steps:
- uses: actions/checkout@v4
- id: get-modules
run: |
modules=$(ls -1d enterprise/backend/src/metabase_enterprise/*/ | xargs -n1 basename | tr '_' '-' | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "modules=$modules" >> $GITHUB_OUTPUT
be-linter-cloverage-enterprise:
needs: setup-ee
# don't run this workflow on a cron for forks
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
runs-on: ubuntu-22.04
@@ -83,7 +138,53 @@ jobs:
strategy:
fail-fast: false
matrix:
modules: ${{ fromJson(needs.setup-ee.outputs.modules) }}
modules:
- action-v2
- advanced-config
- advanced-permissions
- ai-entity-analysis
- ai-sql-fixer
- ai-sql-generation
- analytics
- api
- audit-app
- auth-provider
- billing
- bookmarks
- cache
- cloud-add-ons
- comments
- content-translation
- content-verification
- core
- dashboard-subscription-filters
- database-replication
- database-routing
- dependencies
- documents
- email
- embedding-hub
- gsheets
- harbormaster
- impersonation
- internal-stats
- llm
- metabot-v3
- permission-debug
- premium-features
- public-sharing
- remote-sync
- sandbox
- scim
- search
- semantic-search
- serialization
- snippet-collections
- sso
- stale
- transforms
- transforms-python
- upload-management
steps:
- uses: actions/checkout@v4
- name: Prepare front-end environment
@@ -92,7 +193,7 @@ jobs:
uses: ./.github/actions/prepare-backend
- name: Build static viz frontend
run: bun run build-static-viz
run: yarn build-static-viz
env:
MB_EDITION: ee
- name: Collect the test coverage

View File

@@ -36,22 +36,10 @@ 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: 40
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
@@ -100,8 +88,8 @@ jobs:
(
github.event_name == 'push' &&
(
github.ref_name == 'master' ||
startsWith(github.ref_name, 'release-')
github.ref == 'master' ||
startsWith(github.ref, 'release-')
) &&
''
) ||
@@ -116,7 +104,7 @@ jobs:
with:
java-version: "${{ matrix.java-version }}"
- name: Build static-viz frontend
run: bun run build-static-viz
run: yarn build-static-viz
env:
MB_EDITION: ${{ matrix.job.edition }}
@@ -142,7 +130,6 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.run-java-tests.outcome }}
- name: Publish Test Report (JUnit)
uses: dorny/test-reporter@v1
@@ -196,7 +183,6 @@ jobs:
be-tests-result:
needs:
- be-linter-eastwood
- be-linter-eastwood-test
- be-linter-clj-kondo
- be-cljfmt
- be-check

View File

@@ -19,11 +19,9 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
release
.github
- name: Prepare release scripts
uses: ./.github/actions/prepare-release-scripts
sparse-checkout: release
- name: Prepare build scripts
run: cd ${{ github.workspace }}/release && yarn && yarn build
- name: Send build start message
uses: actions/github-script@v7
env:
@@ -81,11 +79,9 @@ jobs:
exit 1
- uses: actions/checkout@v4
with:
sparse-checkout: |
release
.github
- name: Prepare release scripts
uses: ./.github/actions/prepare-release-scripts
sparse-checkout: release
- name: Prepare build scripts
run: cd ${{ github.workspace }}/release && yarn && yarn build
- name: Get Release Version
uses: actions/github-script@v7
id: canonical_version
@@ -125,11 +121,9 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0 # IMPORTANT! to get all the branches
sparse-checkout: |
release
.github
- name: Prepare release scripts
uses: ./.github/actions/prepare-release-scripts
sparse-checkout: release
- name: Prepare build scripts
run: cd ${{ github.workspace }}/release && yarn && yarn build
- name: Get Release Branch
uses: actions/github-script@v7
id: release_branch
@@ -141,13 +135,12 @@ jobs:
const releaseBranch = getReleaseBranch(version);
return releaseBranch;
- name: Ensure that the specified commit exists in the ${{ steps.release_branch.outputs.result }} release branch
run: | # bash
run: |
RELEASE_BRANCH=${{ steps.release_branch.outputs.result }}
git checkout $RELEASE_BRANCH
git branch --contains ${{ inputs.commit }} | grep -q $RELEASE_BRANCH \
&& echo "Commit found in correct release branch" \
|| (echo "Commit not found in correct release branch" && exit 1)
git checkout -
get-build-requirements:
runs-on: ubuntu-22.04
@@ -159,11 +152,9 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
release
.github
- name: Prepare release scripts
uses: ./.github/actions/prepare-release-scripts
sparse-checkout: release
- name: Prepare build scripts
run: cd ${{ github.workspace }}/release && yarn && yarn build
- name: Get build dependencies
uses: actions/github-script@v7
id: dependencies

View File

@@ -82,7 +82,7 @@ jobs:
# Frontend preparation and caches
# `prepare-frontend` action (1) prepares Node.js and (2) fetches the node_modules cache if it exists.
# It then (3) runs `bun install --frozen-lockfile` which ensures the local cache is fully populated.
# It then (3) runs `yarn install --frozen-lockfile --prefer-offline` which ensures the local cache is fully populated.
# IMPORTANT: Caching node_modules after patches are already applied will break any PR that attempts to either:
# - upgrade a patched library
# - change a patch
@@ -92,31 +92,31 @@ jobs:
with:
apply-patches: "false"
- name: Get bun cache directory
id: bun-cache
- name: Get yarn cache directory
id: yarn-cache-dir
run: |
BUN_CACHE_DIR=$(bun pm cache)
echo "Bun cache directory: $BUN_CACHE_DIR"
echo "dir=$BUN_CACHE_DIR" >> $GITHUB_OUTPUT
YARN_CACHE_DIR=$(yarn cache dir)
echo "Yarn cache directory: $YARN_CACHE_DIR"
echo "dir=$YARN_CACHE_DIR" >> $GITHUB_OUTPUT
- name: Ensure that Cypress executable is ready
run: |
bun run cypress install
bun run cypress cache path
bun run cypress cache list
bun run cypress verify
yarn cypress install
yarn cypress cache path
yarn cypress cache list
yarn cypress verify
shell: bash
# Run Prettier to populate its cache (stored in node_modules/.cache/prettier)
# so it gets saved as part of the node_modules cache below
- name: Run Prettier to populate cache
run: bun run lint-prettier-pure
run: yarn lint-prettier-pure
- name: Update node_modules cache
uses: ./.github/actions/update-cache
with:
path: |
${{ steps.bun-cache.outputs.dir }}
${{ steps.yarn-cache-dir.outputs.dir }}
node_modules
key: ${{ env.NODE_CACHE_KEY }}
@@ -128,10 +128,10 @@ jobs:
# ESLint cache (requires both backend and frontend)
- name: Compile CLJS
run: bun run build-pure:cljs
run: yarn build-pure:cljs
- name: Run ESLint to populate cache
run: bun run lint-eslint-pure
run: yarn lint-eslint-pure
- name: Update eslint cache
uses: ./.github/actions/update-cache

View File

@@ -26,13 +26,11 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
release
.github
sparse-checkout: release
fetch-depth: ${{ env.isNewReleaseBranch == 'false' && 3 || 0 }} # we need the full history to get all the commit messages
ref: master # we want the logic from master, even though we're triggering on a release branch
- name: Prepare release scripts
uses: ./.github/actions/prepare-release-scripts
- name: Prepare build scripts
run: cd ${{ github.workspace }}/release && yarn && yarn build
- uses: actions/github-script@v7
if: ${{ env.isNewReleaseBranch == 'false' }}
name: Set milestone for single commit

View File

@@ -41,7 +41,7 @@ jobs:
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Compile CLJS
run: NODE_ENV=development bun run build-pure:cljs
run: NODE_ENV=development yarn build-pure:cljs
- name: Publish to Chromatic
uses: chromaui/action@v10.2.0
env:

View File

@@ -70,31 +70,6 @@ jobs:
core.setFailed(error.message);
}
- name: Find linked Linear issue
id: find_linear
continue-on-error: true
uses: actions/github-script@v7
env:
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
ISSUE_NUMBER: ${{ steps.get_issue.outputs.issue_number }}
with:
script: |
const { findLinearIssue } = require('${{ github.workspace }}/.github/scripts/find-linear-issue.js');
const githubIssueUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/issues/${process.env.ISSUE_NUMBER}`;
const result = await findLinearIssue({
githubIssueUrl,
linearApiKey: process.env.LINEAR_API_KEY
});
if (result.success) {
console.log(`Found Linear issue: ${result.issue}`);
core.setOutput('linear_issue', result.issue);
} else {
console.log(`Could not find Linear issue: ${result.message}`);
}
- name: Set up git configuration
run: |
git config --global user.name "Claude Code[bot]"
@@ -230,12 +205,6 @@ jobs:
mkdir -p .claude-scratchpad
echo "Scratchpad directory created for Claude's temporary verification files"
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Generate GitHub App Token for Claude
uses: actions/create-github-app-token@v1
id: claude-app-token
@@ -250,7 +219,7 @@ jobs:
github_token: ${{ steps.claude-app-token.outputs.token }}
prompt: ${{ env.BUG_FIX_PROMPT }}
claude_args: |
--allowedTools "Bash(bun run test-unit*),Bash(bun run lint-eslint-pure),Bash(./bin/mage*),Bash(clojure*),Bash(find*),Bash(ls*),Bash(grep*),Write,Read,Glob,Grep,Edit,MultiEdit"
--allowedTools "Bash(yarn test-unit*),Bash(yarn lint-eslint-pure),Bash(./bin/mage*),Bash(clojure*),Bash(find*),Bash(ls*),Bash(grep*),Write,Read,Glob,Grep,Edit,MultiEdit"
--disallowedTools "Bash(git commit*),Bash(git push*),Bash(gh*)"
- name: Clean up scratchpad directory
@@ -290,7 +259,6 @@ jobs:
core.setOutput('reviewers', issueCreator);
- name: Create Pull Request
id: create_pr
if: steps.check_changes.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@v6
with:
@@ -311,8 +279,6 @@ jobs:
body: |
This PR automatically fixes the bug reported in #${{ env.ISSUE_NUMBER }}.
${{ steps.find_linear.outputs.linear_issue && format('Closes {0}', steps.find_linear.outputs.linear_issue) || '' }}
## Bug Report
**Original Issue:** #${{ env.ISSUE_NUMBER }}
**Reporter:** @${{ env.ISSUE_AUTHOR }}
@@ -333,6 +299,7 @@ jobs:
- If issues are found, either fix them or close this PR
---
Closes #${{ env.ISSUE_NUMBER }}
🤖 Generated with [Claude Code](https://claude.ai/code)
@@ -342,7 +309,6 @@ jobs:
automated-fix
ci skip
reviewers: ${{ steps.prepare_reviewers.outputs.reviewers }}
assignees: ${{ github.actor }}
- name: Log existing branch update
if: steps.check_changes.outputs.has_changes == 'true' && env.BRANCH_EXISTS == 'true'

View File

@@ -0,0 +1,98 @@
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)"

101
.github/workflows/claude-docs-review.yml vendored Normal file
View File

@@ -0,0 +1,101 @@
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.
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"

33
.github/workflows/clean-cache.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Cleanup caches by a branch
on:
pull_request:
types:
- closed
jobs:
cleanup:
runs-on: ubuntu-22.04
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Cleanup
run: |
gh extension install actions/gh-actions-cache
REPO=${{ github.repository }}
BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge"
echo "Fetching list of cache key"
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 )
## Setting this to not fail the workflow while deleting cache keys.
set +e
echo "Deleting caches..."
for cacheKey in $cacheKeysForPR
do
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
done
echo "Done"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -39,4 +39,4 @@ jobs:
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Run Cljs tests
run: bun run test-cljs
run: yarn run test-cljs

View File

@@ -1,118 +1,19 @@
name: Codespell
on:
schedule:
# Run nightly at 4:00 AM UTC
- cron: "0 4 * * *"
workflow_dispatch:
push:
branches:
- 'master'
- 'release-**'
pull_request:
jobs:
codespell:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Install codespell
run: pip install codespell
- name: Set up git configuration
run: |
git config --global user.name "Claude Code[bot]"
git config --global user.email "noreply@anthropic.com"
- name: Get current date
id: date
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Run codespell check
id: codespell_check
continue-on-error: true
run: |
set -o pipefail
./bin/mage codespell 2>&1 | tee codespell_output.txt
- name: Generate GitHub App Token for Claude
if: steps.codespell_check.outcome == 'failure'
uses: actions/create-github-app-token@v1
id: claude-app-token
- uses: actions/checkout@v3
- uses: codespell-project/actions-codespell@v1
with:
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_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*)"
- name: Clean up temporary files
if: steps.codespell_check.outcome == 'failure'
run: rm -f codespell_output.txt
- name: Create Pull Request
if: steps.codespell_check.outcome == 'failure'
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: claude-fix/codespell-${{ steps.date.outputs.date }}
base: master
delete-branch: true
commit-message: |
Fix spelling errors detected by codespell (${{ steps.date.outputs.date }})
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
committer: "Claude Code[bot] <noreply@anthropic.com>"
author: "Claude Code[bot] <noreply@anthropic.com>"
title: "Fix spelling errors detected by codespell (${{ steps.date.outputs.date }})"
body: |
This PR automatically fixes spelling errors detected by the scheduled codespell check.
## Automated Fix
🤖 This fix was generated automatically by Claude Code based on codespell output.
**Please review carefully:**
- [ ] The spelling fixes are correct
- [ ] No code functionality was changed
- [ ] Technical terms were preserved correctly
---
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
labels: |
automated-fix
backport
# keep this list in sync with the list in `mage/src/mage/codespell.yml`
path: >-
docs

View File

@@ -4,10 +4,8 @@ run-name: Containerize ${{ inputs.tag }} Uberjar
on:
workflow_call:
inputs:
edition:
description: Metabase edition (ee or oss)
artifact-name:
type: string
required: true
repo:
description: owner and repo like "metabase/metabase"
type: string
@@ -17,18 +15,14 @@ on:
type: string
release:
type: string
default: 'false'
default: 'true'
build-args:
type: string
workflow_dispatch:
inputs:
edition:
description: Metabase edition (ee or oss)
type: choice
options:
- ee
- oss
required: true
artifact-name:
description: usually something like "metabase-release-{{ edition }}-{{ hash }}-uberjar"
type: string
repo:
description: docker owner and repo like "metabase/metabase"
type: string
@@ -38,13 +32,6 @@ on:
commit:
description: the full commit hash
type: string
release:
description: Whether this is a release artifact (uses metabase-release- prefix)
type: choice
options:
- "false"
- "true"
default: "false"
jobs:
containerize:
@@ -58,14 +45,12 @@ jobs:
fetch-depth: 0
# we want the dockerfile from the commit we're building
- run: git checkout ${{ inputs.commit }} -- bin/docker
- name: Retrieve uberjar artifact
- name: Retrieve test build uberjar artifact
uses: ./.github/actions/fetch-artifact
with:
commit: ${{ inputs.commit }}
edition: ${{ inputs.edition }}
release: ${{ inputs.release }}
- name: Prepare release scripts
uses: ./.github/actions/prepare-release-scripts
name: ${{ inputs.artifact-name }}
- name: Prepare build scripts
run: cd ${{ github.workspace }}/release && yarn && yarn build
- name: Get platforms
id: platforms
uses: actions/github-script@v7

View File

@@ -55,13 +55,13 @@ jobs:
uses: ./.github/workflows/containerize-jar.yml
secrets: inherit
with:
edition: ${{ matrix.edition }}
commit: ${{ github.event.inputs.commit || github.event.pull_request.head.sha || github.sha }}
artifact-name: metabase-${{ matrix.edition }}-${{ github.event.inputs.commit || github.event.pull_request.head.sha || github.sha }}-uberjar
commit: ${{ github.event.inputs.commit || github.sha }}
repo: ${{ fromJson(needs.get-container-info.outputs[matrix.edition]).repo }}
tag: ${{ fromJson(needs.get-container-info.outputs[matrix.edition]).tag }}
release: "false"
build-args: |
GIT_COMMIT_SHA=${{ github.event.inputs.commit || github.event.pull_request.head.sha || github.sha }}
GIT_COMMIT_SHA=${{ github.event.inputs.commit || github.sha }}
run-trivy:
needs: [get-container-info, containerize]
@@ -74,7 +74,7 @@ jobs:
- name: Check out the code
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit || github.sha }}
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@0.29.0
env:

View File

@@ -17,7 +17,7 @@ jobs:
uses: ./.github/actions/prepare-backend
- name: Run frontend unit tests
run: |
bun run test-unit \
yarn run test-unit \
--silent \
--coverage \
--testTimeout=60000

View File

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

View File

@@ -1,102 +0,0 @@
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

@@ -1,79 +0,0 @@
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.59.x"
default: "release-x.57.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.59.x' }}
target_branch: ${{ inputs.target_branch || 'release-x.57.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-6-ee:
be-tests-mariadb-10-2-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.6
image: circleci/mariadb:10.2.23
ports:
- "3306:3306"
steps:
- uses: actions/checkout@v4
- name: Test MariaDB driver (10.6)
- name: Test MariaDB driver (10.2)
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mariadb-10-6-ee'
junit-name: 'be-tests-mariadb-10-2-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-4-ee:
be-tests-mysql-8-0-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.4
image: cimg/mysql:8.0
ports:
- "3306:3306"
steps:
- uses: actions/checkout@v4
- name: Test MySQL driver (8.4)
- name: Test MySQL driver (8.0)
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mysql-8-4-ee'
junit-name: 'be-tests-mysql-8-0-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:14-alpine
image: postgres:12-alpine
ports:
- "5432:5432"
env:
@@ -447,7 +447,7 @@ jobs:
POSTGRES_HOST_AUTH_METHOD: trust
steps:
- uses: actions/checkout@v4
- name: Test Postgres driver (14)
- name: Test Postgres driver (12)
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-postgres-ee'

File diff suppressed because it is too large Load Diff

View File

@@ -76,15 +76,19 @@ jobs:
timeout-minutes: 60
name: Stress test E2E flake fix
env:
DISPLAY: "" # https://github.com/cypress-io/cypress/issues/4034
TERM: xterm
TZ: US/Pacific # to make Node match the instance tz
DISPLAY: ""
MB_RUN_MODE: e2e
METASTORE_DEV_SERVER_URL: ${{ secrets.METASTORE_DEV_SERVER_URL }}
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 }}
CYPRESS_MB_PRO_SELF_HOSTED_TOKEN: ${{ secrets.STAGING_MB_PRO_SELF_HOSTED_TOKEN }}
MB_SNOWPLOW_AVAILABLE: true
MB_SNOWPLOW_URL: "http://localhost:9090" # Snowplow micro
MB_EDITION: ${{ inputs.mb_edition }}
CYPRESS_ENABLE_NETWORK_THROTTLING: ${{ inputs.enable_network_throttling }}
TERM: xterm
TZ: US/Pacific # to make node match the instance tz
steps:
- uses: actions/checkout@v4
with:
@@ -110,33 +114,31 @@ jobs:
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Prepare back-end environment
if: ${{ needs.determine-test-type.outputs.type == 'component' }}
uses: ./.github/actions/prepare-backend
with:
m2-cache-key: "cljs"
- name: Build Embedding SDK package
if: ${{ needs.determine-test-type.outputs.type == 'component' }}
run: bun run build-embedding-sdk-package
run: yarn build-embedding-sdk-package
- name: Prepare JDK 21
uses: actions/setup-java@v4
with:
java-version: 21
distribution: "temurin"
- name: Prepare Cypress environment
id: cypress-prep
uses: ./.github/actions/prepare-cypress
- name: Compile CLJS
run: bun run build-pure:cljs
- name: Run Metabase
run: node e2e/runner/run_cypress_ci.js start
- name: Generate default database snapshot
if: inputs.qa_db == false # true boolean, do not use string here!
- name: Generate database snapshots
run: |
node e2e/runner/run_cypress_ci.js snapshot \
--spec "e2e/snapshot-creators/default.cy.snap.js"
- name: Generate all database snapshots
if: inputs.qa_db
run: |
node e2e/runner/run_cypress_ci.js snapshot
--env grepTags="${{inputs.qa_db && '' || '-@external'}}"
# For all options and fine-grained control, take a look at the documentation
# https://github.com/cypress-io/cypress/tree/develop/npm/grep

View File

@@ -29,29 +29,34 @@ on:
required: false
type: number
env:
DISPLAY: "" # https://github.com/cypress-io/cypress/issues/4034
TERM: xterm
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 || '' }}
JOB_NAME: ${{ inputs.name }}
jobs:
e2e-tests:
runs-on: ${{ inputs.runner }}
timeout-minutes: 40
timeout-minutes: 45
name: e2e-tests-${{ inputs.name }}-${{ inputs.edition }}
env:
MB_EDITION: ${{ inputs.edition }}
# 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)
DISPLAY: ""
PR_NUMBER: ${{ github.event.pull_request.number || '' }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HASH: ${{ github.event.pull_request.head.sha || github.sha }}-${{ github.run_attempt }}
JOB_NAME: ${{ inputs.name }}
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")`
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 }}
CYPRESS_MB_PRO_SELF_HOSTED_TOKEN: ${{ secrets.STAGING_MB_PRO_SELF_HOSTED_TOKEN }}
CYPRESS_PULL_REQUEST_ID: ${{ github.event.pull_request.number || '' }}
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title || github.event.head_commit.message || github.event.head.sha }}
MB_EXPERIMENTAL_SEARCH_BLOCK_ON_QUEUE: true
MB_SEARCH_ENGINE: appdb
MB_SNOWPLOW_AVAILABLE: true
MB_SNOWPLOW_URL: "http://localhost:9090" # Snowplow micro
TZ: US/Pacific # to make node match the instance tz
CYPRESS_CI: true
steps:
- uses: actions/checkout@v4
with:
@@ -80,15 +85,18 @@ jobs:
- name: Retrieve uberjar artifact for ${{ inputs.edition }}
uses: ./.github/actions/fetch-artifact
with:
commit: ${{ github.event.pull_request.head.sha || github.sha }}
edition: ${{ inputs.edition }}
name: metabase-${{ inputs.edition }}-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
extract-path: "target/uberjar"
output-name: "metabase.jar"
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Prepare JDK 21
uses: actions/setup-java@v4
with:
java-version: 21
distribution: "temurin"
- name: Prepare Cypress environment
id: cypress-prep
@@ -106,9 +114,6 @@ jobs:
if: github.run_attempt != '1'
run: echo "specs=$(cat failed-specs)" >> $GITHUB_ENV
- name: Compile CLJS
run: bun run build-pure:cljs
- name: Run Metabase
run: node e2e/runner/run_cypress_ci.js start
@@ -118,7 +123,6 @@ jobs:
run: node e2e/runner/run_cypress_ci.js snapshot
- name: Run auto-split Cypress tests on ${{ inputs.name }}
id: split-e2e
if: ${{ !inputs.specs }}
env:
SPLIT: ${{ inputs.total_chunks }}
@@ -142,7 +146,6 @@ jobs:
continue-on-error: true
- name: Run Tagged EE Cypress tests on ${{ inputs.name }}
id: tagged-e2e
if: ${{ inputs.specs }}
shell: bash
env:
@@ -172,12 +175,10 @@ jobs:
# Related to Trunk uploader
variant: e2e-tests
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ inputs.specs && steps.tagged-e2e.outcome || steps.split-e2e.outcome }}
- name: Upload Cypress Artifacts upon failure
uses: actions/upload-artifact@v4
if: |
!cancelled() && (steps.split-e2e.outcome == 'failure' || steps.tagged-e2e.outcome == 'failure')
if: failure()
with:
name: cypress-recording-${{ inputs.name }}-${{ inputs.edition }}
path: |
@@ -187,12 +188,10 @@ jobs:
- name: Upload Failed Tests
uses: actions/upload-artifact@v4
if: |
!cancelled() && (steps.split-e2e.outcome == 'failure' || steps.tagged-e2e.outcome == 'failure')
if: failure()
with:
name: failed-tests-${{ inputs.name }}-${{ inputs.edition }}
path: ./cypress/test-results
if-no-files-found: ignore
- name: Extract chunk-specific changed timings
id: extract-timings

View File

@@ -3,12 +3,6 @@ name: Embedding SDK Component Tests
on:
workflow_call:
env:
DISPLAY: "" # https://github.com/cypress-io/cypress/issues/4034
TERM: xterm
TZ: US/Pacific # to make Node match the instance tz
MB_EDITION: "ee"
jobs:
sdk-component-tests:
name: "SDK component tests (React ${{ matrix.react-version }})"
@@ -19,35 +13,27 @@ jobs:
react-version: [18, 19]
fail-fast: false
env:
# 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)
MB_EDITION: ee
DISPLAY: ""
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")`
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 }}
CYPRESS_MB_PRO_SELF_HOSTED_TOKEN: ${{ secrets.STAGING_MB_PRO_SELF_HOSTED_TOKEN }}
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title || github.event.head_commit.message || github.event.head.sha }}
TZ: US/Pacific # to make node match the instance tz
CYPRESS_CI: true
steps:
- uses: actions/checkout@v4
- name: Prepare Docker containers
uses: ./.github/actions/e2e-prepare-containers
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
maildev: true
openldap: false
webhook: false
snowplow: false
postgres: false
mysql: false
mongo: false
- name: Retrieve uberjar artifact for ee
uses: ./.github/actions/fetch-artifact
with:
commit: ${{ github.event.pull_request.head.sha || github.sha }}
edition: ee
name: metabase-ee-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
extract-path: "target/uberjar"
output-name: "metabase.jar"
@@ -59,8 +45,12 @@ jobs:
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Prepare JDK 21
uses: actions/setup-java@v4
with:
java-version: 21
distribution: "temurin"
- name: Prepare Cypress environment
id: cypress-prep
@@ -82,7 +72,6 @@ jobs:
run: ./bin/embedding-sdk/change-react-version.bash ${{ matrix.react-version }}
- name: Run component tests for Embedding SDK
id: run-tests
env:
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
run: node e2e/runner/run_cypress_ci.js component
@@ -98,7 +87,6 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.run-tests.outcome }}
- name: "SDK component tests > Upload Cypress Artifacts upon failure"
uses: actions/upload-artifact@v4
@@ -154,15 +142,19 @@ jobs:
runs-on: ubuntu-22.04
timeout-minutes: 45
env:
MB_EDITION: ee
DISPLAY: ""
MB_RUN_MODE: "e2e"
METASTORE_DEV_SERVER_URL: ${{ secrets.METASTORE_DEV_SERVER_URL }}
# 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)
# 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")`
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 }}
CYPRESS_MB_PRO_SELF_HOSTED_TOKEN: ${{ secrets.STAGING_MB_PRO_SELF_HOSTED_TOKEN }}
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title || github.event.head_commit.message || github.event.head.sha }}
TZ: US/Pacific # to make node match the instance tz
CYPRESS_CI: true
steps:
- name: Check out the code from the previous commit
@@ -170,19 +162,6 @@ jobs:
with:
ref: ${{ needs.decide-target-ref.outputs.target-ref }}
- name: Prepare Docker containers
uses: ./.github/actions/e2e-prepare-containers
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
maildev: true
openldap: false
webhook: false
snowplow: false
postgres: false
mysql: false
mongo: false
- name: Get the actual commit SHA from the checked out branch
id: get-commit-sha
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
@@ -192,15 +171,12 @@ jobs:
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Attempt to retrieve the ee uberjar artifact from the previous commit
id: download-uberjar
uses: ./.github/actions/fetch-artifact
with:
commit: ${{ steps.get-commit-sha.outputs.sha }}
edition: ee
name: metabase-ee-${{ steps.get-commit-sha.outputs.sha }}-uberjar
extract-path: "target/uberjar"
output-name: "metabase.jar"
continue-on-error: true # do not break the whole job if this step doesn't succeed
@@ -234,7 +210,6 @@ jobs:
--spec "e2e/snapshot-creators/default.cy.snap.js"
- name: Run component tests for Embedding SDK
id: run-tests
env:
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
run: |
@@ -252,7 +227,6 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.run-tests.outcome }}
- name: Upload Cypress Artifacts upon failure
uses: actions/upload-artifact@v4

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