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
10547 changed files with 570332 additions and 1008148 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

@@ -29,11 +29,6 @@ to run 3 tests, or
## 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,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,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."}
@@ -427,6 +410,7 @@
: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
@@ -436,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
@@ -467,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
@@ -527,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
@@ -548,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
@@ -569,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!
@@ -606,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
@@ -709,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
@@ -786,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"}
@@ -932,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"}
@@ -980,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

@@ -97,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
@@ -118,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",
},
},
],
};

7
.gitattributes vendored
View File

@@ -1,7 +0,0 @@
# Custom merge driver for YAML migration files
# Treats each changeset as an atomic unit to avoid conflicts within changesets
# Execute this in your shell to enable custom merge driver:
# echo '\n[include]\npath=../.gitconfig' >> .git/config
#
# TODO (Bryan 12/12/25) -- reenable this when we can completely verify correctness
# resources/migrations/*.yaml merge=yaml-migrations

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 = YAML migration changeset-aware merge
driver = bin/mage -merge-yaml-migrations %O %A %B %L %P

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,21 +25,15 @@ 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;

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,7 +17,7 @@ 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
@@ -28,13 +29,6 @@ runs:
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:

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"
@@ -17,12 +16,6 @@ runs:
node-version: ${{ inputs.node-version }} # this overrides, if present
node-version-file: ".nvmrc"
- name: Install bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.7"
token: ${{ github.token }}
- name: Get cache keys
uses: ./.github/actions/get-cache-keys
id: get-cache-keys
@@ -35,12 +28,12 @@ runs:
~/.gitlibs
key: ${{ steps.get-cache-keys.outputs.m2-cache-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
@@ -48,7 +41,7 @@ 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 }}
@@ -56,9 +49,9 @@ runs:
# 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,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,77 +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.js from current uberjar
shell: bash
run: unzip -j temp/current-uberjar/target/uberjar/metabase.jar frontend_client/app/embedding-sdk.js -d temp/current-bundle
- 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: |
current_size=$(wc -c < temp/current-bundle/embedding-sdk.js) || { echo "::error::Failed to read current bundle"; exit 1; }
base_ref_size=$(wc -c < temp/base-ref-src/resources/frontend_client/app/embedding-sdk.js) || { echo "::error::Failed to read base-ref bundle"; exit 1; }
[ "$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 "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

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,7 +26,7 @@ frontend_ci: &frontend_ci
- ".github/workflows/frontend.yml"
frontend_configs: &frontend_configs
- "bun.lock"
- "yarn.lock"
- "**/tsconfig*.json"
- "package.json"
- "babel.config.json"
@@ -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,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 };

26
.github/team.json vendored
View File

@@ -10,7 +10,14 @@
"metamben",
"wzimrin",
"ericnormand",
"galdre",
"galdre"
]
},
{
"name": "Drivers",
"label": ".Team/Drivers",
"members": [
"snoe",
"rileythomp"
]
},
@@ -55,8 +62,7 @@
"tsplude",
"lbrdnk",
"piranha",
"crisptrutski",
"undrfined"
"crisptrutski"
]
},
{
@@ -80,12 +86,14 @@
"eliotmetabase",
"npfitz",
"johnswanson",
"iethree",
"nvoxland",
"edpaget",
"deniskaber",
"alxnddr",
"bpander",
"andsalves",
"kevinkelleher12"
"gantoreno"
]
},
{
@@ -102,16 +110,6 @@
"uladzimirdev",
"egorgrushin"
]
},
{
"name": "Growth Eng",
"label": ".Team/GrowthEng",
"members": [
"iethree",
"bpander",
"gantoreno",
"losrebellos"
]
}
]
}

View File

@@ -15,7 +15,7 @@ jobs:
be-tests-mariadb:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 40
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
@@ -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-')
) &&
''
) ||
@@ -105,7 +105,6 @@ jobs:
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,12 +123,11 @@ 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:
@@ -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-')
) &&
''
) ||
@@ -229,7 +227,6 @@ jobs:
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,12 +245,11 @@ 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:
@@ -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,33 +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:
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
@@ -35,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
@@ -44,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
@@ -62,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
@@ -82,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
@@ -91,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

@@ -39,7 +39,7 @@ jobs:
be-tests:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 40
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
@@ -88,8 +88,8 @@ jobs:
(
github.event_name == 'push' &&
(
github.ref_name == 'master' ||
startsWith(github.ref_name, 'release-')
github.ref == 'master' ||
startsWith(github.ref, 'release-')
) &&
''
) ||
@@ -104,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 }}
@@ -130,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

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,110 +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: Fix spelling errors with Claude Code
if: steps.codespell_check.outcome == 'failure'
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ steps.claude-app-token.outputs.token }}
prompt: |
You are tasked with fixing spelling errors detected by codespell in this repository.
## Codespell Output
The following spelling errors were detected:
```
$(cat codespell_output.txt)
```
## Instructions
1. **Review the codespell output and identify reported spelling errors** - The usual format codespell uses is `typo ==> correction`
2. **Fix every spelling error** - Use the Edit tool to fix each misspelling with the suggested correction
- If codespell suggests multiple corrections, choose the most appropriate one based on context
Human reviewers will verify the changes before merging, so fix all errors reported by codespell.
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

