Compare commits
560 Commits
python-sec
...
x-version-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8500d40793 | ||
|
|
1d0a9828c6 | ||
|
|
f21779defa | ||
|
|
bbe3913842 | ||
|
|
d96e513272 | ||
|
|
e0659459d4 | ||
|
|
6139e40aa5 | ||
|
|
1e651cafbd | ||
|
|
71f4f2d1f1 | ||
|
|
bc138d50f6 | ||
|
|
db03762557 | ||
|
|
4b2499407f | ||
|
|
48f88cef56 | ||
|
|
ce8a91b935 | ||
|
|
d02755f3e3 | ||
|
|
a8829c874b | ||
|
|
3d463a5af0 | ||
|
|
d83702d8fc | ||
|
|
e76e9a4b3f | ||
|
|
5c92fc1925 | ||
|
|
04055a8dc9 | ||
|
|
df158f6bf4 | ||
|
|
64fe7ab62c | ||
|
|
0678ce7b4a | ||
|
|
64ad18df75 | ||
|
|
4a954de0d7 | ||
|
|
6cf167a0b7 | ||
|
|
22b309b5cf | ||
|
|
db0f4772b5 | ||
|
|
b3b41e6e78 | ||
|
|
96c3eef65e | ||
|
|
3694add7a5 | ||
|
|
de81cf1e69 | ||
|
|
821225a1a1 | ||
|
|
b92ea28d8b | ||
|
|
4c1c35019d | ||
|
|
3c52cee848 | ||
|
|
09e0b94174 | ||
|
|
23da6ce33e | ||
|
|
1a927ef93e | ||
|
|
86e64f550d | ||
|
|
cb32c606ae | ||
|
|
9e6776f004 | ||
|
|
be2c84c7a7 | ||
|
|
1c1c32ccf9 | ||
|
|
cf6856f0fb | ||
|
|
1990beb409 | ||
|
|
b609770fd6 | ||
|
|
44a932b788 | ||
|
|
758a1da37b | ||
|
|
f0c4ed2503 | ||
|
|
1ce579459f | ||
|
|
fa9301038f | ||
|
|
fe176795c2 | ||
|
|
c2b1490c71 | ||
|
|
03e232ef84 | ||
|
|
3be69e496c | ||
|
|
6f4182906c | ||
|
|
cb09342f8c | ||
|
|
2e5e1d3c65 | ||
|
|
7b69b2642e | ||
|
|
ccf8e80c65 | ||
|
|
93cd8a0c3a | ||
|
|
2c124ee7ee | ||
|
|
06cb748753 | ||
|
|
35889aaa84 | ||
|
|
95d079b8c7 | ||
|
|
41c14269eb | ||
|
|
465943a93b | ||
|
|
740ab6d8c9 | ||
|
|
2e09de18f1 | ||
|
|
0c57f8124c | ||
|
|
4a92471b97 | ||
|
|
c52b657556 | ||
|
|
dc6dad7234 | ||
|
|
d02df13cd9 | ||
|
|
5d64170063 | ||
|
|
56a7c4a8cd | ||
|
|
3e6b72d1c9 | ||
|
|
dbd3235414 | ||
|
|
950838c07d | ||
|
|
23ca6062df | ||
|
|
8a2635be18 | ||
|
|
d875b2d2d7 | ||
|
|
d93dbe34aa | ||
|
|
435255a0ff | ||
|
|
7f1c26999e | ||
|
|
07cdf70b69 | ||
|
|
5faa0e05b4 | ||
|
|
c4e95ce68c | ||
|
|
a59cd05bc4 | ||
|
|
979a1f4d08 | ||
|
|
f1f4ee6cba | ||
|
|
896099c10d | ||
|
|
f07ce113be | ||
|
|
9b65b3ff60 | ||
|
|
0e9ccd4b35 | ||
|
|
61fcfa12b4 | ||
|
|
a8e6298eb6 | ||
|
|
54144ee009 | ||
|
|
b74eba7337 | ||
|
|
009b507dae | ||
|
|
9f23db4b88 | ||
|
|
d993e3256e | ||
|
|
eb7328f8e6 | ||
|
|
3fac084240 | ||
|
|
5a8ca2ac12 | ||
|
|
2eb39b00e3 | ||
|
|
62f1388273 | ||
|
|
075b93621b | ||
|
|
fae8f9d907 | ||
|
|
8663f47407 | ||
|
|
b875048d1e | ||
|
|
bf2696c956 | ||
|
|
edc089b69e | ||
|
|
030c0c7c09 | ||
|
|
4696261888 | ||
|
|
80ba029cb3 | ||
|
|
f7fafc175d | ||
|
|
5527f64c45 | ||
|
|
0a7b562b13 | ||
|
|
b1fc7de58b | ||
|
|
125e144c55 | ||
|
|
8739d47821 | ||
|
|
1708df6fe9 | ||
|
|
109161bcd7 | ||
|
|
157485655b | ||
|
|
4bb9956fbb | ||
|
|
e45d72b6fc | ||
|
|
9063854840 | ||
|
|
d9c49e3894 | ||
|
|
20318ff15e | ||
|
|
5407ec6316 | ||
|
|
a02c58b444 | ||
|
|
524e10902e | ||
|
|
b82083a20b | ||
|
|
234cd8650b | ||
|
|
94a7db3a20 | ||
|
|
1c5813a3d4 | ||
|
|
79b253981e | ||
|
|
aa0fb62488 | ||
|
|
8a6e6c98d3 | ||
|
|
ab1a990400 | ||
|
|
a137bc3b78 | ||
|
|
f2ddea054c | ||
|
|
0d17572350 | ||
|
|
97ddbf8241 | ||
|
|
1884cea5c5 | ||
|
|
b375f47020 | ||
|
|
d45ed12d09 | ||
|
|
bd22efcd81 | ||
|
|
bf0b950a6e | ||
|
|
a5812b0a74 | ||
|
|
7d25281104 | ||
|
|
b625cebc20 | ||
|
|
aa3519f63e | ||
|
|
597eef8401 | ||
|
|
9539d33d39 | ||
|
|
6369ea3615 | ||
|
|
ef0b741edf | ||
|
|
7a1388d2b3 | ||
|
|
427c9739de | ||
|
|
c0c12b45a7 | ||
|
|
74cd4a75bd | ||
|
|
72b83990e7 | ||
|
|
3374dbbade | ||
|
|
b17128b3f4 | ||
|
|
b5ca53f2ae | ||
|
|
a6c08f22de | ||
|
|
17a3c2fa4b | ||
|
|
f9fdb487a0 | ||
|
|
cb2bf685ce | ||
|
|
17e3e282ad | ||
|
|
10d4c63540 | ||
|
|
b315fa36aa | ||
|
|
c529dc414e | ||
|
|
e4db7c7aa7 | ||
|
|
712da34206 | ||
|
|
a6510769be | ||
|
|
d96a373dd2 | ||
|
|
ec62ca97ca | ||
|
|
d2df2f7a85 | ||
|
|
3785cd96b3 | ||
|
|
bbe3c2b509 | ||
|
|
43a33dc9a8 | ||
|
|
0d11d471e4 | ||
|
|
b72b1bba53 | ||
|
|
31c596ea5b | ||
|
|
a7c7994a58 | ||
|
|
b9cda3ebec | ||
|
|
c0a9ec4a6c | ||
|
|
f0dc972c68 | ||
|
|
c94c1b3714 | ||
|
|
80f17de84b | ||
|
|
fd35df566a | ||
|
|
c24f1c8c66 | ||
|
|
6488241a39 | ||
|
|
a36b09ac66 | ||
|
|
eb93a0a906 | ||
|
|
68bb2e222d | ||
|
|
4233f7e4dd | ||
|
|
9935851e9a | ||
|
|
37aa580e95 | ||
|
|
2756592a84 | ||
|
|
31467e021f | ||
|
|
28aa9ebf76 | ||
|
|
a942e62612 | ||
|
|
6ad78eb844 | ||
|
|
f884329d9a | ||
|
|
b31cc0523f | ||
|
|
9699dfba74 | ||
|
|
074aaf759b | ||
|
|
e935dbf5e5 | ||
|
|
5bb8d13db5 | ||
|
|
a15602e4b3 | ||
|
|
e9ed0e5c7c | ||
|
|
afb058466e | ||
|
|
e5e4380e0b | ||
|
|
e741e08632 | ||
|
|
d79e1b96c3 | ||
|
|
7f89aa0f6d | ||
|
|
a9a5332467 | ||
|
|
6f582d5091 | ||
|
|
208e3124e0 | ||
|
|
5598b8fb2b | ||
|
|
b1d8c28162 | ||
|
|
10daca1705 | ||
|
|
ddf5113f44 | ||
|
|
c0883dabf4 | ||
|
|
b13c40dcd2 | ||
|
|
0ab3cad804 | ||
|
|
138c32ef2b | ||
|
|
d829cb427b | ||
|
|
156674cc87 | ||
|
|
f240458ae1 | ||
|
|
fae2d4f3f8 | ||
|
|
f68b82cfb7 | ||
|
|
a0183f7a05 | ||
|
|
ed69947170 | ||
|
|
f6706967cd | ||
|
|
46152d32b1 | ||
|
|
cae120dc51 | ||
|
|
482d3d922d | ||
|
|
4a226c73b4 | ||
|
|
c44b55ec89 | ||
|
|
2dd7e98f92 | ||
|
|
34c6e8d86c | ||
|
|
bf0b3476a5 | ||
|
|
50ba384862 | ||
|
|
17a6925328 | ||
|
|
6dfd5b92b2 | ||
|
|
1c5a73a375 | ||
|
|
54d4e39d2c | ||
|
|
eb7cd25703 | ||
|
|
01b2c9daaa | ||
|
|
afc45a4223 | ||
|
|
bbaf280f47 | ||
|
|
bcd2ae22ef | ||
|
|
a40ff858fa | ||
|
|
a11a0aa705 | ||
|
|
444566ac52 | ||
|
|
0ed73243f5 | ||
|
|
fb364c3246 | ||
|
|
84e9631ca9 | ||
|
|
1415cc3a66 | ||
|
|
acee2b5283 | ||
|
|
582ff0270e | ||
|
|
cfda6bac53 | ||
|
|
73ca985f57 | ||
|
|
4bd6fd02e2 | ||
|
|
64096645df | ||
|
|
d17ea7b418 | ||
|
|
c483dfed98 | ||
|
|
2bcf550883 | ||
|
|
2bf33cd22c | ||
|
|
313a67a5fe | ||
|
|
c7c87ada6b | ||
|
|
ac776546c8 | ||
|
|
2247b181ea | ||
|
|
40646ac0cd | ||
|
|
dab88589f9 | ||
|
|
d365676a67 | ||
|
|
62a365b09d | ||
|
|
e4b89f674d | ||
|
|
f650ea23e3 | ||
|
|
bce8816f3c | ||
|
|
1b4080d463 | ||
|
|
808683fd86 | ||
|
|
184999bc50 | ||
|
|
da4778a7a4 | ||
|
|
bf9e3cf701 | ||
|
|
bb6442fbc5 | ||
|
|
fe63f77f8c | ||
|
|
8a1544e6c0 | ||
|
|
1d966f1396 | ||
|
|
0976f56ca2 | ||
|
|
c0d04a547e | ||
|
|
864b49ae03 | ||
|
|
6755cb40e5 | ||
|
|
efb7a83033 | ||
|
|
3ecdb41659 | ||
|
|
f948f076b0 | ||
|
|
46ab29e968 | ||
|
|
b31f1906f6 | ||
|
|
f924d7feb0 | ||
|
|
a420fbf064 | ||
|
|
3b5c370d37 | ||
|
|
c806ee4b0b | ||
|
|
3782c65f7a | ||
|
|
cd1d82cccf | ||
|
|
7fcb6731a9 | ||
|
|
712cc75115 | ||
|
|
176f1714fe | ||
|
|
70bd4b8feb | ||
|
|
8db4dee438 | ||
|
|
981363c75f | ||
|
|
25ede8d131 | ||
|
|
ab84f1723e | ||
|
|
0bc8b12bc6 | ||
|
|
e1343374c4 | ||
|
|
5e0d605170 | ||
|
|
79d5c0d4bc | ||
|
|
0ee3e33a61 | ||
|
|
7e65036be6 | ||
|
|
7f07244f9a | ||
|
|
cfd14e0db1 | ||
|
|
23afcd677e | ||
|
|
94af114408 | ||
|
|
333475ab8b | ||
|
|
503191e176 | ||
|
|
d53c1d347a | ||
|
|
9d68441cdf | ||
|
|
672c84308b | ||
|
|
6eef92192b | ||
|
|
9509a192c5 | ||
|
|
dcc0857c05 | ||
|
|
9142ccd068 | ||
|
|
94cc33c19b | ||
|
|
51eb983f60 | ||
|
|
346fa4aa05 | ||
|
|
6926d0ed4e | ||
|
|
5c1e233360 | ||
|
|
b4cb1c55b5 | ||
|
|
0b34521c4b | ||
|
|
ca6da001b9 | ||
|
|
3f6b0790dd | ||
|
|
f6d76760d2 | ||
|
|
733a4f876f | ||
|
|
650cb8abd1 | ||
|
|
c763638b25 | ||
|
|
9c8636136f | ||
|
|
e54d946007 | ||
|
|
94bd9d066f | ||
|
|
32b6e125c6 | ||
|
|
1e17dac385 | ||
|
|
5d65943889 | ||
|
|
ed5b8a9600 | ||
|
|
81d77e3413 | ||
|
|
02ce4fc023 | ||
|
|
c42ac2aac2 | ||
|
|
d7cbb20436 | ||
|
|
c747a8b193 | ||
|
|
e8a60fd531 | ||
|
|
fedd87b424 | ||
|
|
342b9ddf67 | ||
|
|
693fb67667 | ||
|
|
7b94c0fbdc | ||
|
|
c582f57adf | ||
|
|
65de276f1a | ||
|
|
8047bd26ef | ||
|
|
b2a476dbbe | ||
|
|
8e77833b3b | ||
|
|
0491cc002f | ||
|
|
2110e8c8d0 | ||
|
|
4d97aef6ba | ||
|
|
dafb834464 | ||
|
|
a7a12d0bee | ||
|
|
7fbf305894 | ||
|
|
ba500320c7 | ||
|
|
87c1bbdc50 | ||
|
|
fbb8d252f6 | ||
|
|
d37203ae22 | ||
|
|
0bfaf6af31 | ||
|
|
268863194c | ||
|
|
e365c5aeaf | ||
|
|
2ed0ffb2d6 | ||
|
|
62588151e8 | ||
|
|
ce53241a0b | ||
|
|
5da26b6881 | ||
|
|
564dffaf3b | ||
|
|
5fb46c07ee | ||
|
|
7b6fcb1069 | ||
|
|
b5927af992 | ||
|
|
7e6b56a743 | ||
|
|
78c813bf08 | ||
|
|
b502579ad7 | ||
|
|
b15beb846d | ||
|
|
04cdda021d | ||
|
|
d5dc421476 | ||
|
|
15bbb785bf | ||
|
|
e28326bbbd | ||
|
|
aa77fce3f6 | ||
|
|
0e1ce45d6f | ||
|
|
efcf30e0ec | ||
|
|
3e10af0b27 | ||
|
|
03ee0bec47 | ||
|
|
49f8dd0d64 | ||
|
|
d017bc0f5b | ||
|
|
1d53db91df | ||
|
|
b83aea452b | ||
|
|
39f2cb3b1d | ||
|
|
2dfb1ede25 | ||
|
|
a75fbdbc2d | ||
|
|
7c5a4473b2 | ||
|
|
09b75e6678 | ||
|
|
28586d60a0 | ||
|
|
614f6bf521 | ||
|
|
f1af011f5e | ||
|
|
b20cf8c133 | ||
|
|
722c6bc18d | ||
|
|
21e1f6c88e | ||
|
|
c296807a14 | ||
|
|
9fea80b7c5 | ||
|
|
7eb2bf078a | ||
|
|
7ba6a02410 | ||
|
|
74300cb20c | ||
|
|
337445afea | ||
|
|
51cbcba0f4 | ||
|
|
c87334b94b | ||
|
|
a15e52b401 | ||
|
|
1014179cde | ||
|
|
875709f26b | ||
|
|
03a15af369 | ||
|
|
bf00b5f29c | ||
|
|
7f9a921090 | ||
|
|
82369f46d5 | ||
|
|
2587afbe78 | ||
|
|
bffb349e0b | ||
|
|
ea039adb26 | ||
|
|
c11efb6ab4 | ||
|
|
7e2f185da3 | ||
|
|
2456fab3ae | ||
|
|
368ede9c01 | ||
|
|
16818e5674 | ||
|
|
16e3dc0bad | ||
|
|
c3ec40a378 | ||
|
|
601adc19fb | ||
|
|
0cb8f7c9d7 | ||
|
|
cdcfdd2c43 | ||
|
|
11684ac114 | ||
|
|
c687678f09 | ||
|
|
c117c4bde6 | ||
|
|
0408c343e1 | ||
|
|
6759db19a1 | ||
|
|
e1be32e478 | ||
|
|
9b922e4054 | ||
|
|
cd7e7da811 | ||
|
|
200666cc21 | ||
|
|
69344c4124 | ||
|
|
1b499ced70 | ||
|
|
0119994ea5 | ||
|
|
e2f2584be8 | ||
|
|
9935824876 | ||
|
|
4c8550acf0 | ||
|
|
47459998f4 | ||
|
|
2fa58a08ce | ||
|
|
2638bab3b7 | ||
|
|
d253655a51 | ||
|
|
c2949d4805 | ||
|
|
136e36b8a6 | ||
|
|
cfe7b43948 | ||
|
|
edf76b8069 | ||
|
|
949c4663ed | ||
|
|
c0c43f4d21 | ||
|
|
765116cc8b | ||
|
|
f590798f44 | ||
|
|
120b4830b5 | ||
|
|
7a6be8e99d | ||
|
|
685f193099 | ||
|
|
9bcc0d2e95 | ||
|
|
44a4f7ca7f | ||
|
|
3d8ad85f29 | ||
|
|
dcc3b198cf | ||
|
|
8364a1d334 | ||
|
|
12bc5e501d | ||
|
|
88b69d6b8e | ||
|
|
f65ecb8e3a | ||
|
|
f66380a5b6 | ||
|
|
6b6e54b6fe | ||
|
|
95df601af3 | ||
|
|
0e715fb278 | ||
|
|
75aa3db456 | ||
|
|
735362bc46 | ||
|
|
b282202dfa | ||
|
|
bcfea0d6c8 | ||
|
|
c38455e80b | ||
|
|
dc7151908f | ||
|
|
cec806494e | ||
|
|
85532d68b5 | ||
|
|
b8a8297504 | ||
|
|
988554eb7b | ||
|
|
ae01ada663 | ||
|
|
650a7e6dcc | ||
|
|
02a1e20f87 | ||
|
|
708a36234e | ||
|
|
dea8744fa2 | ||
|
|
8a49a063a3 | ||
|
|
d014973092 | ||
|
|
29d9300a8f | ||
|
|
3110cabfc3 | ||
|
|
277edcd930 | ||
|
|
8bf8853892 | ||
|
|
12c834cc60 | ||
|
|
19cecaba98 | ||
|
|
4055a4f799 | ||
|
|
50786328f2 | ||
|
|
c1a0ca9c71 | ||
|
|
d9b4465228 | ||
|
|
6e46ae0a2c | ||
|
|
d40da31afe | ||
|
|
df414b22f4 | ||
|
|
9d962cae50 | ||
|
|
d5d847f38e | ||
|
|
9d6a8e6d5c | ||
|
|
3e32666e40 | ||
|
|
2c0e5e074b | ||
|
|
b37301453b | ||
|
|
48b42b2faa | ||
|
|
d8731fc411 | ||
|
|
9593db4747 | ||
|
|
490086b84c | ||
|
|
daabf9471f | ||
|
|
a73377914c | ||
|
|
313303df49 | ||
|
|
69792c83b4 | ||
|
|
b7932d8623 | ||
|
|
8ffe92824a | ||
|
|
cccd2f9fb6 | ||
|
|
59c9e995f2 | ||
|
|
2ec3244a5e | ||
|
|
a9bb3d1fea | ||
|
|
3b1e126524 | ||
|
|
6f351996f0 | ||
|
|
d4ab4f01c1 | ||
|
|
041186edbf | ||
|
|
cc3e630ae0 | ||
|
|
842c7fbbaa | ||
|
|
d9327a55ff | ||
|
|
2122ef3b36 | ||
|
|
ecf45b61ec | ||
|
|
bdfa834fc3 | ||
|
|
504f0684b6 | ||
|
|
35223772fa | ||
|
|
4087b3fe78 | ||
|
|
eb3b7c343b | ||
|
|
a7341aabec | ||
|
|
2092b111fc | ||
|
|
653b07a767 | ||
|
|
7b5ea083de | ||
|
|
75f462989c |
@@ -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 lines within definition forms (except pairwise `let`/`cond`)
|
||||
- No blank non-comment lines within definition forms (except pairwise `let`/`cond`)
|
||||
|
||||
## Style Conventions
|
||||
|
||||
|
||||
420
.claude/skills/add-malli-schemas/SKILL.md
Normal file
@@ -0,0 +1,420 @@
|
||||
---
|
||||
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`
|
||||
504
.claude/skills/analytics-events/SKILL.md
Normal file
@@ -0,0 +1,504 @@
|
||||
---
|
||||
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
|
||||
@@ -61,7 +61,7 @@ Use this to scan through changes efficiently:
|
||||
- [ ] Everything `^:private` unless used elsewhere
|
||||
- [ ] No `declare` when avoidable (public functions near end)
|
||||
- [ ] Functions under 20 lines when possible
|
||||
- [ ] No blank lines within definition forms (except pairwise constructs in `let`/`cond`)
|
||||
- [ ] No blank, non-comment lines within definition forms (except pairwise constructs in `let`/`cond`)
|
||||
- [ ] Lines ≤ 120 characters
|
||||
|
||||
### Tests
|
||||
|
||||
@@ -219,7 +219,8 @@
|
||||
;; misc config for built-in linters
|
||||
;;
|
||||
|
||||
:invalid-arity {:skip-args [metabase.lib.util.match/match]} ; TODO (cam): can we fix these?
|
||||
:invalid-arity {:skip-args [metabase.lib.util.match/match
|
||||
metabase.lib.util.match/match-lite]} ; TODO (cam): can we fix these?
|
||||
:refer-all {:level :warning, :exclude [clojure.test]}
|
||||
:unused-referred-var {:exclude {compojure.core [GET DELETE POST PUT]}}
|
||||
|
||||
@@ -238,7 +239,6 @@
|
||||
(metabase.lib.util.match/match)
|
||||
(metabase.lib.util.match/match-one)
|
||||
(metabase.lib.util.match/match-lite)
|
||||
(metabase.lib.util.match/match-lite-recursive)
|
||||
(metabase.lib.util.match/replace)
|
||||
(metabase.lib.util.match/replace-in)
|
||||
(metabase.driver-api.core/match)
|
||||
|
||||
@@ -168,7 +168,8 @@
|
||||
|
||||
api-routes
|
||||
{:team "UX West"
|
||||
:api #{metabase.api-routes.core}
|
||||
:api #{metabase.api-routes.cmd
|
||||
metabase.api-routes.core}
|
||||
:uses :any}
|
||||
|
||||
app-db
|
||||
@@ -263,7 +264,9 @@
|
||||
config
|
||||
events
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
request
|
||||
settings
|
||||
util}}
|
||||
|
||||
@@ -365,7 +368,8 @@
|
||||
|
||||
collections-rest
|
||||
{:team "UX West"
|
||||
:api #{metabase.collections-rest.api}
|
||||
:api #{metabase.collections-rest.api
|
||||
metabase.collections-rest.init}
|
||||
:uses #{api
|
||||
app-db
|
||||
collections
|
||||
@@ -380,6 +384,7 @@
|
||||
queries
|
||||
request
|
||||
revisions
|
||||
settings
|
||||
upload
|
||||
util}}
|
||||
|
||||
@@ -598,7 +603,6 @@
|
||||
metabase.embedding.validation}
|
||||
:uses #{analytics
|
||||
api
|
||||
config
|
||||
premium-features
|
||||
server
|
||||
settings
|
||||
@@ -702,6 +706,7 @@
|
||||
metabase.lib.binning.util
|
||||
metabase.lib.card
|
||||
metabase.lib.computed ; temporarily so that `qp.compile` can use it as lib memoization evolves
|
||||
metabase.lib.convert ; Only so [[metabase.queries.models.card]] can set dynamic vars.
|
||||
metabase.lib.core
|
||||
metabase.lib.equality
|
||||
metabase.lib.field
|
||||
@@ -878,6 +883,7 @@
|
||||
metabase.parameters.custom-values
|
||||
metabase.parameters.dashboard
|
||||
metabase.parameters.field
|
||||
metabase.parameters.field-values
|
||||
metabase.parameters.init
|
||||
metabase.parameters.params
|
||||
metabase.parameters.schema
|
||||
@@ -916,6 +922,7 @@
|
||||
{:team "UX West"
|
||||
:api #{metabase.permissions-rest.api}
|
||||
:uses #{api
|
||||
app-db
|
||||
audit-app
|
||||
events
|
||||
permissions
|
||||
@@ -943,7 +950,8 @@
|
||||
:api #{metabase.premium-features.api
|
||||
metabase.premium-features.core
|
||||
metabase.premium-features.init}
|
||||
:uses #{api
|
||||
:uses #{analytics
|
||||
api
|
||||
app-db
|
||||
classloader
|
||||
config
|
||||
@@ -1005,6 +1013,7 @@
|
||||
collections
|
||||
config
|
||||
driver
|
||||
embedding
|
||||
events
|
||||
lib
|
||||
models
|
||||
@@ -1170,7 +1179,8 @@
|
||||
:api #{metabase.request.core
|
||||
metabase.request.init
|
||||
metabase.request.schema}
|
||||
:uses #{api
|
||||
:uses #{analytics
|
||||
api
|
||||
config
|
||||
embedding
|
||||
permissions ; FIXME
|
||||
@@ -1621,6 +1631,7 @@
|
||||
lib
|
||||
models
|
||||
parameters
|
||||
permissions
|
||||
premium-features
|
||||
query-processor
|
||||
request
|
||||
@@ -1629,7 +1640,6 @@
|
||||
upload
|
||||
util
|
||||
warehouse-schema
|
||||
warehouses-rest
|
||||
xrays}}
|
||||
|
||||
warehouses
|
||||
@@ -2058,6 +2068,7 @@
|
||||
lib-be
|
||||
models
|
||||
native-query-snippets
|
||||
parameters
|
||||
permissions
|
||||
premium-features
|
||||
pulse
|
||||
@@ -2072,7 +2083,6 @@
|
||||
task
|
||||
users-rest
|
||||
util
|
||||
warehouse-schema
|
||||
warehouses-rest
|
||||
enterprise/dependencies
|
||||
enterprise/transforms
|
||||
@@ -2280,6 +2290,7 @@
|
||||
:uses #{analytics
|
||||
api
|
||||
app-db
|
||||
channel
|
||||
driver
|
||||
events
|
||||
lib
|
||||
@@ -2289,6 +2300,7 @@
|
||||
queries
|
||||
query-processor
|
||||
request
|
||||
revisions
|
||||
search
|
||||
settings
|
||||
sync
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
(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))
|
||||
@@ -97,7 +97,6 @@
|
||||
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
|
||||
|
||||
@@ -154,7 +154,6 @@
|
||||
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-lite-recursive [[:inner 0]]
|
||||
metabase.lib.util.match/replace [[:inner 0]]
|
||||
metabase.lib.util.match/replace-in [[:inner 0]]
|
||||
metabase.models.interface/define-batched-hydration-method [[:inner 0]]
|
||||
|
||||
@@ -1,23 +1,35 @@
|
||||
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
|
||||
|
||||
23
.codespellrc
@@ -1,3 +1,24 @@
|
||||
[codespell]
|
||||
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
|
||||
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,
|
||||
ignore-words = .codespellignore
|
||||
|
||||
@@ -4,7 +4,8 @@ FROM mcr.microsoft.com/vscode/devcontainers/java:21
|
||||
|
||||
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 yarn rlwrap fontconfig
|
||||
&& apt-get update && apt-get -y install --no-install-recommends nodejs rlwrap fontconfig \
|
||||
&& npm install --global yarn
|
||||
|
||||
# install Clojure
|
||||
RUN curl https://download.clojure.org/install/linux-install-1.12.0.1488.sh | bash
|
||||
|
||||
120
.env.example
@@ -40,9 +40,60 @@
|
||||
# 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"
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 2. E2E / CYPRESS TESTING
|
||||
# 3. E2E / CYPRESS TESTING
|
||||
# ==============================================================================
|
||||
#
|
||||
# Configuration for running Cypress end-to-end tests locally.
|
||||
@@ -54,19 +105,20 @@
|
||||
# You don't _have to_ export any of these vars (except for the tokens below).
|
||||
# Things will still work.
|
||||
|
||||
# 2.1. E2E Application Database (H2 required)
|
||||
# 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"
|
||||
# MB_DB_FILE="./e2e/tmp/cypress-test.db"
|
||||
|
||||
# Where does Cypress go to look for the application?
|
||||
MB_JETTY_PORT=4000
|
||||
# Where does Cypress go to look for the application? (defaults: localhost:4000)
|
||||
# MB_JETTY_HOST="localhost"
|
||||
# MB_JETTY_PORT=4000
|
||||
|
||||
# 2.2 E2E Local Runner Setup
|
||||
# 3.2 E2E Local Runner Setup
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Whether to run true e2e test suite, or SDK component tests (e2e | component)
|
||||
@@ -85,7 +137,7 @@ GENERATE_SNAPSHOTS="true"
|
||||
# Hint: build the JAR with ./bin/build.sh
|
||||
# JAR_PATH="target/uberjar/metabase.jar"
|
||||
|
||||
# 2.3 E2E Tokens
|
||||
# 3.3 E2E Tokens
|
||||
# ------------------------------------------------------------------------------
|
||||
#
|
||||
# These make sense for Metabase core devs only
|
||||
@@ -96,7 +148,7 @@ CYPRESS_MB_STARTER_CLOUD_TOKEN="op://Shared/Metabase License Dev Tokens/Licenses
|
||||
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"
|
||||
|
||||
# 2.4 E2E Misc.
|
||||
# 3.4 E2E Misc.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# To make Node.js match the instance tz
|
||||
@@ -106,55 +158,3 @@ TZ="US/Pacific"
|
||||
# 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
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 3. LOCAL METABASE DEVELOPMENT
|
||||
# ==============================================================================
|
||||
#
|
||||
# Settings for running Metabase locally. Most have sensible defaults—only
|
||||
# configure what you need.
|
||||
|
||||
# 3.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>"
|
||||
|
||||
# 3.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"
|
||||
|
||||
# 3.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"
|
||||
|
||||
25
.github/actions/fetch-artifact/action.yml
vendored
@@ -38,12 +38,25 @@ runs:
|
||||
|
||||
const artifact_id = artifacts.data.artifacts[0].id;
|
||||
|
||||
const download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: artifact_id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
let download;
|
||||
const maxAttempts = 3;
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: artifact_id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
break;
|
||||
} catch (err) {
|
||||
console.log(`Attempt ${attempt}/${maxAttempts} failed: ${err.message}`);
|
||||
if (attempt === maxAttempts) throw err;
|
||||
const delay = attempt * 15000; // 15s, 30s
|
||||
console.log(`Retrying in ${delay / 1000}s...`);
|
||||
await new Promise(r => setTimeout(r, delay));
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/mb.zip`, Buffer.from(download.data));
|
||||
|
||||
|
||||
19
.github/actions/maximize-disk-space/action.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
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
|
||||
9
.github/actions/test-driver/action.yml
vendored
@@ -12,11 +12,18 @@ inputs:
|
||||
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:
|
||||
quarantine-skipped:
|
||||
description: Whether the driver test was skipped due to quarantine
|
||||
value: ${{ steps.check-driver.outputs.skip }}
|
||||
test-outcome:
|
||||
description: "The outcome of the test run (success, failure, cancelled, skipped)"
|
||||
value: ${{ steps.run-test.outcome }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
@@ -61,7 +68,7 @@ runs:
|
||||
MB_EDITION: ee
|
||||
- name: Test database driver
|
||||
if: steps.check-driver.outputs.skip != 'true'
|
||||
run: clojure -X:dev:ci:ee:ee-dev:drivers:drivers-dev:test ${{ inputs.test-args }}
|
||||
run: clojure -X:dev:ci:${{ inputs.edition }}:${{ inputs.edition }}-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!
|
||||
|
||||
@@ -21,6 +21,8 @@ inputs:
|
||||
variant:
|
||||
required: false
|
||||
default: ""
|
||||
previous-step-outcome:
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
@@ -57,3 +59,4 @@ runs:
|
||||
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 }}
|
||||
|
||||
2
.github/file-paths.yaml
vendored
@@ -85,7 +85,9 @@ 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
|
||||
|
||||
18
.github/workflows/app-db.yml
vendored
@@ -68,8 +68,8 @@ jobs:
|
||||
(
|
||||
github.event_name == 'push' &&
|
||||
(
|
||||
github.ref == 'master' ||
|
||||
startsWith(github.ref, 'release-')
|
||||
github.ref_name == 'master' ||
|
||||
startsWith(github.ref_name, 'release-')
|
||||
) &&
|
||||
''
|
||||
) ||
|
||||
@@ -105,6 +105,7 @@ 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 }}
|
||||
@@ -123,6 +124,7 @@ 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 }}
|
||||
@@ -182,8 +184,8 @@ jobs:
|
||||
(
|
||||
github.event_name == 'push' &&
|
||||
(
|
||||
github.ref == 'master' ||
|
||||
startsWith(github.ref, 'release-')
|
||||
github.ref_name == 'master' ||
|
||||
startsWith(github.ref_name, 'release-')
|
||||
) &&
|
||||
''
|
||||
) ||
|
||||
@@ -227,6 +229,7 @@ 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 }}
|
||||
@@ -245,6 +248,7 @@ 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 }}
|
||||
@@ -297,8 +301,8 @@ jobs:
|
||||
(
|
||||
github.event_name == 'push' &&
|
||||
(
|
||||
github.ref == 'master' ||
|
||||
startsWith(github.ref, 'release-')
|
||||
github.ref_name == 'master' ||
|
||||
startsWith(github.ref_name, 'release-')
|
||||
) &&
|
||||
''
|
||||
) ||
|
||||
@@ -336,6 +340,7 @@ 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 }}
|
||||
@@ -354,6 +359,7 @@ 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:
|
||||
|
||||
5
.github/workflows/backend.yml
vendored
@@ -88,8 +88,8 @@ jobs:
|
||||
(
|
||||
github.event_name == 'push' &&
|
||||
(
|
||||
github.ref == 'master' ||
|
||||
startsWith(github.ref, 'release-')
|
||||
github.ref_name == 'master' ||
|
||||
startsWith(github.ref_name, 'release-')
|
||||
) &&
|
||||
''
|
||||
) ||
|
||||
@@ -130,6 +130,7 @@ 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
|
||||
|
||||
11
.github/workflows/check-milestone.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Major version number (e.g. 52, 78, 109)'
|
||||
description: "Major version number (e.g. 52, 78, 109)"
|
||||
type: number
|
||||
required: true
|
||||
|
||||
@@ -26,11 +26,14 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: release
|
||||
# .github folder is needed for the prepare-release-scripts action in master
|
||||
sparse-checkout: |
|
||||
release
|
||||
.github
|
||||
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 build scripts
|
||||
run: cd ${{ github.workspace }}/release && yarn && yarn build
|
||||
- name: Prepare release scripts
|
||||
uses: ./.github/actions/prepare-release-scripts # this action sets up bun, which is used in master, but not on versions prior to 59
|
||||
- uses: actions/github-script@v7
|
||||
if: ${{ env.isNewReleaseBranch == 'false' }}
|
||||
name: Set milestone for single commit
|
||||
|
||||
111
.github/workflows/codespell.yml
vendored
@@ -1,19 +1,110 @@
|
||||
name: Codespell
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'release-**'
|
||||
pull_request:
|
||||
schedule:
|
||||
# Run nightly at 4:00 AM UTC
|
||||
- cron: "0 4 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
codespell:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: codespell-project/actions-codespell@v1
|
||||
- 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
|
||||
with:
|
||||
# keep this list in sync with the list in `mage/src/mage/codespell.yml`
|
||||
path: >-
|
||||
docs
|
||||
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
|
||||
|
||||
410
.github/workflows/drivers.yml
vendored
@@ -12,12 +12,52 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# This job will determine if the tier 3 driver tests should be skipped.
|
||||
# It will always run first.
|
||||
# This job determines which driver tests should run based on PR context.
|
||||
# All decision logic is consolidated in mage driver-decisions.
|
||||
determine-driver-skips:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
skip: ${{ steps.check-branch-and-files-changed.outputs.skip }}
|
||||
# All driver run decisions (from mage driver-decisions)
|
||||
h2-should-run: ${{ steps.driver-decisions.outputs.h2-should-run }}
|
||||
athena-should-run: ${{ steps.driver-decisions.outputs.athena-should-run }}
|
||||
bigquery-should-run: ${{ steps.driver-decisions.outputs.bigquery-should-run }}
|
||||
clickhouse-should-run: ${{ steps.driver-decisions.outputs.clickhouse-should-run }}
|
||||
databricks-should-run: ${{ steps.driver-decisions.outputs.databricks-should-run }}
|
||||
druid-should-run: ${{ steps.driver-decisions.outputs.druid-should-run }}
|
||||
druid-jdbc-should-run: ${{ steps.driver-decisions.outputs.druid-jdbc-should-run }}
|
||||
mongo-should-run: ${{ steps.driver-decisions.outputs.mongo-should-run }}
|
||||
mongo-ssl-should-run: ${{ steps.driver-decisions.outputs.mongo-ssl-should-run }}
|
||||
mongo-sharded-cluster-should-run: ${{ steps.driver-decisions.outputs.mongo-sharded-cluster-should-run }}
|
||||
mysql-mariadb-should-run: ${{ steps.driver-decisions.outputs.mysql-mariadb-should-run }}
|
||||
oracle-should-run: ${{ steps.driver-decisions.outputs.oracle-should-run }}
|
||||
postgres-should-run: ${{ steps.driver-decisions.outputs.postgres-should-run }}
|
||||
presto-jdbc-should-run: ${{ steps.driver-decisions.outputs.presto-jdbc-should-run }}
|
||||
redshift-should-run: ${{ steps.driver-decisions.outputs.redshift-should-run }}
|
||||
snowflake-should-run: ${{ steps.driver-decisions.outputs.snowflake-should-run }}
|
||||
sparksql-should-run: ${{ steps.driver-decisions.outputs.sparksql-should-run }}
|
||||
sqlite-should-run: ${{ steps.driver-decisions.outputs.sqlite-should-run }}
|
||||
sqlserver-should-run: ${{ steps.driver-decisions.outputs.sqlserver-should-run }}
|
||||
vertica-should-run: ${{ steps.driver-decisions.outputs.vertica-should-run }}
|
||||
# Per-driver quarantine conflict flags (set when driver is quarantined but has file changes)
|
||||
athena-quarantine-conflict: ${{ steps.driver-decisions.outputs.athena-quarantine-conflict }}
|
||||
bigquery-quarantine-conflict: ${{ steps.driver-decisions.outputs.bigquery-quarantine-conflict }}
|
||||
clickhouse-quarantine-conflict: ${{ steps.driver-decisions.outputs.clickhouse-quarantine-conflict }}
|
||||
databricks-quarantine-conflict: ${{ steps.driver-decisions.outputs.databricks-quarantine-conflict }}
|
||||
druid-quarantine-conflict: ${{ steps.driver-decisions.outputs.druid-quarantine-conflict }}
|
||||
druid-jdbc-quarantine-conflict: ${{ steps.driver-decisions.outputs.druid-jdbc-quarantine-conflict }}
|
||||
mongo-quarantine-conflict: ${{ steps.driver-decisions.outputs.mongo-quarantine-conflict }}
|
||||
mongo-ssl-quarantine-conflict: ${{ steps.driver-decisions.outputs.mongo-ssl-quarantine-conflict }}
|
||||
mongo-sharded-cluster-quarantine-conflict: ${{ steps.driver-decisions.outputs.mongo-sharded-cluster-quarantine-conflict }}
|
||||
mysql-mariadb-quarantine-conflict: ${{ steps.driver-decisions.outputs.mysql-mariadb-quarantine-conflict }}
|
||||
oracle-quarantine-conflict: ${{ steps.driver-decisions.outputs.oracle-quarantine-conflict }}
|
||||
postgres-quarantine-conflict: ${{ steps.driver-decisions.outputs.postgres-quarantine-conflict }}
|
||||
presto-jdbc-quarantine-conflict: ${{ steps.driver-decisions.outputs.presto-jdbc-quarantine-conflict }}
|
||||
redshift-quarantine-conflict: ${{ steps.driver-decisions.outputs.redshift-quarantine-conflict }}
|
||||
snowflake-quarantine-conflict: ${{ steps.driver-decisions.outputs.snowflake-quarantine-conflict }}
|
||||
sparksql-quarantine-conflict: ${{ steps.driver-decisions.outputs.sparksql-quarantine-conflict }}
|
||||
sqlite-quarantine-conflict: ${{ steps.driver-decisions.outputs.sqlite-quarantine-conflict }}
|
||||
sqlserver-quarantine-conflict: ${{ steps.driver-decisions.outputs.sqlserver-quarantine-conflict }}
|
||||
vertica-quarantine-conflict: ${{ steps.driver-decisions.outputs.vertica-quarantine-conflict }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -26,29 +66,26 @@ jobs:
|
||||
run: |
|
||||
# Fetch the base branch to ensure its history is available for diffing
|
||||
git fetch --depth=1 origin ${{ github.event.pull_request.base.ref }}:${{ github.event.pull_request.base.ref }}
|
||||
# Only run this if it's a pull request and the base ref exists
|
||||
- name: Check if driver tests should run
|
||||
id: check-branch-and-files-changed
|
||||
- name: Determine which drivers should run
|
||||
id: driver-decisions
|
||||
run: |
|
||||
IS_MASTER_OR_RELEASE="${{ github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/release-') }}"
|
||||
echo "Is master or release: ${IS_MASTER_OR_RELEASE}"
|
||||
|
||||
if [[ "${IS_MASTER_OR_RELEASE}" == "true" ]]; then
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
echo "Merging into master or a release branch. All driver tests will run."
|
||||
else
|
||||
echo "Checking for changes in relevant modules..."
|
||||
if ./bin/mage can-skip-driver-tests "${{ github.event.pull_request.base.ref }}"; then
|
||||
echo 'skip=true' >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo 'skip=false' >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
# All decision logic is in mage - it analyzes module dependencies,
|
||||
# branch context, labels, and file changes to decide what runs.
|
||||
MAGE_ARGS=(
|
||||
--git-ref="${{ github.event.pull_request.base.ref || github.ref_name }}"
|
||||
--is-master-or-release="${{ github.ref_name == 'master' || startsWith(github.ref_name, 'release-') }}"
|
||||
--pr-labels="${{ join(github.event.pull_request.labels.*.name, ',') }}"
|
||||
--skip="${{ inputs.skip }}"
|
||||
)
|
||||
# First call: show human-readable output for debugging
|
||||
./bin/mage -driver-decisions "${MAGE_ARGS[@]}"
|
||||
# Second call: output only key=value lines for GITHUB_OUTPUT
|
||||
./bin/mage -driver-decisions "${MAGE_ARGS[@]}" --github-output-only >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
be-tests-athena-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
if: ${{ needs.determine-driver-skips.outputs.athena-should-run == 'true' || needs.determine-driver-skips.outputs.athena-quarantine-conflict == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -65,6 +102,16 @@ jobs:
|
||||
MB_ATHENA_TEST_WITHOUT_GET_TABLE_METADATA_SECRET_KEY: ${{ secrets.MB_ATHENA_TEST_WITHOUT_GET_TABLE_METADATA_SECRET_KEY }}
|
||||
name: Athena
|
||||
steps:
|
||||
- name: Fail on quarantine conflict
|
||||
if: ${{ needs.determine-driver-skips.outputs.athena-quarantine-conflict == 'true' }}
|
||||
run: |
|
||||
echo "::error::Athena driver is quarantined but you changed files in modules/drivers/athena/"
|
||||
echo ""
|
||||
echo "Your changes will NOT be tested because the driver is currently quarantined."
|
||||
echo "To run Athena tests, add the label: break-quarantine-athena"
|
||||
echo ""
|
||||
echo "If you merge without testing, you risk breaking the Athena driver!"
|
||||
exit 1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test Athena driver
|
||||
id: test-driver
|
||||
@@ -76,7 +123,7 @@ jobs:
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -85,10 +132,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-bigquery-cloud-sdk-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
if: ${{ needs.determine-driver-skips.outputs.bigquery-should-run == 'true' || needs.determine-driver-skips.outputs.bigquery-quarantine-conflict == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -99,6 +147,7 @@ jobs:
|
||||
test-args: >-
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
:fail-fast? true
|
||||
# DS 2025-09-26 temporarily disabled while we diagnose flakes
|
||||
# - name: Transforms Python Tests
|
||||
# test-args: >-
|
||||
@@ -116,6 +165,16 @@ jobs:
|
||||
MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON_ROUTING: ${{ secrets.MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON_ROUTING }}
|
||||
name: BigQuery ${{ matrix.job.name }}
|
||||
steps:
|
||||
- name: Fail on quarantine conflict
|
||||
if: ${{ needs.determine-driver-skips.outputs.bigquery-quarantine-conflict == 'true' }}
|
||||
run: |
|
||||
echo "::error::BigQuery driver is quarantined but you changed files in modules/drivers/bigquery-cloud-sdk/"
|
||||
echo ""
|
||||
echo "Your changes will NOT be tested because the driver is currently quarantined."
|
||||
echo "To run BigQuery tests, add the label: break-quarantine-bigquery"
|
||||
echo ""
|
||||
echo "If you merge without testing, you risk breaking the BigQuery driver!"
|
||||
exit 1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup python-runner
|
||||
if: ${{ matrix.job.needs-python-runner == true }}
|
||||
@@ -131,7 +190,7 @@ jobs:
|
||||
logs-artifact-suffix: ${{ matrix.job.name }}
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -140,6 +199,7 @@ 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 }}
|
||||
- name: Cleanup python-runner
|
||||
if: ${{ always() && matrix.job.needs-python-runner == true }}
|
||||
uses: ./.github/actions/cleanup-python-runner
|
||||
@@ -149,7 +209,7 @@ jobs:
|
||||
|
||||
be-tests-clickhouse-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
if: ${{ needs.determine-driver-skips.outputs.clickhouse-should-run == 'true' || needs.determine-driver-skips.outputs.clickhouse-quarantine-conflict == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -173,6 +233,16 @@ jobs:
|
||||
MB_CLICKHOUSE_TEST_NGINX_PORT: 8127
|
||||
name: Clickhouse ${{ matrix.job.name }}
|
||||
steps:
|
||||
- name: Fail on quarantine conflict
|
||||
if: ${{ needs.determine-driver-skips.outputs.clickhouse-quarantine-conflict == 'true' }}
|
||||
run: |
|
||||
echo "::error::ClickHouse driver is quarantined but you changed files in modules/drivers/clickhouse/"
|
||||
echo ""
|
||||
echo "Your changes will NOT be tested because the driver is currently quarantined."
|
||||
echo "To run ClickHouse tests, add the label: break-quarantine-clickhouse"
|
||||
echo ""
|
||||
echo "If you merge without testing, you risk breaking the ClickHouse driver!"
|
||||
exit 1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup python-runner
|
||||
if: ${{ matrix.job.needs-python-runner == true }}
|
||||
@@ -203,7 +273,7 @@ jobs:
|
||||
logs-artifact-suffix: ${{ matrix.job.name }}
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -212,6 +282,7 @@ 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 }}
|
||||
- name: Cleanup python-runner
|
||||
if: ${{ always() && matrix.job.needs-python-runner == true }}
|
||||
uses: ./.github/actions/cleanup-python-runner
|
||||
@@ -221,7 +292,7 @@ jobs:
|
||||
|
||||
be-tests-druid-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
if: ${{ needs.determine-driver-skips.outputs.druid-should-run == 'true' || needs.determine-driver-skips.outputs.druid-quarantine-conflict == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -236,6 +307,16 @@ jobs:
|
||||
CLUSTER_SIZE: nano-quickstart
|
||||
name: Druid (Legacy)
|
||||
steps:
|
||||
- name: Fail on quarantine conflict
|
||||
if: ${{ needs.determine-driver-skips.outputs.druid-quarantine-conflict == 'true' }}
|
||||
run: |
|
||||
echo "::error::Druid driver is quarantined but you changed files in modules/drivers/druid/"
|
||||
echo ""
|
||||
echo "Your changes will NOT be tested because the driver is currently quarantined."
|
||||
echo "To run Druid tests, add the label: break-quarantine-druid"
|
||||
echo ""
|
||||
echo "If you merge without testing, you risk breaking the Druid driver!"
|
||||
exit 1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test Druid driver
|
||||
id: test-driver
|
||||
@@ -247,7 +328,7 @@ jobs:
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -256,10 +337,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-druid-jdbc-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
if: ${{ needs.determine-driver-skips.outputs.druid-jdbc-should-run == 'true' || needs.determine-driver-skips.outputs.druid-jdbc-quarantine-conflict == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -274,6 +356,16 @@ jobs:
|
||||
CLUSTER_SIZE: nano-quickstart
|
||||
name: Druid (JDBC)
|
||||
steps:
|
||||
- name: Fail on quarantine conflict
|
||||
if: ${{ needs.determine-driver-skips.outputs.druid-jdbc-quarantine-conflict == 'true' }}
|
||||
run: |
|
||||
echo "::error::Druid JDBC driver is quarantined but you changed files in modules/drivers/druid-jdbc/"
|
||||
echo ""
|
||||
echo "Your changes will NOT be tested because the driver is currently quarantined."
|
||||
echo "To run Druid JDBC tests, add the label: break-quarantine-druid-jdbc"
|
||||
echo ""
|
||||
echo "If you merge without testing, you risk breaking the Druid JDBC driver!"
|
||||
exit 1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test Druid JDBC driver
|
||||
id: test-driver
|
||||
@@ -285,7 +377,7 @@ jobs:
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -294,10 +386,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-databricks-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
if: ${{ needs.determine-driver-skips.outputs.databricks-should-run == 'true' || needs.determine-driver-skips.outputs.databricks-quarantine-conflict == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -312,6 +405,16 @@ jobs:
|
||||
MB_DATABRICKS_TEST_OAUTH_SECRET: ${{ secrets.MB_DATABRICKS_JDBC_TEST_OAUTH_SECRET }}
|
||||
name: Databricks
|
||||
steps:
|
||||
- name: Fail on quarantine conflict
|
||||
if: ${{ needs.determine-driver-skips.outputs.databricks-quarantine-conflict == 'true' }}
|
||||
run: |
|
||||
echo "::error::Databricks driver is quarantined but you changed files in modules/drivers/databricks/"
|
||||
echo ""
|
||||
echo "Your changes will NOT be tested because the driver is currently quarantined."
|
||||
echo "To run Databricks tests, add the label: break-quarantine-databricks"
|
||||
echo ""
|
||||
echo "If you merge without testing, you risk breaking the Databricks driver!"
|
||||
exit 1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test Databricks driver
|
||||
id: test-driver
|
||||
@@ -323,7 +426,7 @@ jobs:
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -332,10 +435,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-databricks-multi-catalog-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
if: ${{ needs.determine-driver-skips.outputs.databricks-should-run == 'true' || needs.determine-driver-skips.outputs.databricks-quarantine-conflict == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -351,6 +455,16 @@ jobs:
|
||||
MB_DATABRICKS_TEST_OAUTH_SECRET: ${{ secrets.MB_DATABRICKS_JDBC_TEST_OAUTH_SECRET }}
|
||||
name: Databricks (Multi-Catalog)
|
||||
steps:
|
||||
- name: Fail on quarantine conflict
|
||||
if: ${{ needs.determine-driver-skips.outputs.databricks-quarantine-conflict == 'true' }}
|
||||
run: |
|
||||
echo "::error::Databricks driver is quarantined but you changed files in modules/drivers/databricks/"
|
||||
echo ""
|
||||
echo "Your changes will NOT be tested because the driver is currently quarantined."
|
||||
echo "To run Databricks tests, add the label: break-quarantine-databricks"
|
||||
echo ""
|
||||
echo "If you merge without testing, you risk breaking the Databricks driver!"
|
||||
exit 1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test Databricks driver with multi-catalog
|
||||
id: test-driver
|
||||
@@ -362,7 +476,7 @@ jobs:
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -371,9 +485,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-h2:
|
||||
if: ${{ !inputs.skip }}
|
||||
needs: determine-driver-skips
|
||||
if: ${{ needs.determine-driver-skips.outputs.h2-should-run == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -391,7 +507,7 @@ jobs:
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -400,10 +516,43 @@ 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-h2-oss:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ needs.determine-driver-skips.outputs.h2-should-run == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CI: 'true'
|
||||
name: H2 (OSS)
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test H2 driver (OSS)
|
||||
id: test-driver
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
junit-name: 'be-tests-h2-oss'
|
||||
edition: 'oss'
|
||||
test-args: >-
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
|
||||
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
|
||||
|
||||
be-tests-mysql-mariadb:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
if: ${{ needs.determine-driver-skips.outputs.mysql-mariadb-should-run == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -461,8 +610,8 @@ jobs:
|
||||
(
|
||||
github.event_name == 'push' &&
|
||||
(
|
||||
github.ref == 'master' ||
|
||||
startsWith(github.ref, 'release-')
|
||||
github.ref_name == 'master' ||
|
||||
startsWith(github.ref_name, 'release-')
|
||||
) &&
|
||||
''
|
||||
) ||
|
||||
@@ -521,7 +670,7 @@ jobs:
|
||||
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -530,6 +679,7 @@ 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 }}
|
||||
- name: Cleanup python-runner
|
||||
if: always()
|
||||
uses: ./.github/actions/cleanup-python-runner
|
||||
@@ -539,7 +689,7 @@ jobs:
|
||||
|
||||
be-tests-mongo:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
if: ${{ needs.determine-driver-skips.outputs.mongo-should-run == 'true' || needs.determine-driver-skips.outputs.mongo-quarantine-conflict == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -576,6 +726,16 @@ jobs:
|
||||
MONGO_INITDB_ROOT_PASSWORD: metasample123
|
||||
name: ${{ matrix.version.name }} ${{ matrix.job.name }}
|
||||
steps:
|
||||
- name: Fail on quarantine conflict
|
||||
if: ${{ needs.determine-driver-skips.outputs.mongo-quarantine-conflict == 'true' }}
|
||||
run: |
|
||||
echo "::error::MongoDB driver is quarantined but you changed files in modules/drivers/mongo/"
|
||||
echo ""
|
||||
echo "Your changes will NOT be tested because the driver is currently quarantined."
|
||||
echo "To run MongoDB tests, add the label: break-quarantine-mongo"
|
||||
echo ""
|
||||
echo "If you merge without testing, you risk breaking the MongoDB driver!"
|
||||
exit 1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup python-runner
|
||||
if: ${{ matrix.job.needs-python-runner == true }}
|
||||
@@ -591,7 +751,7 @@ jobs:
|
||||
logs-artifact-suffix: ${{ matrix.job.name }}
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -600,6 +760,7 @@ 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 }}
|
||||
- name: Cleanup python-runner
|
||||
if: ${{ always() && matrix.job.needs-python-runner == true }}
|
||||
uses: ./.github/actions/cleanup-python-runner
|
||||
@@ -609,7 +770,7 @@ jobs:
|
||||
|
||||
be-tests-mongo-ssl:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
if: ${{ needs.determine-driver-skips.outputs.mongo-ssl-should-run == 'true' || needs.determine-driver-skips.outputs.mongo-ssl-quarantine-conflict == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -627,6 +788,16 @@ jobs:
|
||||
MB_TEST_MONGO_REQUIRES_SSL: true
|
||||
name: "${{ matrix.version.name }} (SSL)"
|
||||
steps:
|
||||
- name: Fail on quarantine conflict
|
||||
if: ${{ needs.determine-driver-skips.outputs.mongo-ssl-quarantine-conflict == 'true' }}
|
||||
run: |
|
||||
echo "::error::MongoDB SSL driver is quarantined but you changed files in modules/drivers/mongo/"
|
||||
echo ""
|
||||
echo "Your changes will NOT be tested because the driver is currently quarantined."
|
||||
echo "To run MongoDB SSL tests, add the label: break-quarantine-mongo-ssl"
|
||||
echo ""
|
||||
echo "If you merge without testing, you risk breaking the MongoDB SSL driver!"
|
||||
exit 1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Spin up Mongo docker container
|
||||
run: >-
|
||||
@@ -663,7 +834,7 @@ jobs:
|
||||
:only-tags [:mb/driver-tests]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -672,10 +843,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-mongo-sharded-cluster-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
if: ${{ needs.determine-driver-skips.outputs.mongo-sharded-cluster-should-run == 'true' || needs.determine-driver-skips.outputs.mongo-sharded-cluster-quarantine-conflict == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -688,6 +860,16 @@ jobs:
|
||||
- "27017:17017"
|
||||
name: MongoDB Sharded Cluster
|
||||
steps:
|
||||
- name: Fail on quarantine conflict
|
||||
if: ${{ needs.determine-driver-skips.outputs.mongo-sharded-cluster-quarantine-conflict == 'true' }}
|
||||
run: |
|
||||
echo "::error::MongoDB Sharded Cluster driver is quarantined but you changed files in modules/drivers/mongo/"
|
||||
echo ""
|
||||
echo "Your changes will NOT be tested because the driver is currently quarantined."
|
||||
echo "To run MongoDB Sharded Cluster tests, add the label: break-quarantine-mongo-sharded-cluster"
|
||||
echo ""
|
||||
echo "If you merge without testing, you risk breaking the MongoDB Sharded Cluster driver!"
|
||||
exit 1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test MongoDB driver (Sharded cluster)
|
||||
id: test-driver
|
||||
@@ -698,7 +880,7 @@ jobs:
|
||||
:only metabase.driver.mongo.sharded-cluster-test
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -707,10 +889,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-oracle:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
if: ${{ needs.determine-driver-skips.outputs.oracle-should-run == 'true' || needs.determine-driver-skips.outputs.oracle-quarantine-conflict == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -761,6 +944,16 @@ jobs:
|
||||
- "1521:${{ matrix.version.port }}"
|
||||
name: ${{ matrix.version.name }}
|
||||
steps:
|
||||
- name: Fail on quarantine conflict
|
||||
if: ${{ needs.determine-driver-skips.outputs.oracle-quarantine-conflict == 'true' }}
|
||||
run: |
|
||||
echo "::error::Oracle driver is quarantined but you changed files in modules/drivers/oracle/"
|
||||
echo ""
|
||||
echo "Your changes will NOT be tested because the driver is currently quarantined."
|
||||
echo "To run Oracle tests, add the label: break-quarantine-oracle"
|
||||
echo ""
|
||||
echo "If you merge without testing, you risk breaking the Oracle driver!"
|
||||
exit 1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test ${{ matrix.version.name }}
|
||||
id: test-driver
|
||||
@@ -773,7 +966,7 @@ jobs:
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -782,10 +975,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:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
if: ${{ needs.determine-driver-skips.outputs.postgres-should-run == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -823,8 +1017,8 @@ jobs:
|
||||
(
|
||||
github.event_name == 'push' &&
|
||||
(
|
||||
github.ref == 'master' ||
|
||||
startsWith(github.ref, 'release-')
|
||||
github.ref_name == 'master' ||
|
||||
startsWith(github.ref_name, 'release-')
|
||||
) &&
|
||||
''
|
||||
) ||
|
||||
@@ -871,12 +1065,13 @@ jobs:
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
junit-name: 'be-tests-${{ matrix.version.junit-name }}'
|
||||
logs-artifact-suffix: ${{ matrix.version.name }}-${{ matrix.job.name }}
|
||||
test-args: >-
|
||||
${{ matrix.job.test-args }}
|
||||
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -885,6 +1080,7 @@ 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 }}
|
||||
- name: Cleanup python-runner
|
||||
if: always()
|
||||
uses: ./.github/actions/cleanup-python-runner
|
||||
@@ -894,7 +1090,7 @@ jobs:
|
||||
|
||||
be-tests-presto-jdbc-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
if: ${{ needs.determine-driver-skips.outputs.presto-jdbc-should-run == 'true' || needs.determine-driver-skips.outputs.presto-jdbc-quarantine-conflict == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -917,6 +1113,16 @@ jobs:
|
||||
env:
|
||||
JAVA_TOOL_OPTIONS: "-Xmx2g"
|
||||
steps:
|
||||
- name: Fail on quarantine conflict
|
||||
if: ${{ needs.determine-driver-skips.outputs.presto-jdbc-quarantine-conflict == 'true' }}
|
||||
run: |
|
||||
echo "::error::Presto JDBC driver is quarantined but you changed files in modules/drivers/presto-jdbc/"
|
||||
echo ""
|
||||
echo "Your changes will NOT be tested because the driver is currently quarantined."
|
||||
echo "To run Presto JDBC tests, add the label: break-quarantine-presto-jdbc"
|
||||
echo ""
|
||||
echo "If you merge without testing, you risk breaking the Presto JDBC driver!"
|
||||
exit 1
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/await-port
|
||||
with:
|
||||
@@ -953,7 +1159,7 @@ jobs:
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -962,10 +1168,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-redshift-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
if: ${{ needs.determine-driver-skips.outputs.redshift-should-run == 'true' || needs.determine-driver-skips.outputs.redshift-quarantine-conflict == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -1010,6 +1217,16 @@ jobs:
|
||||
MB_REDSHIFT_TEST_PASSWORD: ${{ secrets.MB_REDSHIFT_TEST_PASSWORD }}
|
||||
name: ${{ matrix.job.name }}
|
||||
steps:
|
||||
- name: Fail on quarantine conflict
|
||||
if: ${{ needs.determine-driver-skips.outputs.redshift-quarantine-conflict == 'true' }}
|
||||
run: |
|
||||
echo "::error::Redshift driver is quarantined but you changed files in modules/drivers/redshift/"
|
||||
echo ""
|
||||
echo "Your changes will NOT be tested because the driver is currently quarantined."
|
||||
echo "To run Redshift tests, add the label: break-quarantine-redshift"
|
||||
echo ""
|
||||
echo "If you merge without testing, you risk breaking the Redshift driver!"
|
||||
exit 1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup python-runner
|
||||
if: ${{ matrix.job.needs-python-runner == true }}
|
||||
@@ -1027,7 +1244,7 @@ jobs:
|
||||
logs-artifact-suffix: ${{ matrix.job.name }}
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: ${{ !matrix.job.skip && !cancelled() && steps.test-driver.outputs.quarantine-skipped != 'true' }}
|
||||
if: ${{ !matrix.job.skip && !cancelled() }}
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -1036,6 +1253,7 @@ 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 }}
|
||||
- name: Cleanup python-runner
|
||||
if: ${{ always() && matrix.job.needs-python-runner == true }}
|
||||
uses: ./.github/actions/cleanup-python-runner
|
||||
@@ -1045,7 +1263,7 @@ jobs:
|
||||
|
||||
be-tests-snowflake-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
if: ${{ needs.determine-driver-skips.outputs.snowflake-should-run == 'true' || needs.determine-driver-skips.outputs.snowflake-quarantine-conflict == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -1056,6 +1274,7 @@ jobs:
|
||||
test-args: >-
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
:fail-fast? true
|
||||
|
||||
# DS 2025-09-26 temporarily disabled while we diagnose flakes
|
||||
# - name: Transforms Python Tests
|
||||
@@ -1083,6 +1302,16 @@ jobs:
|
||||
MB_SNOWFLAKE_TEST_RSA_ROLE_TEST_DB: RSA_ROLE_TEST_DB
|
||||
name: Snowflake ${{ matrix.job.name }}
|
||||
steps:
|
||||
- name: Fail on quarantine conflict
|
||||
if: ${{ needs.determine-driver-skips.outputs.snowflake-quarantine-conflict == 'true' }}
|
||||
run: |
|
||||
echo "::error::Snowflake driver is quarantined but you changed files in modules/drivers/snowflake/"
|
||||
echo ""
|
||||
echo "Your changes will NOT be tested because the driver is currently quarantined."
|
||||
echo "To run Snowflake tests, add the label: break-quarantine-snowflake"
|
||||
echo ""
|
||||
echo "If you merge without testing, you risk breaking the Snowflake driver!"
|
||||
exit 1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup python-runner
|
||||
if: ${{ matrix.job.needs-python-runner == true }}
|
||||
@@ -1098,7 +1327,7 @@ jobs:
|
||||
logs-artifact-suffix: ${{ matrix.job.name }}
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -1107,6 +1336,7 @@ 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 }}
|
||||
- name: Cleanup python-runner
|
||||
if: ${{ always() && matrix.job.needs-python-runner == true }}
|
||||
uses: ./.github/actions/cleanup-python-runner
|
||||
@@ -1116,7 +1346,7 @@ jobs:
|
||||
|
||||
be-tests-sparksql-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
if: ${{ needs.determine-driver-skips.outputs.sparksql-should-run == 'true' || needs.determine-driver-skips.outputs.sparksql-quarantine-conflict == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -1129,6 +1359,16 @@ jobs:
|
||||
- "10000:10000"
|
||||
name: Spark SQL
|
||||
steps:
|
||||
- name: Fail on quarantine conflict
|
||||
if: ${{ needs.determine-driver-skips.outputs.sparksql-quarantine-conflict == 'true' }}
|
||||
run: |
|
||||
echo "::error::SparkSQL driver is quarantined but you changed files in modules/drivers/sparksql/"
|
||||
echo ""
|
||||
echo "Your changes will NOT be tested because the driver is currently quarantined."
|
||||
echo "To run SparkSQL tests, add the label: break-quarantine-sparksql"
|
||||
echo ""
|
||||
echo "If you merge without testing, you risk breaking the SparkSQL driver!"
|
||||
exit 1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test Spark driver
|
||||
id: test-driver
|
||||
@@ -1140,7 +1380,7 @@ jobs:
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -1149,10 +1389,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-sqlite-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
if: ${{ needs.determine-driver-skips.outputs.sqlite-should-run == 'true' || needs.determine-driver-skips.outputs.sqlite-quarantine-conflict == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -1160,6 +1401,16 @@ jobs:
|
||||
DRIVERS: sqlite
|
||||
name: SQLite
|
||||
steps:
|
||||
- name: Fail on quarantine conflict
|
||||
if: ${{ needs.determine-driver-skips.outputs.sqlite-quarantine-conflict == 'true' }}
|
||||
run: |
|
||||
echo "::error::SQLite driver is quarantined but you changed files in modules/drivers/sqlite/"
|
||||
echo ""
|
||||
echo "Your changes will NOT be tested because the driver is currently quarantined."
|
||||
echo "To run SQLite tests, add the label: break-quarantine-sqlite"
|
||||
echo ""
|
||||
echo "If you merge without testing, you risk breaking the SQLite driver!"
|
||||
exit 1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test SQLite driver
|
||||
id: test-driver
|
||||
@@ -1171,7 +1422,7 @@ jobs:
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -1180,10 +1431,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-sqlserver:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
if: ${{ needs.determine-driver-skips.outputs.sqlserver-should-run == 'true' || needs.determine-driver-skips.outputs.sqlserver-quarantine-conflict == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
@@ -1222,6 +1474,16 @@ jobs:
|
||||
MSSQL_MEMORY_LIMIT_MB: 1024
|
||||
name: ${{ matrix.version.name }} ${{ matrix.job.name }}
|
||||
steps:
|
||||
- name: Fail on quarantine conflict
|
||||
if: ${{ needs.determine-driver-skips.outputs.sqlserver-quarantine-conflict == 'true' }}
|
||||
run: |
|
||||
echo "::error::SQL Server driver is quarantined but you changed files in modules/drivers/sqlserver/"
|
||||
echo ""
|
||||
echo "Your changes will NOT be tested because the driver is currently quarantined."
|
||||
echo "To run SQL Server tests, add the label: break-quarantine-sqlserver"
|
||||
echo ""
|
||||
echo "If you merge without testing, you risk breaking the SQL Server driver!"
|
||||
exit 1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup python-runner
|
||||
if: ${{ matrix.job.needs-python-runner == true }}
|
||||
@@ -1237,7 +1499,7 @@ jobs:
|
||||
logs-artifact-suffix: ${{ matrix.version.name }}
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -1246,6 +1508,7 @@ 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 }}
|
||||
- name: Cleanup python-runner
|
||||
if: ${{ always() && matrix.job.needs-python-runner == true }}
|
||||
uses: ./.github/actions/cleanup-python-runner
|
||||
@@ -1258,7 +1521,7 @@ jobs:
|
||||
# needs: determine-driver-skips
|
||||
# if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
# runs-on: ubuntu-latest
|
||||
# timeout-minutes: 60
|
||||
# timeout-minutes: 40
|
||||
# env:
|
||||
# CI: 'true'
|
||||
# DRIVERS: starburst
|
||||
@@ -1294,7 +1557,7 @@ jobs:
|
||||
|
||||
login-to-ecr:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
if: ${{ needs.determine-driver-skips.outputs.vertica-should-run == 'true' || needs.determine-driver-skips.outputs.vertica-quarantine-conflict == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
@@ -1324,7 +1587,7 @@ jobs:
|
||||
|
||||
be-tests-vertica-ee:
|
||||
needs: [determine-driver-skips, login-to-ecr]
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
if: ${{ needs.determine-driver-skips.outputs.vertica-should-run == 'true' || needs.determine-driver-skips.outputs.vertica-quarantine-conflict == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -1340,6 +1603,16 @@ jobs:
|
||||
password: ${{ needs.login-to-ecr.outputs.docker_password }}
|
||||
name: Vertica
|
||||
steps:
|
||||
- name: Fail on quarantine conflict
|
||||
if: ${{ needs.determine-driver-skips.outputs.vertica-quarantine-conflict == 'true' }}
|
||||
run: |
|
||||
echo "::error::Vertica driver is quarantined but you changed files in modules/drivers/vertica/"
|
||||
echo ""
|
||||
echo "Your changes will NOT be tested because the driver is currently quarantined."
|
||||
echo "To run Vertica tests, add the label: break-quarantine-vertica"
|
||||
echo ""
|
||||
echo "If you merge without testing, you risk breaking the Vertica driver!"
|
||||
exit 1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Make plugins directory
|
||||
run: mkdir plugins
|
||||
@@ -1353,7 +1626,7 @@ jobs:
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -1362,6 +1635,7 @@ 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 }}
|
||||
|
||||
drivers-tests-result:
|
||||
needs:
|
||||
|
||||
11
.github/workflows/e2e-stress-test-flake-fix.yml
vendored
@@ -76,19 +76,15 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
name: Stress test E2E flake fix
|
||||
env:
|
||||
DISPLAY: ""
|
||||
MB_RUN_MODE: e2e
|
||||
METASTORE_DEV_SERVER_URL: ${{ secrets.METASTORE_DEV_SERVER_URL }}
|
||||
DISPLAY: "" # https://github.com/cypress-io/cypress/issues/4034
|
||||
TERM: xterm
|
||||
TZ: US/Pacific # to make Node match the instance tz
|
||||
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:
|
||||
@@ -114,7 +110,6 @@ jobs:
|
||||
|
||||
- name: Prepare front-end environment
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
|
||||
|
||||
28
.github/workflows/e2e-test.yml
vendored
@@ -29,6 +29,15 @@ 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 }}
|
||||
@@ -36,21 +45,12 @@ jobs:
|
||||
name: e2e-tests-${{ inputs.name }}-${{ inputs.edition }}
|
||||
env:
|
||||
MB_EDITION: ${{ inputs.edition }}
|
||||
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 }}
|
||||
# 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 }}
|
||||
TZ: US/Pacific # to make node match the instance tz
|
||||
CYPRESS_CI: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -113,6 +113,7 @@ 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 }}
|
||||
@@ -136,6 +137,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run Tagged EE Cypress tests on ${{ inputs.name }}
|
||||
id: tagged-e2e
|
||||
if: ${{ inputs.specs }}
|
||||
shell: bash
|
||||
env:
|
||||
@@ -165,10 +167,12 @@ 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: failure()
|
||||
if: |
|
||||
!cancelled() && (steps.split-e2e.outcome == 'failure' || steps.tagged-e2e.outcome == 'failure')
|
||||
with:
|
||||
name: cypress-recording-${{ inputs.name }}-${{ inputs.edition }}
|
||||
path: |
|
||||
@@ -178,10 +182,12 @@ jobs:
|
||||
|
||||
- name: Upload Failed Tests
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
if: |
|
||||
!cancelled() && (steps.split-e2e.outcome == 'failure' || steps.tagged-e2e.outcome == '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
|
||||
|
||||
@@ -3,6 +3,12 @@ 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 }})"
|
||||
@@ -13,17 +19,12 @@ jobs:
|
||||
react-version: [18, 19]
|
||||
fail-fast: false
|
||||
env:
|
||||
MB_EDITION: ee
|
||||
DISPLAY: ""
|
||||
# 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
|
||||
@@ -79,6 +80,7 @@ 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
|
||||
@@ -94,6 +96,7 @@ 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
|
||||
@@ -149,8 +152,6 @@ 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 }}
|
||||
# Any env starting with `CYPRESS_` will be available to all Cypress tests via `Cypress.env()`
|
||||
@@ -159,9 +160,6 @@ jobs:
|
||||
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
|
||||
@@ -232,6 +230,7 @@ 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: |
|
||||
@@ -249,6 +248,7 @@ 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
|
||||
|
||||
@@ -3,6 +3,14 @@ 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 }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || '' }}
|
||||
MB_EDITION: ee
|
||||
jobs:
|
||||
get-sample-apps-data:
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -100,15 +108,9 @@ 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:
|
||||
@@ -117,6 +119,8 @@ 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
|
||||
@@ -187,6 +191,12 @@ jobs:
|
||||
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' }}
|
||||
@@ -231,14 +241,7 @@ 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 }}
|
||||
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
|
||||
|
||||
2
.github/workflows/frontend.yml
vendored
@@ -90,6 +90,7 @@ jobs:
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
- name: Run frontend unit tests
|
||||
id: fe-tests
|
||||
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
|
||||
@@ -106,6 +107,7 @@ 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 }}
|
||||
|
||||
56
.github/workflows/fuzzing.yml
vendored
@@ -1,56 +0,0 @@
|
||||
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
|
||||
14
.github/workflows/minimal-tests.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CI: 'true'
|
||||
CI: "true"
|
||||
DRIVERS: postgres
|
||||
MB_DB_TYPE: postgres
|
||||
MB_DB_PORT: 5432
|
||||
@@ -53,10 +53,11 @@ 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]'
|
||||
@@ -73,6 +74,7 @@ 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:
|
||||
@@ -81,7 +83,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CI: 'true'
|
||||
CI: "true"
|
||||
DRIVERS: postgres
|
||||
MB_DB_TYPE: postgres
|
||||
MB_DB_PORT: 5432
|
||||
@@ -102,9 +104,10 @@ 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]
|
||||
@@ -119,6 +122,7 @@ 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:
|
||||
@@ -137,6 +141,7 @@ jobs:
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
- name: Run frontend unit tests
|
||||
id: run-tests
|
||||
run: yarn run test-unit --silent
|
||||
continue-on-error: true
|
||||
- name: Upload Test Results
|
||||
@@ -150,6 +155,7 @@ 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:
|
||||
|
||||
44
.github/workflows/openapi-check.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: OpenAPI Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled, unlabeled]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check-openapi-specs:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 5
|
||||
if: contains(github.event.pull_request.labels.*.name, 'openapi-self-healing')
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# we're intentionally using METABASE_AUTOMATION_USER_TOKEN to trigger the whole CI pipeline again
|
||||
# after the self-healing commit is created
|
||||
# if we used GITHUB_TOKEN, the self-healing commit would not trigger the CI pipeline again
|
||||
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
# fetch full git history to allow fetching the latest base branch commit and comparing changes
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js 22
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.x
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
- name: Generate version.properties
|
||||
run: ./bin/build.sh :steps [:version]
|
||||
- name: Get latest base branch commit
|
||||
run: |
|
||||
git fetch origin ${{ github.event.pull_request.base.ref }}
|
||||
echo "LATEST_BASE_SHA=$(git rev-parse origin/${{ github.event.pull_request.base.ref }})" >> $GITHUB_ENV
|
||||
- name: Check OpenAPI specs are up to date
|
||||
env:
|
||||
AUTO_COMMIT: "true"
|
||||
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||
BASE_SHA: ${{ env.LATEST_BASE_SHA }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
run: ./bin/openapi-check.sh
|
||||
37
.github/workflows/release-embedding-sdk.yml
vendored
@@ -8,9 +8,9 @@ on:
|
||||
description: "Branch we want to release the SDK from (to release from other branches use the workflow from those branches)"
|
||||
type: choice
|
||||
required: true
|
||||
default: "master"
|
||||
default: "release-x.58.x"
|
||||
options:
|
||||
- master
|
||||
- release-x.58.x
|
||||
|
||||
concurrency:
|
||||
# We want to ensure only one job is running at a time per branch because
|
||||
@@ -369,42 +369,11 @@ jobs:
|
||||
name: metabase-sdk
|
||||
path: sdk
|
||||
|
||||
- name: Compute master tag
|
||||
id: compute-tag
|
||||
run: |
|
||||
if [ -n "${{ needs.determine-sdk-version.outputs.pre_release_label }}" ]; then
|
||||
echo "masterTag=${{ needs.determine-sdk-version.outputs.pre_release_label }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "masterTag=nightly" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Publish to NPM
|
||||
working-directory: sdk
|
||||
run: |
|
||||
echo //registry.npmjs.org/:_authToken=${{ secrets.NPM_RELEASE_TOKEN }} > .npmrc
|
||||
|
||||
TAG=$(jq -r --null-input \
|
||||
--arg branch "${{ inputs.branch }}" \
|
||||
--arg masterTag "${{ steps.compute-tag.outputs.masterTag }}" '
|
||||
{
|
||||
"master": $masterTag,
|
||||
"release-x.51.x": "51-stable",
|
||||
"release-x.52.x": "52-stable",
|
||||
"release-x.53.x": "53-stable",
|
||||
"release-x.54.x": "54-stable",
|
||||
"release-x.55.x": "55-stable",
|
||||
"release-x.56.x": "56-stable"
|
||||
}[$branch]'
|
||||
)
|
||||
|
||||
echo "Resolved npm tag: $TAG"
|
||||
npm publish --tag "$TAG"
|
||||
|
||||
- name: Add `latest` tag to the latest release branch (`release-x.55.x`) deployment
|
||||
if: ${{ inputs.branch == 'release-x.56.x' }}
|
||||
working-directory: sdk
|
||||
run: |
|
||||
npm dist-tag add @metabase/embedding-sdk-react@${{ env.sdk_version }} latest
|
||||
npm publish --tag 58-stable
|
||||
|
||||
git-tag:
|
||||
needs: [publish-npm, determine-sdk-version]
|
||||
|
||||
12
.github/workflows/release-log.yml
vendored
@@ -11,13 +11,13 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Major Metabase version (e.g. 45, 52, 68)'
|
||||
description: "Major Metabase version (e.g. 45, 52, 68)"
|
||||
type: number
|
||||
required: true
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Major Metabase version (e.g. 45, 52, 68)'
|
||||
description: "Major Metabase version (e.g. 45, 52, 68)"
|
||||
type: number # needs to be a number to pass variables
|
||||
required: true
|
||||
|
||||
@@ -38,10 +38,10 @@ jobs:
|
||||
with:
|
||||
ref: master # this only works on master
|
||||
fetch-depth: 0 # we want all branches and tags
|
||||
- name: Install Dependencies
|
||||
run: yarn --cwd release --frozen-lockfile && npm i -g tsx
|
||||
- name: Build release scripts
|
||||
run: yarn --cwd release build
|
||||
- name: Prepare release scripts
|
||||
uses: ./.github/actions/prepare-release-scripts
|
||||
- name: Install tsx
|
||||
run: npm i -g tsx
|
||||
- name: Get version number
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
|
||||
8
.github/workflows/semantic-search.yml
vendored
@@ -46,6 +46,7 @@ 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 }}
|
||||
@@ -64,6 +65,7 @@ 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 }}
|
||||
|
||||
semantic-search-tests-mariadb:
|
||||
if: ${{ !inputs.skip }}
|
||||
@@ -118,6 +120,7 @@ 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 }}
|
||||
@@ -136,6 +139,7 @@ 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 }}
|
||||
|
||||
semantic-search-tests-mysql:
|
||||
if: ${{ !inputs.skip }}
|
||||
@@ -190,6 +194,7 @@ 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 }}
|
||||
@@ -208,6 +213,7 @@ 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 }}
|
||||
|
||||
semantic-search-tests-postgres:
|
||||
if: ${{ !inputs.skip }}
|
||||
@@ -263,6 +269,7 @@ 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 }}
|
||||
@@ -281,6 +288,7 @@ 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 }}
|
||||
|
||||
semantic-search-tests-result:
|
||||
needs:
|
||||
|
||||
34
.github/workflows/snyk.yml
vendored
@@ -3,15 +3,15 @@ name: "Snyk"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'release-**'
|
||||
- "master"
|
||||
- "release-**"
|
||||
paths:
|
||||
- '**/deps.edn'
|
||||
- '**/package.json'
|
||||
- '.github/workflows/snyk.yml'
|
||||
- '.github/scripts/write-poms.sh'
|
||||
- "**/deps.edn"
|
||||
- "**/package.json"
|
||||
- ".github/workflows/snyk.yml"
|
||||
- ".github/scripts/write-poms.sh"
|
||||
schedule:
|
||||
- cron: '0 5 * * *'
|
||||
- cron: "0 5 * * *"
|
||||
|
||||
jobs:
|
||||
# Generate pom.xml files for all Maven projects
|
||||
@@ -45,26 +45,6 @@ jobs:
|
||||
file: yarn.lock
|
||||
package-manager: yarn
|
||||
category: main-frontend-yarn
|
||||
- name: Embedding SDK Template
|
||||
file: bin/embedding-sdk/templates/yarn.lock
|
||||
package-manager: yarn
|
||||
category: embedding-sdk-template
|
||||
- name: E2E Angular 20 Host App
|
||||
file: e2e/embedding-sdk-host-apps/angular-20-host-app/package-lock.json
|
||||
package-manager: npm
|
||||
category: e2e-angular-host
|
||||
- name: E2E Next 15 App Router
|
||||
file: e2e/embedding-sdk-host-apps/next-15-app-router-host-app/package-lock.json
|
||||
package-manager: npm
|
||||
category: e2e-next-app-router
|
||||
- name: E2E Next 15 Pages Router
|
||||
file: e2e/embedding-sdk-host-apps/next-15-pages-router-host-app/package-lock.json
|
||||
package-manager: npm
|
||||
category: e2e-next-pages-router
|
||||
- name: E2E Vite 6 Host App
|
||||
file: e2e/embedding-sdk-host-apps/vite-6-host-app/package-lock.json
|
||||
package-manager: npm
|
||||
category: e2e-vite-host
|
||||
- name: Release Helper
|
||||
file: release/yarn.lock
|
||||
package-manager: yarn
|
||||
|
||||
4
.gitignore
vendored
@@ -21,7 +21,8 @@
|
||||
/*.mv.db
|
||||
/*.trace.db
|
||||
/.babel_cache
|
||||
/.env
|
||||
/.env*
|
||||
!/.env.example
|
||||
/.envrc
|
||||
/.lein-env
|
||||
/.lein-failures
|
||||
@@ -104,6 +105,7 @@ dev/serialization_deltas/
|
||||
*.tsbuildinfo
|
||||
/trace-output/
|
||||
/temp-types/
|
||||
/frontend/src/metabase-types/openapi
|
||||
|
||||
**/cypress_sample_database.json
|
||||
**/cypress_sample_instance_data.json
|
||||
|
||||
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 201 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.2 KiB |