@@ -8,7 +8,7 @@ on:
target_branch:
description: "Target branch for documentation update"
required: false
default: "release-x.58.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.58.x' }}
target_branch: ${{ inputs.target_branch || 'release-x.57.x' }}
secrets:
automation_token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}

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

View File

@@ -3,13 +3,6 @@ name: "[NOT REQUIRED] SDK Compatibility 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
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HASH: ${{ github.event.pull_request.head.sha || github.sha }}-${{ github.run_attempt }}
MB_EDITION: ee
jobs:
get-sample-apps-data:
runs-on: ubuntu-22.04
@@ -20,7 +13,12 @@ jobs:
TEST_SUITES: |
[
"metabase-nodejs-react-sdk-embedding-sample-e2e",
"metabase-nextjs-sdk-embedding-sample-e2e"
"metabase-nextjs-sdk-embedding-sample-e2e",
"shoppy-e2e"
]
TEST_SUITES_TO_RUN_IN_MASTER_ONLY: |
[
"shoppy-e2e"
]
SAMPLE_APP_MAIN_BRANCH_NAME: "main"
SAMPLE_APP_STABLE_RELEASE_BRANCH_NAME_SUFFIX: "stable"
@@ -30,8 +28,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Prepare release scripts
uses: ./.github/actions/prepare-release-scripts
- name: Prepare build scripts
run: cd ${{ github.workspace }}/release && yarn && yarn build
- name: Build sample app compatibility data
uses: actions/github-script@v7
@@ -44,6 +42,7 @@ jobs:
} = require('${{ github.workspace }}/release/dist/index.cjs');
const testSuites = JSON.parse(process.env.TEST_SUITES);
const testSuitesToRunInMasterOnly = JSON.parse(process.env.TEST_SUITES_TO_RUN_IN_MASTER_ONLY);
const ref = '${{ github.base_ref || github.ref }}';
const branchName = ref.replace(/^refs\/heads\//, '');
@@ -67,16 +66,19 @@ jobs:
const sampleAppBranchName = `${currentBranchVersion}-${{ env.SAMPLE_APP_STABLE_RELEASE_BRANCH_NAME_SUFFIX }}`
const shouldRunTestsForVersion = (currentReleaseVersion - currentBranchVersion) <= (supportedReleasesCount - 1);
const releaseBranchTestSuites = testSuites.filter(testSuite => !testSuitesToRunInMasterOnly.includes(testSuite));
return {
branch_name: sampleAppBranchName,
matrix: shouldRunTestsForVersion ? testSuites : [],
matrix: shouldRunTestsForVersion
? releaseBranchTestSuites
: []
};
}
return {
branch_name: '',
matrix: [],
matrix: []
};
# In order to check compatibility between Sample Apps and Embedding SDK, the job for each sample app:
@@ -98,9 +100,15 @@ jobs:
name: e2e-sample-app-${{ matrix.test_suite }}-tests-${{ matrix.environment }}-bundle
env:
MB_EDITION: ee
DISPLAY: ""
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number || '' }}
HASH: ${{ github.event.pull_request.head.sha || github.sha }}-${{ github.run_attempt }}
MB_RUN_MODE: e2e
METASTORE_DEV_SERVER_URL: ${{ secrets.METASTORE_DEV_SERVER_URL }}
CYPRESS_MB_ALL_FEATURES_TOKEN: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
TZ: US/Pacific # to make node match the instance tz
CYPRESS_CI: true
SAMPLE_APP_BRANCH_NAME: ${{ needs.get-sample-apps-data.outputs.branch_name }}
SAMPLE_APP_ENVIRONMENT: ${{ matrix.environment }}
permissions:
@@ -109,8 +117,6 @@ jobs:
steps:
- uses: actions/checkout@v4
- 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
@@ -127,8 +133,7 @@ jobs:
- 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"
@@ -158,16 +163,30 @@ jobs:
echo "sample_app_branch_exists=false" >> $GITHUB_OUTPUT
fi
- name: Prepare and launch Sample Apps
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' }}
- name: Configure AWS credentials
if: ${{ matrix.test_suite == 'shoppy-e2e' }}
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.SHOPPY_IAM_ROLE }}
role-session-name: GitHub_to_AWS_via_FederatedOIDC
aws-region: us-east-1
- name: Download Shoppy's App DB PostgreSQL dump from S3
if: ${{ matrix.test_suite == 'shoppy-e2e' }}
run: |
aws s3 cp s3://${{ secrets.SHOPPY_METABASE_APP_DB_S3_BUCKET }}/${{ secrets.SHOPPY_METABASE_APP_DB_S3_KEY }} ./e2e/tmp/db_dumps/shoppy_metabase_app_db_dump.sql
- name: Prepare and launch Sample Apps for shoppy-e2e test suite
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' && matrix.test_suite == 'shoppy-e2e' }}
env:
METABASE_APP_DB_USER: ${{ secrets.SHOPPY_METABASE_APP_DB_USER }}
METABASE_APP_DB: ${{ secrets.SHOPPY_METABASE_APP_DB_DATABASE }}
run: npx tsx ./e2e/runner/embedding-sdk/sample-apps/start-ci.ts shoppy-e2e
- name: Prepare and launch Sample Apps for other test suites
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' && matrix.test_suite != 'shoppy-e2e' }}
run: npx tsx ./e2e/runner/embedding-sdk/sample-apps/start-ci.ts ${{ matrix.test_suite }}
- name: Prune build cache
run: docker builder prune -af
- name: Log free disk space
run: df -h /
- name: Run e2e tests for Sample App
id: run-e2e-tests
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' }}
@@ -180,7 +199,7 @@ jobs:
uses: actions/upload-artifact@v4
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' && steps.run-e2e-tests.outcome != 'success' }}
with:
name: cypress-recording-sample-app-${{ matrix.test_suite }}-${{ matrix.environment }}-latest
name: cypress-recording-sample-app-${{ matrix.test_suite }}-latest
path: |
./e2e/tmp/${{ steps.get-sample-app-name.outputs.sample_app_name }}/cypress
if-no-files-found: ignore
@@ -190,149 +209,6 @@ jobs:
run: |
exit 1
# Shoppy e2e tests run separately using the shared composite action for setup.
e2e-shoppy-tests:
needs: [get-sample-apps-data]
strategy:
matrix:
environment: ["production", "development"]
if: |
!cancelled() && needs.get-sample-apps-data.result == 'success' &&
(github.ref == 'refs/heads/master' || github.base_ref == 'master')
runs-on: ubuntu-22.04
timeout-minutes: 45
name: e2e-sample-app-shoppy-e2e-tests-${{ matrix.environment }}-bundle
env:
MB_EDITION: ee
MB_RUN_MODE: e2e
METASTORE_DEV_SERVER_URL: ${{ secrets.METASTORE_DEV_SERVER_URL }}
CYPRESS_MB_ALL_FEATURES_TOKEN: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
SAMPLE_APP_BRANCH_NAME: ${{ needs.get-sample-apps-data.outputs.branch_name }}
SAMPLE_APP_ENVIRONMENT: ${{ matrix.environment }}
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- name: Prepare Shoppy E2E environment
uses: ./.github/actions/prepare-shoppy-e2e
id: shoppy-prep
with:
commit: ${{ github.event.pull_request.head.sha || github.sha }}
sample-app-branch-name: ${{ needs.get-sample-apps-data.outputs.branch_name }}
sample-app-environment: ${{ matrix.environment }}
staging-mb-all-features-token: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
shoppy-iam-role: ${{ secrets.SHOPPY_IAM_ROLE }}
shoppy-metabase-app-db-s3-bucket: ${{ secrets.SHOPPY_METABASE_APP_DB_S3_BUCKET }}
shoppy-metabase-app-db-s3-key: ${{ secrets.SHOPPY_METABASE_APP_DB_S3_KEY }}
shoppy-metabase-app-db-user: ${{ secrets.SHOPPY_METABASE_APP_DB_USER }}
shoppy-metabase-app-db-database: ${{ secrets.SHOPPY_METABASE_APP_DB_DATABASE }}
metastore-dev-server-url: ${{ secrets.METASTORE_DEV_SERVER_URL }}
shoppy-datadog-application-id: ${{ secrets.SHOPPY_METABASE_APP_DATADOG_APPLICATION_ID }}
shoppy-datadog-client-token: ${{ secrets.SHOPPY_METABASE_APP_DATADOG_CLIENT_TOKEN }}
- name: Run e2e tests for Shoppy
id: run-e2e-tests
if: ${{ steps.shoppy-prep.outputs.sample_app_branch_exists == 'true' }}
continue-on-error: true
env:
CYPRESS_BROWSER: ${{ steps.shoppy-prep.outputs.chrome-path }}
run: node e2e/runner/run_cypress_ci.js shoppy-e2e
- name: Upload Cypress Artifacts upon failure
uses: actions/upload-artifact@v4
if: ${{ steps.shoppy-prep.outputs.sample_app_branch_exists == 'true' && steps.run-e2e-tests.outcome != 'success' }}
with:
name: cypress-recording-sample-app-shoppy-e2e-${{ matrix.environment }}-latest
path: |
./e2e/tmp/shoppy/cypress
if-no-files-found: ignore
- name: Fail job if tests failed
if: ${{ steps.shoppy-prep.outputs.sample_app_branch_exists == 'true' && steps.run-e2e-tests.outcome != 'success' }}
run: |
exit 1
# Synthetic monitoring tests run only on master merges, in parallel with the regular e2e tests.
# They use the shoppy production environment and include performance measurement tests.
e2e-shoppy-synthetic-tests:
needs: [get-sample-apps-data]
if: |
!cancelled() && needs.get-sample-apps-data.result == 'success' &&
github.event_name == 'push' &&
github.ref == 'refs/heads/master'
runs-on: ubuntu-22.04
timeout-minutes: 45
name: e2e-shoppy-synthetic-monitoring-tests
env:
MB_EDITION: ee
MB_RUN_MODE: e2e
METASTORE_DEV_SERVER_URL: ${{ secrets.METASTORE_DEV_SERVER_URL }}
CYPRESS_MB_ALL_FEATURES_TOKEN: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
SAMPLE_APP_BRANCH_NAME: ${{ needs.get-sample-apps-data.outputs.branch_name }}
SAMPLE_APP_ENVIRONMENT: production
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Prepare Shoppy E2E environment
uses: ./.github/actions/prepare-shoppy-e2e
id: shoppy-prep
with:
commit: ${{ github.event.pull_request.head.sha || github.sha }}
sample-app-branch-name: ${{ needs.get-sample-apps-data.outputs.branch_name }}
sample-app-environment: production
staging-mb-all-features-token: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
shoppy-iam-role: ${{ secrets.SHOPPY_IAM_ROLE }}
shoppy-metabase-app-db-s3-bucket: ${{ secrets.SHOPPY_METABASE_APP_DB_S3_BUCKET }}
shoppy-metabase-app-db-s3-key: ${{ secrets.SHOPPY_METABASE_APP_DB_S3_KEY }}
shoppy-metabase-app-db-user: ${{ secrets.SHOPPY_METABASE_APP_DB_USER }}
shoppy-metabase-app-db-database: ${{ secrets.SHOPPY_METABASE_APP_DB_DATABASE }}
metastore-dev-server-url: ${{ secrets.METASTORE_DEV_SERVER_URL }}
shoppy-datadog-application-id: ${{ secrets.SHOPPY_METABASE_APP_DATADOG_APPLICATION_ID }}
shoppy-datadog-client-token: ${{ secrets.SHOPPY_METABASE_APP_DATADOG_CLIENT_TOKEN }}
- name: Get merged PR number
if: ${{ steps.shoppy-prep.outputs.sample_app_branch_exists == 'true' }}
id: get-merged-pr-number
run: |
PR=$(gh api repos/${{ github.repository }}/commits/${{ github.sha }}/pulls --jq '.[0].number' 2>/dev/null || echo "")
if [ -z "$PR" ] || [ "$PR" = "null" ]; then
echo "number=" >> "$GITHUB_OUTPUT"
else
echo "number=$PR" >> "$GITHUB_OUTPUT"
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Run synthetic monitoring tests
id: run-e2e-tests
if: ${{ steps.shoppy-prep.outputs.sample_app_branch_exists == 'true' }}
continue-on-error: true
env:
CYPRESS_BROWSER: ${{ steps.shoppy-prep.outputs.chrome-path }}
PR_NUMBER: ${{ steps.get-merged-pr-number.outputs.number }}
INCLUDE_SYNTHETIC_MONITORING_TESTS: "true"
run: node e2e/runner/run_cypress_ci.js shoppy-e2e
- name: Upload Cypress Artifacts upon failure
uses: actions/upload-artifact@v4
if: ${{ steps.shoppy-prep.outputs.sample_app_branch_exists == 'true' && steps.run-e2e-tests.outcome != 'success' }}
with:
name: cypress-recording-shoppy-synthetic-tests-latest
path: |
./e2e/tmp/shoppy/cypress
if-no-files-found: ignore
- name: Fail job if tests failed
if: ${{ steps.shoppy-prep.outputs.sample_app_branch_exists == 'true' && steps.run-e2e-tests.outcome != 'success' }}
run: |
exit 1
# The job checks integration of the Embedding SDK with consumer's apps that use different frameworks/bundlers,
# or when we want to test some tricky integration cases like conflicting types.
# The job
@@ -355,7 +231,16 @@ jobs:
timeout-minutes: 45
name: e2e-host-app-${{ matrix.test_suite }}-tests-${{ matrix.environment }}-bundle
env:
MB_EDITION: ee
DISPLAY: ""
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number || '' }}
HASH: ${{ github.event.pull_request.head.sha || github.sha }}-${{ github.run_attempt }}
MB_RUN_MODE: e2e
METASTORE_DEV_SERVER_URL: ${{ secrets.METASTORE_DEV_SERVER_URL }}
CYPRESS_MB_ALL_FEATURES_TOKEN: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
TZ: US/Pacific # to make node match the instance tz
CYPRESS_CI: true
HOST_APP_ENVIRONMENT: ${{ matrix.environment }}
permissions:
id-token: write
@@ -366,8 +251,7 @@ jobs:
- 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"
@@ -379,13 +263,17 @@ jobs:
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Prepare Cypress environment
id: cypress-prep
uses: ./.github/actions/prepare-cypress
- name: Prepare JDK 21
uses: actions/setup-java@v4
with:
java-version: 21
distribution: "temurin"
- name: Run Metabase
run: node e2e/runner/run_cypress_ci.js start

View File

@@ -23,7 +23,7 @@ jobs:
# Remove the matchers to not report false positives on every PR
# unrelated to embedding (EMB-765)
echo "::remove-matcher owner=tsc::"
bun run build-embedding-sdk-package
yarn build-embedding-sdk-package
- name: Prepare Embedding SDK artifact
uses: actions/upload-artifact@v4
with:

View File

@@ -23,10 +23,6 @@ on:
description: "Whether to run host and sample apps compatibility tests."
required: true
type: boolean
bundle-size-check:
description: "Whether to run bundle size check."
required: true
type: boolean
skip:
description: Whether to skip running tests due to one of the artifacts failing to build.
required: true
@@ -51,7 +47,7 @@ jobs:
name: embedding-sdk-${{ github.event.pull_request.head.sha || github.sha }}
path: ${{ github.workspace }}/resources/embedding-sdk
- name: Run frontend embedding SDK snippets type check
run: bun run embedding-sdk:cli-snippets:type-check
run: yarn run embedding-sdk:cli-snippets:type-check
embedding-sdk-docs-snippets-type-check:
if: inputs.docs-snippets-type-check && !inputs.skip
@@ -67,7 +63,7 @@ jobs:
name: embedding-sdk-${{ github.event.pull_request.head.sha || github.sha }}
path: ${{ github.workspace }}/resources/embedding-sdk
- name: Run frontend embedding SDK snippets type check
run: bun run embedding-sdk:docs-snippets:type-check
run: yarn run embedding-sdk:docs-snippets:type-check
embedding-sdk-api-documentation-generation-validation:
if: inputs.documentation-validation && !inputs.skip
@@ -90,7 +86,7 @@ jobs:
shell: bash
run: |
set -o pipefail
bun run embedding-sdk:docs:generate:pure --emit none 2>&1 \
yarn embedding-sdk:docs:generate:pure --emit none 2>&1 \
| tee /dev/stdout \
| tee >( tail -n 10 \
| sed '1s/^/output<<EOF\n/' \
@@ -102,14 +98,14 @@ jobs:
with:
remove-previous-comment: true
add-new-comment: ${{ steps.docs-generation.outcome == 'failure' && contains(steps.docs-generation.outputs.output, 'not included in the documentation') }}
message: "📝 Some exported types from `@metabase/embedding-sdk-react` reference other types that are not exported, i.e. internal types.<br><br>The internal type reference breaks API documentation generation for the SDK and also may unexpectedly increase the output size of the type declaration file.<br><br>Please check the CI logs to find the name of such type.<br><br>- If a type should be publicly available in the SDK, add a description to it or a TypeDoc annotation and export it from SDK.<br>- If a type is internal and should _not_ be publicly exposed, refactor types to prevent referencing other types that are not exported from SDK.<br><br>You can run `bun run embedding-sdk:docs:generate` locally to verify that the issue has been resolved."
message: "📝 Some exported types from `@metabase/embedding-sdk-react` reference other types that are not exported, i.e. internal types.<br><br>The internal type reference breaks API documentation generation for the SDK and also may unexpectedly increase the output size of the type declaration file.<br><br>Please check the CI logs to find the name of such type.<br><br>- If a type should be publicly available in the SDK, add a description to it or a TypeDoc annotation and export it from SDK.<br>- If a type is internal and should _not_ be publicly exposed, refactor types to prevent referencing other types that are not exported from SDK.<br><br>You can run `yarn embedding-sdk:docs:generate` locally to verify that the issue has been resolved."
include-log: true
- name: Comment on PR if 'Failed to resolve link' error happens
uses: ./.github/actions/notify-pull-request
with:
remove-previous-comment: true
add-new-comment: ${{ steps.docs-generation.outcome == 'failure' && contains(steps.docs-generation.outputs.output, 'Failed to resolve link') }}
message: "📝 A typedoc `{@link <TYPE_NAME>}` annotation is found that can't be resolved.<br><br>Most likely it happens when the referenced type is not exported. Please check the CI logs to find the name of such type.<br><br>- If a referenced in `{@link}` type should be publicly available in the SDK, add a description to it or a TypeDoc annotation and export it from SDK.<br>- If a type is internal and should _not_ be publicly exposed, change types to prevent unexported type reference in `{@link}` annotation.<br><br>You can run `bun run embedding-sdk:docs:generate` locally to verify that the issue has been resolved."
message: "📝 A typedoc `{@link <TYPE_NAME>}` annotation is found that can't be resolved.<br><br>Most likely it happens when the referenced type is not exported. Please check the CI logs to find the name of such type.<br><br>- If a referenced in `{@link}` type should be publicly available in the SDK, add a description to it or a TypeDoc annotation and export it from SDK.<br>- If a type is internal and should _not_ be publicly exposed, change types to prevent unexported type reference in `{@link}` annotation.<br><br>You can run `yarn embedding-sdk:docs:generate` locally to verify that the issue has been resolved."
include-log: true
- name: Fail Job
if: ${{ steps.docs-generation.outcome == 'failure' }}
@@ -128,52 +124,8 @@ jobs:
uses: ./.github/workflows/embedding-sdk-host-sample-apps-tests.yml
secrets: inherit
embedding-sdk-bundle-size-check:
if: inputs.bundle-size-check
continue-on-error: true
runs-on: ubuntu-22.04
timeout-minutes: 20
env:
BUNDLE_SIZE_THRESHOLD_PERCENT: 2
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/sdk-bundle-size-check
id: compare
with:
edition: ee
base-ref-commit: ${{ github.event.pull_request.base.sha }}
threshold-percent: ${{ env.BUNDLE_SIZE_THRESHOLD_PERCENT }}
- name: Comment on PR if bundle size has increased
uses: ./.github/actions/notify-pull-request
if: success() && steps.compare.outputs.status == 'increased'
with:
remove-previous-comment: true
add-new-comment: true
message: "⚠️ Embedding SDK bundle size increased by ${{ steps.compare.outputs.size_change_percent }}% compared to the ${{ github.base_ref }} branch (threshold: ${{ env.BUNDLE_SIZE_THRESHOLD_PERCENT }}%). Please contact Embedding team."
include-log: false
- name: Send notification to Slack via Zapier
if: success() && steps.compare.outputs.status == 'increased'
shell: bash
run: |
curl -s -X POST "${{ secrets.EMBEDDING_ZAPIER_WEBHOOK_URL }}" \
-d '⚠️ Embedding SDK bundle size increased by ${{ steps.compare.outputs.size_change_percent }}% (threshold: ${{ env.BUNDLE_SIZE_THRESHOLD_PERCENT }}%)
PR #${{ github.event.pull_request.number }} ${{ github.event.pull_request.title }} (${{ github.event.pull_request.html_url }})'
- name: Comment on PR if bundle size has decreased
uses: ./.github/actions/notify-pull-request
if: success() && steps.compare.outputs.status == 'decreased'
with:
remove-previous-comment: true
add-new-comment: true
message: "🎉 Embedding SDK bundle size decreased by ${{ steps.compare.outputs.size_change_percent }}% compared to the ${{ github.base_ref }} branch. Nice work!"
include-log: false
embedding-sdk-tests-result:
needs:
- embedding-sdk-bundle-size-check
- embedding-sdk-cli-snippets-type-check
- embedding-sdk-docs-snippets-type-check
- embedding-sdk-api-documentation-generation-validation

View File

@@ -44,12 +44,12 @@ jobs:
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Build cljs
run: bun run build:cljs
run: yarn build:cljs
- name: Stress-test ${{ github.event.inputs.spec }} ${{ github.event.inputs.burn_in }} times
run: |
for i in {1..${{ github.event.inputs.burn_in }}}; do
bun run jest --runInBand --ci --silent --no-cache \
--testPathPatterns '${{ github.event.inputs.spec }}' \
yarn jest --runInBand --ci --silent --no-cache \
--testPathPattern '${{ github.event.inputs.spec }}' \
--testNamePattern '${{ github.event.inputs.grep }}'
if [[ "$?" != 0 ]]; then
echo "Failed after $i attempts" && break;

View File

@@ -46,15 +46,15 @@ jobs:
key: ${{ steps.get-cache-keys.outputs.eslint-cache-key }}
- name: Compile CLJS
run: bun run build-pure:cljs
run: yarn build-pure:cljs
- name: Find CSS variables that are used but never defined
run: bun run find-never-defined-css-variables
run: yarn find-never-defined-css-variables
- name: Run Prettier formatting linter
run: bun run lint-prettier-pure
run: yarn run lint-prettier-pure
- name: Run ESLint linter
run: bun run lint-eslint-pure
run: yarn run lint-eslint-pure
- name: Run CSS linter
run: bun run lint-css
run: yarn run lint-css
fe-type-check:
if: ${{ !inputs.skip }}
@@ -66,9 +66,9 @@ jobs:
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- run: bun run build-pure:cljs
- run: yarn build-pure:cljs
name: Compile CLJS
- run: bun run type-check-pure
- run: yarn type-check-pure
name: Check types
fe-tests-unit:
@@ -90,8 +90,7 @@ jobs:
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Run frontend unit tests
id: fe-tests
run: bun run test-unit --silent --shard=${{ matrix.shard }}/${{ env.SHARDS }}
run: yarn run test-unit --silent --shard=${{ matrix.shard }}/${{ env.SHARDS }}
# Trunk Quarantine controls the final state of the run in the CI!
# Even if this step fails we let it pass, and then `trunk-io/analytics-uploader` determines
# whether to fail or to pass the whole job, based on the quarantine list.
@@ -107,7 +106,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.fe-tests.outcome }}
fe-tests-timezones:
if: ${{ !inputs.skip }}
@@ -120,7 +118,7 @@ jobs:
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Run frontend timezones tests
run: bun run test-timezones
run: yarn run test-timezones
fe-tests-result:
needs:

56
.github/workflows/fuzzing.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: Fuzzing
on:
workflow_dispatch:
push:
branches:
- "master"
- "release-**"
paths:
- "frontend/src/metabase/querying/expressions/**"
pull_request:
paths:
- "frontend/src/metabase/querying/expressions/**"
jobs:
fe-fuzz-lexifier:
runs-on: ubuntu-22.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- run: yarn test-unit frontend/src/metabase/querying/expressions/pratt/fuzz.lexifier.unit.spec.ts
env:
MB_FUZZ: 1
name: Run fuzz testing on the lexifier
fe-fuzz-compiler:
runs-on: ubuntu-22.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- run: yarn test-unit frontend/src/metabase/querying/expressions/fuzz.compiler.unit.spec.ts
env:
MB_FUZZ: 1
name: Run fuzz testing on the custom expression compiler
fe-fuzz-string-quoting:
runs-on: ubuntu-22.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- run: yarn test-unit frontend/src/metabase/querying/expressions/fuzz.string.unit.spec.ts
env:
MB_FUZZ: 1
name: Run fuzz testing on the custom expression string quoter

View File

@@ -16,7 +16,7 @@ on:
description: 'Seconds (Number of seconds after which next iteration will not start)'
test:
description: 'Test or namespace to run'
default: metabase.query-processor.generative-test/execution-with-cards-test
default: metabase.query-processor-test.generative-test/execution-with-cards-test
ref:
description: 'Ref to run generative tests on'
default: qgen-ms2

View File

@@ -1,51 +0,0 @@
name: JS Stats Update
on:
workflow_dispatch:
schedule:
- cron: 40 1 * * 4 # every Thursday at 1:40
jobs:
js-stats:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: get JS file count
id: get-js-file-count
run: |
oss=$(find ./frontend/src/metabase -type f -name "*.js" -o -name "*.jsx" | wc -l)
ee=$(find ./enterprise/frontend/src/metabase-enterprise -type f -name "*.js" -o -name "*.jsx" | wc -l)
echo $(($oss+$ee)) javascript files
echo "JS_FILES=$(($oss+$ee))" >> $GITHUB_OUTPUT
- name: Upload count to Stats
uses: actions/github-script@v7
env:
JS_FILES: ${{ steps.get-js-file-count.outputs.JS_FILES }}
API_KEY: ${{ secrets.STATS_API_KEY }}
with:
script: | # js
const { uploadCsvToMb } = require('${{ github.workspace }}/.github/scripts/csv-to-mb.js');
console.log("JS_FILES", Number(process.env.JS_FILES));
const data = [
{
'Date': new Date().toISOString().slice(0, 10), // YYYY-MM-DD
'Js files': Number(process.env.JS_FILES),
}
];
uploadCsvToMb({
baseUrl: 'https://stats.metabase.com',
tableId: 73270,
jsonData: data,
mode: 'append',
}).then(() => {
console.log('Data uploaded successfully');
}).catch((error) => {
console.error('Error uploading data:', error);
process.exit(1);
});

View File

@@ -1,194 +0,0 @@
name: Loki Visual Stress Test Flake Fix
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'frontend/**/*.stories.tsx'
- 'enterprise/frontend/**/*.stories.tsx'
workflow_dispatch:
inputs:
story_files:
description: "Comma-separated story file paths relative to repo root"
type: string
required: true
burn_in:
description: "Number of times to run the test (e.g. 50)"
type: string
required: true
default: "50"
jobs:
detect-changed-stories:
name: Detect changed stories
runs-on: ubuntu-22.04
timeout-minutes: 5
outputs:
story_files: ${{ steps.detect.outputs.story_files }}
should_run: ${{ steps.detect.outputs.should_run }}
steps:
- name: Detect changed story files
id: detect
env:
GH_TOKEN: ${{ github.token }}
run: |
validate_paths() {
local files="$1"
for file in $(echo "$files" | tr ',' '\n'); do
file=$(echo "$file" | xargs) # trim whitespace
if [[ -z "$file" ]]; then
continue
fi
if [[ ! "$file" =~ ^(frontend/|enterprise/frontend/).+\.stories\. ]]; then
echo "::error::Invalid story path: $file"
echo "Paths must start with 'frontend/' or 'enterprise/frontend/' and contain '.stories.'"
return 1
fi
done
return 0
}
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
FILES=$(echo "${{ inputs.story_files }}" | tr ',' '\n' | xargs -n1 | paste -sd',')
if ! validate_paths "$FILES"; then
exit 1
fi
echo "story_files=$FILES" >> $GITHUB_OUTPUT
echo "should_run=true" >> $GITHUB_OUTPUT
exit 0
fi
FILES=$( \
gh pr view ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --json files --jq '.files[].path' \
| grep '\.stories\.tsx$' \
| xargs -n1 \
| sort -u \
| paste -sd',' \
)
if [[ -z "$FILES" ]]; then
echo "No changed story files found"
echo "should_run=false" >> $GITHUB_OUTPUT
exit 0
fi
if ! validate_paths "$FILES"; then
exit 1
fi
echo "Changed story files: $FILES"
echo "story_files=$FILES" >> $GITHUB_OUTPUT
echo "should_run=true" >> $GITHUB_OUTPUT
workflow-summary:
name: Summary
needs: detect-changed-stories
if: needs.detect-changed-stories.outputs.should_run == 'true'
runs-on: ubuntu-22.04
timeout-minutes: 5
steps:
- name: Generate workflow summary
run: |
echo '## Loki Stress Test' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '**Branch:** `${{ github.head_ref || github.ref_name }}`' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '**Story files:**' >> $GITHUB_STEP_SUMMARY
echo '${{ needs.detect-changed-stories.outputs.story_files }}' | tr ',' '\n' | while read -r file; do
echo "- \`$file\`" >> $GITHUB_STEP_SUMMARY
done
echo '' >> $GITHUB_STEP_SUMMARY
echo '**Burn-in:** ${{ inputs.burn_in || 50 }} runs' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo 'Triggered by PR #${{ github.event.pull_request.number }} from @${{ github.event.pull_request.user.login }}' >> $GITHUB_STEP_SUMMARY
else
echo 'Triggered manually by @${{ github.actor }}' >> $GITHUB_STEP_SUMMARY
fi
stress-test-loki:
needs: detect-changed-stories
if: needs.detect-changed-stories.outputs.should_run == 'true'
runs-on: ubuntu-latest
timeout-minutes: 60
name: Stress test Loki visual tests
services:
docker:
image: docker:19.03.12
options: --privileged
ports:
- 2376:2376
env:
DOCKER_TLS_CERTDIR: /certs
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /tmp/docker-certs:/certs/client
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Prepare frontend environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Compile CLJS
run: NODE_ENV=development bun run build-pure:cljs
- name: Build Storybook
run: bun run build-storybook
env:
STORYBOOK_STORIES_FILTER: ${{ needs.detect-changed-stories.outputs.story_files }}
- name: Stress-test Loki
run: |
BURN_IN=${{ inputs.burn_in || '50' }}
STORY_FILES='${{ needs.detect-changed-stories.outputs.story_files }}'
echo "Story files: $STORY_FILES"
echo "Running Loki $BURN_IN times"
echo ""
FAILED=0
for i in $(seq 1 $BURN_IN); do
echo "=========================================="
echo "Run $i of $BURN_IN"
echo "=========================================="
if bun run test-visual:loki \
--reactUri file:./storybook-static \
--verboseRenderer; then
echo "✅ Run $i passed"
else
echo "❌ Run $i FAILED"
FAILED=$((FAILED + 1))
mkdir -p .loki/failures/run-$i
cp -r .loki/difference/* .loki/failures/run-$i/ 2>/dev/null || true
fi
echo ""
done
echo "=========================================="
echo "Summary: $((BURN_IN - FAILED))/$BURN_IN passed, $FAILED failed"
echo "=========================================="
if [ $FAILED -gt 0 ]; then
echo "::error::$FAILED out of $BURN_IN runs failed"
exit 1
fi
- name: Generate Visual Report on Failure
if: failure()
run: bun run test-visual:loki-report
- name: Upload Artifact
if: failure()
uses: actions/upload-artifact@v4
with:
name: loki-stress-test-report
include-hidden-files: true
path: .loki/
if-no-files-found: ignore

View File

@@ -22,10 +22,10 @@ jobs:
uses: ./.github/actions/prepare-backend
- name: Compile CLJS
run: bun run build-pure:cljs
run: yarn build-pure:cljs
- name: Update Loki Snapshots
run: bun run test-visual:loki:ci || bun run test-visual:loki-approve-diff && bun run test-visual:loki-prune
run: yarn test-visual:loki:ci || yarn test-visual:loki-approve-diff && yarn test-visual:loki-prune
- name: Check for changes
id: check-changes

View File

@@ -49,14 +49,14 @@ jobs:
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: Run Loki Visual Tests
run: bun run test-visual:loki:ci
run: yarn test-visual:loki:ci
- name: Generate Visual Report on Failure
if: failure()
run: bun run test-visual:loki-report
run: yarn test-visual:loki-report
- name: Upload Artifact
id: artifact-upload-step

View File

@@ -2,9 +2,7 @@ name: Mage Tests
on:
pull_request:
paths:
- "mage/**"
- "bb.edn"
paths: ["mage/**"]
jobs:
# run mage tests even if the rest of be isnt needed
@@ -15,7 +13,13 @@ jobs:
- uses: actions/checkout@v4
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Check mage runs
run: ./bin/mage -h
- name: Setup Babashka
uses: turtlequeue/setup-babashka@v1.7.0
with:
babashka-version: 1.12.197
- name: Check bb runs
run: bb --version
- name: bb task list
run: bb tasks
- name: Run mage tests
run: ./bin/mage -test
run: bb ./mage/test/mage/runtests.clj

View File

@@ -30,9 +30,9 @@ jobs:
needs: check-label
if: needs.check-label.outputs.should-run == 'true'
runs-on: ubuntu-22.04
timeout-minutes: 40
timeout-minutes: 60
env:
CI: "true"
CI: 'true'
DRIVERS: postgres
MB_DB_TYPE: postgres
MB_DB_PORT: 5432
@@ -53,11 +53,10 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Test Postgres App DB
id: test-driver
uses: ./.github/actions/test-driver
with:
build-static-viz: true
junit-name: "minimal-be-tests-postgres-ee"
junit-name: 'minimal-be-tests-postgres-ee'
test-args: >-
:only '["test" "enterprise/backend/test"]'
:exclude-tags '[:mb/driver-tests :mb/transforms-python-test]'
@@ -74,16 +73,15 @@ 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 }}
# Postgres driver tests
postgres-driver-test:
needs: check-label
if: needs.check-label.outputs.should-run == 'true'
runs-on: ubuntu-latest
timeout-minutes: 40
timeout-minutes: 60
env:
CI: "true"
CI: 'true'
DRIVERS: postgres
MB_DB_TYPE: postgres
MB_DB_PORT: 5432
@@ -104,10 +102,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Test Postgres Driver
id: test-driver
uses: ./.github/actions/test-driver
with:
junit-name: "minimal-be-tests-postgres-driver-ee"
junit-name: 'minimal-be-tests-postgres-driver-ee'
test-args: >-
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/transforms-python-test]
@@ -122,7 +119,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 }}
# Frontend unit tests
frontend-unit-tests:
@@ -141,8 +137,7 @@ jobs:
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Run frontend unit tests
id: run-tests
run: bun run test-unit --silent
run: yarn run test-unit --silent
continue-on-error: true
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
@@ -155,7 +150,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 }}
# Summary job
minimal-tests-result:

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