Compare commits
466 Commits
table-ref-
...
python_run
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a43b2d35f | ||
|
|
832f8ac44d | ||
|
|
88bdc6c06c | ||
|
|
bdfc24938c | ||
|
|
123f2dafd2 | ||
|
|
16504fd3c8 | ||
|
|
997b8d898e | ||
|
|
ceb518ab4b | ||
|
|
0ed6c4deed | ||
|
|
6c09fca355 | ||
|
|
049526c61c | ||
|
|
a799178420 | ||
|
|
f7b82d5c54 | ||
|
|
a51d1179d3 | ||
|
|
cbed2e45a8 | ||
|
|
418d7a0317 | ||
|
|
3bce633480 | ||
|
|
48327532c5 | ||
|
|
26f71cfeb5 | ||
|
|
3d4e44b776 | ||
|
|
86f3e524ba | ||
|
|
056d61238e | ||
|
|
926610da14 | ||
|
|
75251f933a | ||
|
|
d959d54aec | ||
|
|
0933b7378f | ||
|
|
e3b200b319 | ||
|
|
fb9c0e4499 | ||
|
|
40522b6cba | ||
|
|
b6edd15de2 | ||
|
|
16042f45c7 | ||
|
|
b28ab0ec1d | ||
|
|
add8906504 | ||
|
|
3345365546 | ||
|
|
862dc50c64 | ||
|
|
0941091421 | ||
|
|
0bd69823a4 | ||
|
|
6a01533ca7 | ||
|
|
7d18a1b5a6 | ||
|
|
0bf7e41984 | ||
|
|
95a25e4a3e | ||
|
|
a2c4eef9de | ||
|
|
2f81937643 | ||
|
|
48bf58b162 | ||
|
|
761a7cf01e | ||
|
|
8a76f71003 | ||
|
|
fd90d13ae3 | ||
|
|
a62aab3f1b | ||
|
|
bafd49ee17 | ||
|
|
1f0b461d6f | ||
|
|
1712274b85 | ||
|
|
ea203102e6 | ||
|
|
3c72591b17 | ||
|
|
9a19a5e4f0 | ||
|
|
f387d4aca4 | ||
|
|
1bb6f6a74a | ||
|
|
2ad4292465 | ||
|
|
37c3758748 | ||
|
|
ae9cad5c58 | ||
|
|
bd14e14974 | ||
|
|
85319217cd | ||
|
|
240acb4782 | ||
|
|
e6bc40c223 | ||
|
|
76fcb4d573 | ||
|
|
a92317dd27 | ||
|
|
087a317f70 | ||
|
|
c63fd746d8 | ||
|
|
f29a446aab | ||
|
|
d26bf4e454 | ||
|
|
7aa18ae448 | ||
|
|
97da3d7c7b | ||
|
|
10e8a68cf5 | ||
|
|
209a34b102 | ||
|
|
a6df84c136 | ||
|
|
adb9a5cbf4 | ||
|
|
d491eb6d51 | ||
|
|
0f20f4d2f0 | ||
|
|
9304e6cb6e | ||
|
|
21f043d697 | ||
|
|
d6bb46208f | ||
|
|
e2f7cc9d86 | ||
|
|
0880e7297d | ||
|
|
4f23bc973f | ||
|
|
1afabc3d01 | ||
|
|
cdf5111a0d | ||
|
|
70cf127afd | ||
|
|
ac421d0a6d | ||
|
|
9f5b23ecdd | ||
|
|
f8551c9800 | ||
|
|
4befb6089f | ||
|
|
c902d22458 | ||
|
|
7451ffcd93 | ||
|
|
410471a4d0 | ||
|
|
a2b3490355 | ||
|
|
3b8e1ecaa4 | ||
|
|
0176c399f9 | ||
|
|
2287567b12 | ||
|
|
d9b8b9a2c0 | ||
|
|
17f473a9f1 | ||
|
|
110cb5bca7 | ||
|
|
1149c3e30c | ||
|
|
735b98d6f8 | ||
|
|
359158e594 | ||
|
|
9a5e8227d0 | ||
|
|
287665a6fa | ||
|
|
82f16a8528 | ||
|
|
45b4b682cc | ||
|
|
7a96e1f142 | ||
|
|
a48f246c50 | ||
|
|
dd00b33545 | ||
|
|
51eab9a902 | ||
|
|
4b47763109 | ||
|
|
d4960babe5 | ||
|
|
3856fd989e | ||
|
|
2ac5ed4fb0 | ||
|
|
1b80d7dc16 | ||
|
|
34266fc636 | ||
|
|
e3ced91a43 | ||
|
|
a37b3fb0ec | ||
|
|
1284c201b5 | ||
|
|
feb0c5b34a | ||
|
|
58e4025456 | ||
|
|
9ceae0061a | ||
|
|
2cb8a32def | ||
|
|
59adba6f98 | ||
|
|
1da7a523b8 | ||
|
|
6165e70718 | ||
|
|
1a2f012afe | ||
|
|
c8b6f74578 | ||
|
|
905c5dde99 | ||
|
|
913e0c9ee8 | ||
|
|
9eb6d7c74d | ||
|
|
a27a3d286b | ||
|
|
03d26b2a19 | ||
|
|
4fa44b5fc7 | ||
|
|
2e3746de55 | ||
|
|
46b0aa85a2 | ||
|
|
bb66df77e1 | ||
|
|
293d47d3e8 | ||
|
|
3f1ff1ba60 | ||
|
|
e694056f0f | ||
|
|
de171c2092 | ||
|
|
fd2bf97c1f | ||
|
|
0bba5e8742 | ||
|
|
3302e9cafd | ||
|
|
18c485d06c | ||
|
|
68d40f2770 | ||
|
|
b92393b9f0 | ||
|
|
83ca220730 | ||
|
|
ba7c5d1fc7 | ||
|
|
2cf11310a8 | ||
|
|
06d4da18c3 | ||
|
|
44e118b5e3 | ||
|
|
3a490774cc | ||
|
|
d5ad9d6142 | ||
|
|
eacff63768 | ||
|
|
6cea29bace | ||
|
|
e3d6d76bf8 | ||
|
|
37243f5469 | ||
|
|
ad60b57c4a | ||
|
|
fa924880ad | ||
|
|
bace1d68c5 | ||
|
|
0523cf2397 | ||
|
|
c75465cd07 | ||
|
|
a76e880fdd | ||
|
|
032d1e9ea5 | ||
|
|
b2840f32f9 | ||
|
|
499eacf3d1 | ||
|
|
23d9096d0b | ||
|
|
594cd0cc89 | ||
|
|
f3c0be5fc1 | ||
|
|
950b555558 | ||
|
|
2e65eac90e | ||
|
|
c73ccb8be7 | ||
|
|
7976ad4f47 | ||
|
|
825719067a | ||
|
|
95af300c15 | ||
|
|
d2f3648298 | ||
|
|
60093c461b | ||
|
|
aab9fc000a | ||
|
|
28baf73201 | ||
|
|
2a70fd1794 | ||
|
|
05105be4c7 | ||
|
|
c0663854c7 | ||
|
|
fe4d26ab63 | ||
|
|
30cd48cc7c | ||
|
|
b3f62b6b6d | ||
|
|
50270bc649 | ||
|
|
b595e58985 | ||
|
|
08b309f4c8 | ||
|
|
db6808c754 | ||
|
|
ce252a2a8b | ||
|
|
701b90bb9c | ||
|
|
16c291f753 | ||
|
|
a469da0955 | ||
|
|
9a6393ff4a | ||
|
|
46bef2348b | ||
|
|
0266970fff | ||
|
|
88ec74f7b3 | ||
|
|
b07c00001d | ||
|
|
11e96da48b | ||
|
|
5ab27ac0fb | ||
|
|
7cd7ef457a | ||
|
|
79f43e1259 | ||
|
|
739a915d5d | ||
|
|
5c6c075103 | ||
|
|
2d76c2fb74 | ||
|
|
63be293945 | ||
|
|
221b76041d | ||
|
|
8d4837c6b0 | ||
|
|
7879a97840 | ||
|
|
4d820ce589 | ||
|
|
cdcaaf5f33 | ||
|
|
a2e9d66d7a | ||
|
|
04d68a6a41 | ||
|
|
ad87e0366a | ||
|
|
90ba3e39e7 | ||
|
|
ee9ba65762 | ||
|
|
469c2f4681 | ||
|
|
696d8eec73 | ||
|
|
fbe2dd23a4 | ||
|
|
85fe26063c | ||
|
|
afdd39c531 | ||
|
|
fb22fed921 | ||
|
|
07c5e47251 | ||
|
|
fd9a13e05e | ||
|
|
a46764dd85 | ||
|
|
a4e306f9cf | ||
|
|
4ec8c0cb0e | ||
|
|
bff5dfb5eb | ||
|
|
0deffd6231 | ||
|
|
9311117086 | ||
|
|
9a37785bc3 | ||
|
|
3b52306116 | ||
|
|
7a753622e0 | ||
|
|
09860c0032 | ||
|
|
48e3b5c190 | ||
|
|
08421d9ec1 | ||
|
|
1c9de41832 | ||
|
|
4497b15aed | ||
|
|
7a8b15e271 | ||
|
|
75476f4ae1 | ||
|
|
4e60b2efeb | ||
|
|
3315a9a65a | ||
|
|
d20f6f9f11 | ||
|
|
686de1c06e | ||
|
|
7b6b37264a | ||
|
|
20ec35314f | ||
|
|
c7f4935d58 | ||
|
|
80b6060ab7 | ||
|
|
b374706a41 | ||
|
|
a131b9c921 | ||
|
|
d4486c9dff | ||
|
|
ad5cc2877d | ||
|
|
88647269a0 | ||
|
|
f0ab6a222c | ||
|
|
cc9f7e63ad | ||
|
|
3798862761 | ||
|
|
3fbd0ff00d | ||
|
|
9f30405075 | ||
|
|
363baef1d9 | ||
|
|
6cdb558f0e | ||
|
|
13ec0f6fc8 | ||
|
|
a6b5cb7371 | ||
|
|
2e6bb7f669 | ||
|
|
c822a6228c | ||
|
|
2229469ca4 | ||
|
|
43ad099236 | ||
|
|
b496e05834 | ||
|
|
2847a18d60 | ||
|
|
413b32e277 | ||
|
|
4a2fade226 | ||
|
|
117e2a22a3 | ||
|
|
cd5bc6bf72 | ||
|
|
f1626f8a81 | ||
|
|
c82582de0c | ||
|
|
d124262aca | ||
|
|
22fa6d762a | ||
|
|
c771b10ea8 | ||
|
|
98adee8b12 | ||
|
|
7fe74f70f8 | ||
|
|
63ce826faa | ||
|
|
63f180034d | ||
|
|
89fa63c99b | ||
|
|
670828db79 | ||
|
|
88684d3b2c | ||
|
|
d7b8e86987 | ||
|
|
322a6a0323 | ||
|
|
d5b070b8a3 | ||
|
|
aa5a35f94c | ||
|
|
adf35e2c4c | ||
|
|
da9fbb67a2 | ||
|
|
314122dee5 | ||
|
|
3975adb932 | ||
|
|
65a2afa186 | ||
|
|
c65cbf7791 | ||
|
|
42312e5fc2 | ||
|
|
376ff5d1cf | ||
|
|
8cbc058a0c | ||
|
|
6b04138204 | ||
|
|
65d34e4e37 | ||
|
|
fd6740eee7 | ||
|
|
18cf4fac85 | ||
|
|
17349e4f12 | ||
|
|
bfb78b8d6a | ||
|
|
343ae8d213 | ||
|
|
e69b260f0b | ||
|
|
336180ee0c | ||
|
|
d5925e28f7 | ||
|
|
5bc3af2630 | ||
|
|
c5f433336a | ||
|
|
dca8465cfb | ||
|
|
618e6e8701 | ||
|
|
6f637ee050 | ||
|
|
c06b4c2511 | ||
|
|
7cdbdc8e94 | ||
|
|
1bcc022f6b | ||
|
|
0d13842be7 | ||
|
|
a7d35f1baf | ||
|
|
0c04a6a3c1 | ||
|
|
1888c80fff | ||
|
|
c1f08f65a6 | ||
|
|
844123dbf2 | ||
|
|
ebd1476a1e | ||
|
|
78263d64ce | ||
|
|
7f08349271 | ||
|
|
bfa61de71f | ||
|
|
d1a55ddff1 | ||
|
|
4e3d8f0caf | ||
|
|
75ab330328 | ||
|
|
69abc4cf1c | ||
|
|
6cef5f80d0 | ||
|
|
5dd7e2049e | ||
|
|
2393146b8c | ||
|
|
112d1d0235 | ||
|
|
3f2ad43a43 | ||
|
|
835b3efc27 | ||
|
|
d8d4537f29 | ||
|
|
3f35a82ea7 | ||
|
|
a1a62cd603 | ||
|
|
8542738fe7 | ||
|
|
f290bbbbe0 | ||
|
|
1f366dde49 | ||
|
|
edeea2ead0 | ||
|
|
fbdf7574ed | ||
|
|
a2aea53b1c | ||
|
|
d972f4529f | ||
|
|
f1f47d4050 | ||
|
|
476f61760c | ||
|
|
b24727409c | ||
|
|
119487b1cc | ||
|
|
a5afeea12d | ||
|
|
4521f75ee6 | ||
|
|
cfdd4de405 | ||
|
|
24f586428d | ||
|
|
0128771ad1 | ||
|
|
e3f05d0cbb | ||
|
|
53203af754 | ||
|
|
71c188c09f | ||
|
|
db48071330 | ||
|
|
05d04cf64b | ||
|
|
6c99df07a8 | ||
|
|
5b92cc2b02 | ||
|
|
fea1cbad4b | ||
|
|
024c43ff39 | ||
|
|
ff4f2bf956 | ||
|
|
d2d8d1fe83 | ||
|
|
e5ab9db7bc | ||
|
|
4786076692 | ||
|
|
1e3a879116 | ||
|
|
2c01e5063b | ||
|
|
b4912b71f1 | ||
|
|
1d8b669f62 | ||
|
|
25091a2fd1 | ||
|
|
6f00f47c47 | ||
|
|
07e2560596 | ||
|
|
d47b9d6759 | ||
|
|
b222e32bdf | ||
|
|
390e559dc2 | ||
|
|
44aa361a2c | ||
|
|
cf7026a6a2 | ||
|
|
afef9ac2ae | ||
|
|
b5f483d951 | ||
|
|
7247ffca2d | ||
|
|
8d7ffb6845 | ||
|
|
2b97f841bc | ||
|
|
fe7668963c | ||
|
|
29b1e70d21 | ||
|
|
b8113ad526 | ||
|
|
6d1cb34859 | ||
|
|
0c2996b7da | ||
|
|
9daa5119f6 | ||
|
|
48b6a9d289 | ||
|
|
037391a1ea | ||
|
|
c8c18494ac | ||
|
|
cfcf6c16eb | ||
|
|
050a38bb19 | ||
|
|
0e21ffcaac | ||
|
|
c36208d52e | ||
|
|
d4ba2829bd | ||
|
|
9f6d9f46f3 | ||
|
|
cb7c3a09ac | ||
|
|
d0327d0b4b | ||
|
|
42a7260209 | ||
|
|
df292ad856 | ||
|
|
81010429ec | ||
|
|
487036f0b1 | ||
|
|
7a36d9143a | ||
|
|
fb96706972 | ||
|
|
3459c5344d | ||
|
|
42c8a22c37 | ||
|
|
8ee6859b19 | ||
|
|
3f4e0678f5 | ||
|
|
26f6e2b590 | ||
|
|
0cc6677886 | ||
|
|
f14e1f1444 | ||
|
|
09d69f2cdf | ||
|
|
75f3c9fc01 | ||
|
|
101fc34a37 | ||
|
|
dd830db392 | ||
|
|
286b89bf92 | ||
|
|
141d49a4c5 | ||
|
|
462a62a092 | ||
|
|
4c9fe80dc5 | ||
|
|
23eae39c3c | ||
|
|
e4eab0c668 | ||
|
|
b2bac8e014 | ||
|
|
b149b004b0 | ||
|
|
fdabf871f3 | ||
|
|
b99d3d186d | ||
|
|
d2da731428 | ||
|
|
687d28227a | ||
|
|
50e46d56a7 | ||
|
|
7596158b4a | ||
|
|
711a728569 | ||
|
|
c95d1d4883 | ||
|
|
6635453dae | ||
|
|
73a25b84ef | ||
|
|
4bece3e5c1 | ||
|
|
e37c65d34b | ||
|
|
04dbcf1823 | ||
|
|
2c7d8835d8 | ||
|
|
0972cf4e5b | ||
|
|
6d00178043 | ||
|
|
8ce1d65f8c | ||
|
|
453478f9fb | ||
|
|
c857b75646 | ||
|
|
269891d61c | ||
|
|
b5083f4afa | ||
|
|
b8117583a8 | ||
|
|
cb398d5630 | ||
|
|
4680a440e1 | ||
|
|
1e4e2b3f7b | ||
|
|
f42b947b37 | ||
|
|
1e94db5a93 | ||
|
|
b2a430f208 | ||
|
|
172aa84fd7 | ||
|
|
3d5c36960d | ||
|
|
7038c00b38 | ||
|
|
93e1a7a60b | ||
|
|
4207ad9722 | ||
|
|
6d002819df | ||
|
|
52bfdad99d | ||
|
|
f601dea72d | ||
|
|
cf9868d509 | ||
|
|
0ddc4ab309 |
12
.claude/commands/ci-report.md
Normal file
@@ -0,0 +1,12 @@
|
||||
Generate a CI failure report for PR $PR_NUM_OR_URL (or current branch if no argument given).
|
||||
|
||||
Run `./bin/mage ci-report $PR_NUM_OR_URL` to generate the report, then:
|
||||
|
||||
1. **Summarize the failures** - What tests failed and in which jobs?
|
||||
2. **Categorize** - Are these real failures, flaky tests, or infrastructure issues?
|
||||
3. **Recommend next steps**:
|
||||
- If flaky: suggest rerunning with `gh run rerun <run-id> --failed -R metabase/metabase`
|
||||
- If real: identify the root cause and suggest fixes
|
||||
- If infrastructure: note the issue type (timeout, OOM, network)
|
||||
|
||||
Focus on actionable insights. If all checks are passing, confirm that briefly.
|
||||
@@ -29,7 +29,7 @@ This guide covers Clojure and ClojureScript coding conventions for Metabase. See
|
||||
|
||||
**Comments:**
|
||||
|
||||
- `TODO` format: `;; TODO (Name M/D/YY) -- description`
|
||||
- `TODO` format: `;; TODO (Name YYYY-MM-DD) -- description`
|
||||
|
||||
## Code Organization
|
||||
|
||||
@@ -42,7 +42,7 @@ This guide covers Clojure and ClojureScript coding conventions for Metabase. See
|
||||
|
||||
- Break up functions > 20 lines
|
||||
- Lines ≤ 120 characters
|
||||
- No blank lines within definition forms (except pairwise `let`/`cond`)
|
||||
- No blank non-comment lines within definition forms (except pairwise `let`/`cond`)
|
||||
|
||||
## Style Conventions
|
||||
|
||||
|
||||
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
|
||||
@@ -54,14 +54,14 @@ Use this to scan through changes efficiently:
|
||||
- [ ] Public vars in `src` or `enterprise/backend/src` have useful docstrings
|
||||
- [ ] Docstrings use Markdown conventions
|
||||
- [ ] References use `[[other-var]]` not backticks
|
||||
- [ ] `TODO` comments include author and date: `;; TODO (Name 1/1/25) -- description`
|
||||
- [ ] `TODO` comments include author and date: `;; TODO (Name 2025-01-01) -- description`
|
||||
|
||||
### Code Organization
|
||||
|
||||
- [ ] 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
|
||||
@@ -118,7 +118,7 @@ Quick scan for common issues:
|
||||
| `update-db`, `save-model` | Missing `!` for side effects: `update-db!`, `save-model!` |
|
||||
| `snake_case_var` | Should use kebab-case |
|
||||
| Public var without docstring | Add docstring explaining purpose |
|
||||
| `;; TODO fix this` | Missing author/date: `;; TODO (Name 1/1/25) -- description` |
|
||||
| `;; TODO fix this` | Missing author/date: `;; TODO (Name 2025-01-01) -- description` |
|
||||
| `(defn foo ...)` in namespace used elsewhere | Should be `(defn ^:private foo ...)` |
|
||||
| Function > 20 lines | Consider breaking up into smaller functions |
|
||||
| `/api/dashboards/:id` | Use singular: `/api/dashboard/:id` |
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
:metabase/missing-test-expr-requires-in-cljs {:level :warning}
|
||||
:metabase/modules {:level :warning} ; more config in `.clj-kondo/config/modules/config.edn`
|
||||
:metabase/namespace-name {:level :warning}
|
||||
:metabase/no-jsqlparser-imports {:level :warning}
|
||||
:metabase/require-shape-checker {:level :warning}
|
||||
:metabase/test-helpers-use-non-thread-safe-functions {:level :warning} ; more config in `.clj-kondo/config/test-helpers/config.edn`
|
||||
:metabase/tests-must-live-in-known-modules {:level :warning}
|
||||
@@ -299,7 +300,8 @@
|
||||
metabase-enterprise.transforms.interface/execute! {:message "Use metabase-enterprise.transforms.execute/execute! instead of metabase-enterprise.transforms.interface/execute!"}
|
||||
next.jdbc/active-tx? {:message "Use metabase.app-db.core/in-transaction? to see if we are in a transaction"}
|
||||
toucan2.core/debug {:message "Don't commit code using t2/debug"}
|
||||
toucan2.tools.debug/debug {:message "Don't commit code using t2/debug"}}
|
||||
toucan2.tools.debug/debug {:message "Don't commit code using t2/debug"}
|
||||
macaw.core/parsed-query {:message "Use metabase.driver.util/parsed-query instead of macaw.core/parsed-query"}}
|
||||
|
||||
:discouraged-namespace
|
||||
{camel-snake-kebab.core {:message "CSK is not Turkish-safe, use the versions in metabase.util instead."}
|
||||
@@ -522,8 +524,10 @@
|
||||
metabase-enterprise.advanced-permissions.models.permissions.application-permissions-test/with-new-group-and-current-graph hooks.common/with-two-top-level-bindings
|
||||
metabase-enterprise.audit-app.pages-test/with-temp-objects hooks.common/with-one-binding
|
||||
metabase-enterprise.cache.config-test/with-temp-persist-models hooks.common/with-seven-bindings
|
||||
metabase-enterprise.dependencies.api-test/with-dependents-test! hooks.common/with-two-bindings
|
||||
metabase-enterprise.metabot-v3.tools.dependencies-test/with-dependent-transforms! hooks.common/with-two-bindings
|
||||
metabase-enterprise.semantic-search.db.connection/with-migrate-tx hooks.common/let-one-with-optional-value
|
||||
metabase-enterprise.serialization.api-test/with-serialization-test-data! hooks.common/with-three-bindings
|
||||
metabase-enterprise.serialization.test-util/with-temp-dpc hooks.toucan2.tools.with-temp/with-temp
|
||||
metabase-enterprise.transforms-python.drivers-test/with-test-table hooks.common/with-two-bindings
|
||||
metabase.api.common.internal-test/with-jetty-server hooks.common/with-one-binding
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
;; ## `team`:
|
||||
;;
|
||||
;; The team that owns this module. This has to match one of the team names in `.github/team.json` EXACTLY. You can
|
||||
;; run [metabase.core.modules-test/all-modules-have-teams-test]] to make sure you did this correctly.
|
||||
;; run [[metabase.core.modules-test/all-modules-have-teams-test]] to make sure you did this correctly.
|
||||
;;
|
||||
;; ## `api`:
|
||||
;;
|
||||
@@ -39,6 +39,17 @@
|
||||
;; 2. A set of module symbols. This is the list of modules that are allowed to be referenced. An empty set means no
|
||||
;; other modules are allowed to be referenced; this is the default for any modules that aren't listed here.
|
||||
;;
|
||||
;; ## `friends`:
|
||||
;;
|
||||
;; 'Friend' in the C++ sense; set of modules that are allowed free access into the internals of this module including
|
||||
;; namespaces that are not `:api` namespaces. This is useful when writing modules that are 'extensions' of other
|
||||
;; modules, for example `enterprise/search` might be enterprise extensions of `search`, or `search-rest` might be the
|
||||
;; REST API component for `search`.
|
||||
;;
|
||||
;; search
|
||||
;; {...
|
||||
;; :friends #{enterprise/search search-rest}}
|
||||
;;
|
||||
;; ## Tips
|
||||
;;
|
||||
;; PRO TIP: Check out the [[dev.deps-graph]] namespace for helpful tools to see where a module is used externally, and
|
||||
@@ -147,8 +158,7 @@
|
||||
metabase.api.settings
|
||||
metabase.api.util
|
||||
metabase.api.util.handlers}
|
||||
:uses #{api-routes
|
||||
config
|
||||
:uses #{config
|
||||
events
|
||||
models
|
||||
settings
|
||||
@@ -368,11 +378,11 @@
|
||||
|
||||
collections-rest
|
||||
{:team "UX West"
|
||||
:api #{metabase.collections-rest.api}
|
||||
:api #{metabase.collections-rest.api
|
||||
metabase.collections-rest.init}
|
||||
:uses #{api
|
||||
app-db
|
||||
collections
|
||||
config
|
||||
eid-translation
|
||||
events
|
||||
lib-be
|
||||
@@ -383,6 +393,7 @@
|
||||
queries
|
||||
request
|
||||
revisions
|
||||
settings
|
||||
upload
|
||||
util}}
|
||||
|
||||
@@ -499,7 +510,8 @@
|
||||
:api #{metabase.documents.api
|
||||
metabase.documents.core
|
||||
metabase.documents.init
|
||||
metabase.documents.prose-mirror}
|
||||
metabase.documents.prose-mirror
|
||||
metabase.documents.schema}
|
||||
:uses #{activity-feed
|
||||
api
|
||||
app-db
|
||||
@@ -601,7 +613,6 @@
|
||||
metabase.embedding.validation}
|
||||
:uses #{analytics
|
||||
api
|
||||
config
|
||||
premium-features
|
||||
server
|
||||
settings
|
||||
@@ -702,19 +713,13 @@
|
||||
lib
|
||||
{:team "Querying"
|
||||
:api #{metabase.lib.binning
|
||||
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
|
||||
metabase.lib.field.resolution
|
||||
metabase.lib.filter
|
||||
metabase.lib.filter.desugar.jvm ; only so [[metabase.legacy-mbql.jvm-util]] can use defined regexes
|
||||
metabase.lib.hierarchy
|
||||
metabase.lib.init
|
||||
metabase.lib.join.util
|
||||
metabase.lib.metadata
|
||||
metabase.lib.metadata.cached-provider
|
||||
metabase.lib.metadata.column
|
||||
@@ -738,25 +743,28 @@
|
||||
metabase.lib.schema.join
|
||||
metabase.lib.schema.literal
|
||||
metabase.lib.schema.mbql-clause
|
||||
metabase.lib.schema.measure
|
||||
metabase.lib.schema.metadata
|
||||
metabase.lib.schema.metadata.fingerprint
|
||||
metabase.lib.schema.middleware-options
|
||||
metabase.lib.schema.order-by
|
||||
metabase.lib.schema.parameter
|
||||
metabase.lib.schema.ref
|
||||
metabase.lib.schema.settings
|
||||
metabase.lib.schema.template-tag
|
||||
metabase.lib.schema.temporal-bucketing
|
||||
metabase.lib.schema.util
|
||||
metabase.lib.schema.validate
|
||||
metabase.lib.temporal-bucket
|
||||
metabase.lib.types.isa
|
||||
metabase.lib.util
|
||||
metabase.lib.util.match
|
||||
metabase.lib.walk}
|
||||
:uses #{config
|
||||
graph
|
||||
legacy-mbql
|
||||
types
|
||||
util}}
|
||||
util}
|
||||
:friends #{query-processor}}
|
||||
|
||||
lib-be
|
||||
{:team "Querying"
|
||||
@@ -791,6 +799,19 @@
|
||||
settings
|
||||
util}}
|
||||
|
||||
measures
|
||||
{:team "Querying"
|
||||
:api #{metabase.measures.api}
|
||||
:uses #{api
|
||||
events
|
||||
lib
|
||||
lib-be
|
||||
models
|
||||
permissions
|
||||
remote-sync
|
||||
search
|
||||
util}}
|
||||
|
||||
model-persistence
|
||||
{:team "Querying"
|
||||
:api #{metabase.model-persistence.api
|
||||
@@ -952,8 +973,10 @@
|
||||
app-db
|
||||
classloader
|
||||
config
|
||||
events
|
||||
internal-stats
|
||||
settings
|
||||
task
|
||||
util}}
|
||||
|
||||
product-feedback
|
||||
@@ -1010,6 +1033,7 @@
|
||||
collections
|
||||
config
|
||||
driver
|
||||
embedding
|
||||
events
|
||||
lib
|
||||
models
|
||||
@@ -1041,6 +1065,7 @@
|
||||
config
|
||||
content-verification
|
||||
dashboards
|
||||
documents
|
||||
events
|
||||
lib
|
||||
lib-be
|
||||
@@ -1253,11 +1278,11 @@
|
||||
metabase.segments.schema}
|
||||
:uses #{api
|
||||
events
|
||||
legacy-mbql
|
||||
lib
|
||||
lib-be
|
||||
models
|
||||
permissions
|
||||
remote-sync
|
||||
search
|
||||
util
|
||||
xrays}}
|
||||
@@ -1363,6 +1388,7 @@
|
||||
permissions
|
||||
premium-features
|
||||
settings
|
||||
system
|
||||
util}}
|
||||
|
||||
startup
|
||||
@@ -1425,9 +1451,12 @@
|
||||
metabase.task-history.core
|
||||
metabase.task-history.init}
|
||||
:uses #{api
|
||||
app-db
|
||||
config
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
query-processor
|
||||
request
|
||||
task
|
||||
util}}
|
||||
@@ -1471,7 +1500,6 @@
|
||||
:uses #{analytics
|
||||
api
|
||||
collections
|
||||
config
|
||||
events
|
||||
models
|
||||
util}}
|
||||
@@ -1610,6 +1638,7 @@
|
||||
permissions
|
||||
premium-features
|
||||
query-processor
|
||||
remote-sync
|
||||
search
|
||||
util
|
||||
warehouses}}
|
||||
@@ -1635,7 +1664,6 @@
|
||||
upload
|
||||
util
|
||||
warehouse-schema
|
||||
warehouses-rest
|
||||
xrays}}
|
||||
|
||||
warehouses
|
||||
@@ -1753,6 +1781,7 @@
|
||||
premium-features
|
||||
query-permissions
|
||||
query-processor
|
||||
remote-sync
|
||||
util
|
||||
warehouses
|
||||
enterprise/impersonation}}
|
||||
@@ -1947,7 +1976,9 @@
|
||||
metabase-enterprise.dependencies.init}
|
||||
:uses #{analyze
|
||||
api
|
||||
app-db
|
||||
collections
|
||||
config
|
||||
documents
|
||||
driver
|
||||
events
|
||||
@@ -1960,10 +1991,12 @@
|
||||
premium-features
|
||||
queries
|
||||
query-processor
|
||||
request
|
||||
revisions
|
||||
settings
|
||||
task
|
||||
util}}
|
||||
util
|
||||
enterprise/transforms}}
|
||||
|
||||
enterprise/email
|
||||
{:team "UX West"
|
||||
@@ -2197,6 +2230,7 @@
|
||||
appearance
|
||||
collections
|
||||
config
|
||||
events
|
||||
logger
|
||||
models
|
||||
plugins
|
||||
@@ -2282,10 +2316,12 @@
|
||||
metabase-enterprise.transforms.init
|
||||
metabase-enterprise.transforms.instrumentation
|
||||
metabase-enterprise.transforms.interface
|
||||
metabase-enterprise.transforms.schema
|
||||
metabase-enterprise.transforms.util}
|
||||
:uses #{analytics
|
||||
api
|
||||
app-db
|
||||
channel
|
||||
collections
|
||||
driver
|
||||
events
|
||||
@@ -2295,7 +2331,9 @@
|
||||
premium-features
|
||||
queries
|
||||
query-processor
|
||||
remote-sync
|
||||
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))
|
||||
@@ -99,12 +99,48 @@
|
||||
:message "Metabase namespaces should have the form metabase[-enterprise].<module>.* [:metabase/namespace-name]"
|
||||
:type :metabase/namespace-name)))))))
|
||||
|
||||
(defn- ns-form-node->import-node [ns-form-node]
|
||||
(some (fn [node]
|
||||
(when (and (hooks/list-node? node)
|
||||
(let [first-child (first (:children node))]
|
||||
(and (hooks/keyword-node? first-child)
|
||||
(= (hooks/sexpr first-child) :import))))
|
||||
node))
|
||||
(:children ns-form-node)))
|
||||
|
||||
(defn- jsqlparser-import?
|
||||
"Check if a symbol or string represents a jsqlparser import."
|
||||
[s]
|
||||
(str/starts-with? (str s) "net.sf.jsqlparser"))
|
||||
|
||||
(defn- lint-jsqlparser-imports [ns-form-node]
|
||||
(when-let [import-node (ns-form-node->import-node ns-form-node)]
|
||||
(doseq [node (rest (:children import-node))
|
||||
:when (not (contains? (hooks.common/ignored-linters node) :metabase/no-jsqlparser-imports))]
|
||||
(cond
|
||||
;; Handle (net.sf.jsqlparser.foo Bar Baz) form
|
||||
(hooks/list-node? node)
|
||||
(let [package-node (first (:children node))
|
||||
package-name (when package-node (str (hooks/sexpr package-node)))]
|
||||
(when (jsqlparser-import? package-name)
|
||||
(hooks/reg-finding! (assoc (meta node)
|
||||
:message "Don't import net.sf.jsqlparser classes directly. This code should be agnostic to the parsing library. [:metabase/no-jsqlparser-imports]"
|
||||
:type :metabase/no-jsqlparser-imports))))
|
||||
|
||||
;; Handle net.sf.jsqlparser.foo.Bar form (single class import)
|
||||
(hooks/token-node? node)
|
||||
(when (jsqlparser-import? (hooks/sexpr node))
|
||||
(hooks/reg-finding! (assoc (meta node)
|
||||
:message "Don't import net.sf.jsqlparser classes directly. This code should be agnostic to the parsing library. [:metabase/no-jsqlparser-imports]"
|
||||
:type :metabase/no-jsqlparser-imports)))))))
|
||||
|
||||
(defn lint-ns [x]
|
||||
(doto (:node x)
|
||||
lint-require-shapes
|
||||
lint-requires-on-new-lines
|
||||
(lint-modules (modules/config x))
|
||||
lint-namespace-name)
|
||||
lint-namespace-name
|
||||
lint-jsqlparser-imports)
|
||||
x)
|
||||
|
||||
(comment
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
second
|
||||
symbol))))
|
||||
|
||||
(defn module-api-namespaces
|
||||
(defn- module-api-namespaces
|
||||
"Set of API namespace symbols for a given module. `:any` means you can use anything, there are no API namespaces for
|
||||
this module (yet). If unspecified, the default is just the `<module>.core` namespace."
|
||||
[config module]
|
||||
@@ -51,6 +51,12 @@
|
||||
(symbol (str ns-prefix ".core"))
|
||||
(symbol (str ns-prefix ".init"))}))))
|
||||
|
||||
(defn- module-friends
|
||||
[config module]
|
||||
"Set of modules that are `:friends` of `module`, i.e. allowed to use *any* namespace from the module, not just the
|
||||
designated [[module-api-namespaces]]."
|
||||
(set (get-in config [:metabase/modules module :friends])))
|
||||
|
||||
(defn allowed-modules
|
||||
"Set of namespace symbols that `module` is allowed to use. `:any` means it's allowed to use anything."
|
||||
[config module]
|
||||
@@ -61,11 +67,13 @@
|
||||
(or (= allowed-modules :any)
|
||||
(contains? (set allowed-modules) required-module))))
|
||||
|
||||
(defn allowed-module-namespace? [config ns-symb]
|
||||
(defn- allowed-module-namespace? [config current-module ns-symb]
|
||||
(let [module (module ns-symb)
|
||||
module-api-namespaces (module-api-namespaces config module)]
|
||||
module-api-namespaces (module-api-namespaces config module)
|
||||
module-friends (module-friends config module)]
|
||||
(or (empty? module-api-namespaces)
|
||||
(contains? module-api-namespaces ns-symb))))
|
||||
(contains? module-api-namespaces ns-symb)
|
||||
(contains? module-friends current-module))))
|
||||
|
||||
(defn usage-error
|
||||
"Find usage errors when a `required-namespace` is required in the `current-module`. Returns a string describing the
|
||||
@@ -81,7 +89,7 @@
|
||||
current-module
|
||||
current-module)
|
||||
|
||||
(not (allowed-module-namespace? config required-namespace))
|
||||
(not (allowed-module-namespace? config current-module required-namespace))
|
||||
(format "Namespace %s is not an allowed external API namespace for the %s module. [:metabase/modules %s :api]"
|
||||
required-namespace
|
||||
required-module
|
||||
|
||||
@@ -43,6 +43,17 @@
|
||||
'{:metabase/modules {api {:uses #{search}}
|
||||
search {:api #{metabase.search.core}}}}))))
|
||||
|
||||
(deftest ^:parallel module-checker-friends-test
|
||||
(is (= []
|
||||
(lint-modules
|
||||
'(ns metabase.search-rest.api
|
||||
(:require
|
||||
[metabase.search.config :as search.config]
|
||||
[metabase.search.core :as search]))
|
||||
'{:metabase/modules {search {:api #{metabase.search.core}
|
||||
:friends #{search-rest}}
|
||||
search-rest {:uses #{search}}}}))))
|
||||
|
||||
(deftest ^:parallel module-checker-api-namespaces-ignore-test
|
||||
(is (empty? (lint-modules
|
||||
'(ns metabase.api.search
|
||||
|
||||
@@ -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
|
||||
|
||||
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"
|
||||
|
||||
23
.github/ISSUE_TEMPLATE/epic.md
vendored
@@ -1,23 +0,0 @@
|
||||
---
|
||||
name: Epic (Internal Use)
|
||||
about: This issue is used to track a feature/project that may span several days
|
||||
to weeks to implement. This is used by the core Metabase team to project plan
|
||||
and manage work.
|
||||
title: "[Epic] Title"
|
||||
labels: ".Epic"
|
||||
---
|
||||
|
||||
### Links
|
||||
|
||||
- [product doc](LINK_TO_PRODUCT_DOC)
|
||||
- [technical design doc](LINK_TO_TECHNICAL_DESIGN_DOC)
|
||||
- [testing plan](LINK_TO_TESTING_PLAN)
|
||||
- feature branch: `BRANCH_NAME`
|
||||
- issue links: _related issues, if any_
|
||||
|
||||
### Implementation Plan
|
||||
|
||||
```[tasklist]
|
||||
#### Milestone 1
|
||||
- [ ] _Add tasks here_
|
||||
```
|
||||
13
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -7,16 +7,13 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
**What problem will this feature request solve?**
|
||||
|
||||
**Describe the solution you'd like**
|
||||
**Describe the solution you'd like.**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
**How does this feature request impact you?**
|
||||
For example, is it a minor inconvenience, or does it block important work?
|
||||
|
||||
**How important is this feature to you?**
|
||||
|
||||
**Additional context**
|
||||
**Additional information**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
14
.github/ISSUE_TEMPLATE/task.md
vendored
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Task (Internal Use)
|
||||
about: This issue is used to track a smaller feature/task that does not require
|
||||
being broken down to milestones. This is used by the core Metabase team to
|
||||
project plan and manage work.
|
||||
labels: ".Task"
|
||||
|
||||
---
|
||||
|
||||
**Context**
|
||||
|
||||
|
||||
- issue links: _related issues if any_
|
||||
|
||||
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -25,3 +25,4 @@ _Upload a demo video or before/after screenshots if sensible or remove the secti
|
||||
### Checklist
|
||||
|
||||
- [ ] Tests have been added/updated to cover changes in this PR
|
||||
- [ ] If adding new Loki tests: they pass [stress testing](https://github.com/metabase/metabase/actions/workflows/loki-stress-test-flake-fix.yml)
|
||||
|
||||
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
|
||||
1
.github/actions/prepare-frontend/action.yml
vendored
@@ -1,4 +1,5 @@
|
||||
name: Prepare front-end environment
|
||||
description: "Prepare the front-end environment for the CI build"
|
||||
inputs:
|
||||
node-version:
|
||||
description: "The version of Node.js to use, overriding .nvmrc"
|
||||
|
||||
41
.github/actions/test-driver/action.yml
vendored
@@ -13,54 +13,21 @@ inputs:
|
||||
required: false
|
||||
description: Suffix to append to the test logs artifact name to avoid conflicts in matrix jobs
|
||||
|
||||
outputs:
|
||||
quarantine-skipped:
|
||||
description: Whether the driver test was skipped due to quarantine
|
||||
value: ${{ steps.check-driver.outputs.skip }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Check if driver is ignored
|
||||
id: check-driver
|
||||
shell: bash
|
||||
run: |
|
||||
# If DRIVERS env var is not set, proceed with tests
|
||||
if [ -z "$DRIVERS" ]; then
|
||||
echo "No DRIVERS env var set. Proceeding with tests."
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Fetch CI config and check if driver should be skipped
|
||||
curl -sSf -o ci-test-config.json https://raw.githubusercontent.com/metabase/ci-test-config/refs/heads/master/ci-test-config.json || true
|
||||
|
||||
# Check if the driver is in the ignored list (gracefully handle missing/invalid config)
|
||||
IS_IGNORED=$(jq --arg driver "$DRIVERS" '.ignored.drivers // [] | contains([$driver])' ci-test-config.json 2>/dev/null || echo "false")
|
||||
|
||||
if [ "$IS_IGNORED" = "true" ]; then
|
||||
echo "⚠️ Driver '$DRIVERS' is in the ignored list. Skipping tests."
|
||||
echo "View the ignored list here: https://github.com/metabase/ci-test-config/blob/master/ci-test-config.json"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Driver '$DRIVERS' is not ignored. Proceeding with tests."
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Prepare back-end environment
|
||||
if: steps.check-driver.outputs.skip != 'true'
|
||||
uses: ./.github/actions/prepare-backend
|
||||
- name: Prepare front-end environment
|
||||
if: ${{ steps.check-driver.outputs.skip != 'true' && inputs.build-static-viz == 'true' }}
|
||||
if: ${{ inputs.build-static-viz == 'true' }}
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
- name: Build static viz frontend
|
||||
if: ${{ steps.check-driver.outputs.skip != 'true' && inputs.build-static-viz == 'true' }}
|
||||
if: ${{ inputs.build-static-viz == 'true' }}
|
||||
run: yarn build-static-viz
|
||||
shell: bash
|
||||
env:
|
||||
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 }}
|
||||
shell: bash
|
||||
id: run-test
|
||||
@@ -73,7 +40,7 @@ runs:
|
||||
# As per the documentation: https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#steps-context
|
||||
# test.outcome gives us access to the result before `continue-on-error` overrides it and sets it to 'success'.
|
||||
# We want test reports even if the test step gets cancelled (e.g. times out), hence we match anything that is not a 'success'.
|
||||
if: ${{ steps.run-test.outcome != 'success' && steps.check-driver.outputs.skip != 'true' }}
|
||||
if: ${{ steps.run-test.outcome != 'success' }}
|
||||
with:
|
||||
path: "target/junit/**/*_test.xml"
|
||||
name: JUnit Test Report ${{ inputs.junit-name }}
|
||||
@@ -82,7 +49,7 @@ runs:
|
||||
list-tests: failed
|
||||
fail-on-error: false
|
||||
- name: Upload Logs on Failure
|
||||
if: always() && steps.run-test.outcome != 'success' && steps.check-driver.outputs.skip != 'true'
|
||||
if: always() && steps.run-test.outcome != 'success'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ format('test-logs-{0}{1}', github.job, (inputs.logs-artifact-suffix == '' || inputs.logs-artifact-suffix == null) && '' || format('-{0}', inputs.logs-artifact-suffix)) }}
|
||||
|
||||
40
.github/scripts/find-linear-issue.js
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
async function findLinearIssue({ githubIssueUrl, linearApiKey }) {
|
||||
const response = await fetch("https://api.linear.app/graphql", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: linearApiKey,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
query {
|
||||
attachmentsForURL(url: "${githubIssueUrl}") {
|
||||
nodes {
|
||||
issue { identifier }
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
return { success: false, message: `Linear API error: ${response.status}` };
|
||||
}
|
||||
|
||||
if (!data.data?.attachmentsForURL?.nodes?.length) {
|
||||
return {
|
||||
success: false,
|
||||
message: `No Linear issue linked to ${githubIssueUrl}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
issue: data.data.attachmentsForURL.nodes[0].issue.identifier,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { findLinearIssue };
|
||||
317
.github/scripts/generate-llms-txt.js
vendored
Normal file
@@ -0,0 +1,317 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const REPO = "metabase/metabase";
|
||||
const OUTPUT_FILE = "llms.txt";
|
||||
|
||||
// Sections to generate llms-<name>-full.txt for.
|
||||
// Each section can define a preamble (introductory content before the main docs).
|
||||
const LLMS_FULL_TO_GENERATE = [
|
||||
{
|
||||
section: "embedding",
|
||||
getPreamble: (branch) =>
|
||||
aboveVersion(branch, 57) ? getModularEmbeddingGotchaNotes() : "",
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* List directory recursively to get markdown files
|
||||
*
|
||||
* @param {string} docsPath - full path to docs directory
|
||||
* @returns {Array} list of markdown file objects with fullPath and relativePath
|
||||
*/
|
||||
function getMarkdownFilesList(docsPath) {
|
||||
if (!fs.existsSync(docsPath)) {
|
||||
console.error(`Error: Docs directory '${docsPath}' does not exist`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(docsPath, {
|
||||
recursive: true,
|
||||
withFileTypes: true,
|
||||
});
|
||||
|
||||
return entries
|
||||
.filter((entry) => {
|
||||
return (
|
||||
entry.isFile() &&
|
||||
// Only markdown files
|
||||
entry.name.endsWith(".md") &&
|
||||
// Ignore README.md
|
||||
entry.name !== "README.md" &&
|
||||
// Ignore node_modules and hidden directories
|
||||
!entry.parentPath
|
||||
.split(path.sep)
|
||||
.some((dir) => dir === "node_modules" || dir.startsWith("."))
|
||||
);
|
||||
})
|
||||
.map((entry) => {
|
||||
const fullPath = path
|
||||
.join(entry.parentPath, entry.name)
|
||||
.replace(/\\/g, "/");
|
||||
|
||||
const relativePath = path
|
||||
.relative(docsPath, fullPath)
|
||||
.replace(/\\/g, "/");
|
||||
|
||||
return { fullPath, relativePath };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate llms.txt file with links to all markdown files in docs/
|
||||
*
|
||||
* @param {string} branch - branch name (e.g. 'master' or 'release-x.58.x')
|
||||
* @param {Array} files - list of all markdown file objects
|
||||
* @param {Array<Object>} sectionsConfig - list of section config objects to generate complete references for
|
||||
*/
|
||||
function generateLlmsTxt(branch, files, sectionsConfig) {
|
||||
const baseUrl = `https://raw.githubusercontent.com/${REPO}/refs/heads/${branch}`;
|
||||
|
||||
const filesToExtract = files
|
||||
// Files in embedding/sdk/api/snippets does not exist in the repo.
|
||||
.filter(
|
||||
(file) => !file.relativePath.startsWith("embedding/sdk/api/snippets"),
|
||||
);
|
||||
|
||||
const entries = filesToExtract.map((file) => {
|
||||
const content = fs.readFileSync(file.fullPath, "utf8");
|
||||
const title = extractTitle(content, file.relativePath);
|
||||
const url = `${baseUrl}/docs/${file.relativePath}`;
|
||||
|
||||
return { title, url, path: file.relativePath };
|
||||
});
|
||||
|
||||
// Generate table of contents entries
|
||||
const tableOfContents = entries.map(
|
||||
(entry) => `- [${entry.title}](${entry.url})`,
|
||||
);
|
||||
|
||||
// Generate complete references entries
|
||||
const completeReferences = sectionsConfig
|
||||
.filter((config) =>
|
||||
files.some((file) => file.relativePath.startsWith(`${config.section}/`)),
|
||||
)
|
||||
.map((config) => {
|
||||
const capitalized =
|
||||
config.section.charAt(0).toUpperCase() + config.section.slice(1);
|
||||
return `- [${capitalized} - Complete Reference](${baseUrl}/llms-${config.section}-full.txt)`;
|
||||
});
|
||||
|
||||
// Generate llms.txt content using template literal
|
||||
const content = `# Metabase Documentation
|
||||
Metabase - The simplest, fastest way to get business intelligence and analytics to everyone in your company.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
${tableOfContents.join("\n")}
|
||||
|
||||
## Complete References
|
||||
|
||||
These files are very large and are around 90,000 tokens. Do not use by default unless the context window is huge or RAG is supported in your editor.
|
||||
|
||||
${completeReferences.join("\n")}
|
||||
`;
|
||||
|
||||
fs.writeFileSync(OUTPUT_FILE, content, "utf8");
|
||||
|
||||
console.log(
|
||||
`✅ Generated ${OUTPUT_FILE} with ${entries.length} markdown files`,
|
||||
);
|
||||
console.log(` Branch: ${branch}`);
|
||||
console.log(` Base URL: ${baseUrl}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate llms-full.txt files for specified sections
|
||||
*
|
||||
* @param {string} branch - branch name (e.g., 'master', 'release-x.58.x')
|
||||
* @param {Array} files - list of all markdown file objects
|
||||
* @param {Array<Object>} sectionsConfig - list of section config objects to generate full files for
|
||||
*/
|
||||
function generateLlmsFullFiles(branch, files, sectionsConfig) {
|
||||
const baseUrl = `https://raw.githubusercontent.com/${REPO}/refs/heads/${branch}`;
|
||||
|
||||
sectionsConfig.forEach((config) => {
|
||||
generateLlmsFullTxt(branch, config, files, baseUrl);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* LLMs are likely to pay attention to the very first lines.
|
||||
*
|
||||
* We add the most important context for LLMs to avoid
|
||||
* confusion and pitfalls like out-of-date APIs in trained data.
|
||||
*
|
||||
* @returns {string} Markdown formatted gotcha notes
|
||||
*/
|
||||
function getModularEmbeddingGotchaNotes() {
|
||||
return `> **Important Version Notes**
|
||||
>
|
||||
> Watch out for these deprecated props and gotchas for Metabase 57 onwards, for modular embedding:
|
||||
>
|
||||
> 1. \`config\` prop on MetabaseProvider no longer exist - it is replaced by \`authConfig\`.
|
||||
> 2. \`authProviderUri\` field no longer exist.
|
||||
> 3. \`jwtProviderUri\` optional field only exists in v58+. This is used to make JWT auth faster by skipping the \`GET /auth/sso\` discovery request. Not needed for initial implementation.
|
||||
> 4. \`fetchRequestToken\` is not needed by default. This is only used to customize how the SDK fetches the request token.
|
||||
> 5. Numeric IDs must be integers not strings, e.g. \`dashboardId={1}\`. When the ID is retrieved from the URL and it is numeric, convert it to an integer via \`parseInt\` before passing it to the SDK. IDs can also be strings for entity ids, so you should not parse all IDs as numbers if entity ids are also to be expected.
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate llms-full.txt file for a specific section
|
||||
* @param {string} branch - The branch name
|
||||
* @param {Object} config - Section config object with section name and optional getPreamble function
|
||||
* @param {Array} files - Array of all markdown file objects
|
||||
* @param {string} baseUrl - Base URL for raw GitHub links
|
||||
*/
|
||||
function generateLlmsFullTxt(branch, config, files, baseUrl) {
|
||||
const { section, getPreamble } = config;
|
||||
|
||||
// Filter docs for this section
|
||||
const sectionFiles = files.filter((file) =>
|
||||
file.relativePath.startsWith(`${section}/`),
|
||||
);
|
||||
|
||||
if (sectionFiles.length === 0) {
|
||||
console.log(` Skipping llms-full.txt for ${section} (no files found)`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build content
|
||||
const sectionCapitalized = section.charAt(0).toUpperCase() + section.slice(1);
|
||||
const header = `# Metabase ${sectionCapitalized} - Complete Reference for AI agents
|
||||
|
||||
> Table of contents: ${baseUrl}/${OUTPUT_FILE}
|
||||
|
||||
`;
|
||||
|
||||
// Add preamble if provided (e.g., version-specific notes, warnings)
|
||||
const preamble = getPreamble ? getPreamble(branch) : "";
|
||||
|
||||
// Concatenate all documents
|
||||
const docs = sectionFiles
|
||||
.map((file) => readAndCleanMarkdown(file.fullPath))
|
||||
.join("\n");
|
||||
|
||||
// Write llms-full.txt file at repo root
|
||||
const content = header + preamble + "\n" + docs;
|
||||
const llmsFullPath = `llms-${section}-full.txt`;
|
||||
fs.writeFileSync(llmsFullPath, content, "utf8");
|
||||
|
||||
console.log(
|
||||
`✅ Generated ${llmsFullPath} with ${sectionFiles.length} documents`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract title from markdown file frontmatter or filename.
|
||||
*
|
||||
* We try to extract the title in this order: 1) YAML frontmatter
|
||||
* 2) first H1 heading, and 3) the Markdown file name.
|
||||
*
|
||||
* Example Markdown and frontmatter format:
|
||||
* ```markdown
|
||||
* ---
|
||||
* title: "Getting Started"
|
||||
* description: "A guide"
|
||||
* ---
|
||||
* # Getting Started
|
||||
* ```
|
||||
*
|
||||
* @param {string} content - file content
|
||||
* @param {string} filePath - relative file path
|
||||
* @returns {string} title of content
|
||||
*/
|
||||
function extractTitle(content, filePath) {
|
||||
// Try to extract title from YAML frontmatter
|
||||
// First, match against the YAML frontmatter delimiter `---`
|
||||
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
|
||||
|
||||
if (frontmatterMatch) {
|
||||
// Then, match against the title property
|
||||
// Example: `title: "My Page"`
|
||||
const frontmatter = frontmatterMatch[1];
|
||||
const titleMatch = frontmatter.match(/^title:\s*(.+)$/m);
|
||||
|
||||
// Remove quotes if present
|
||||
if (titleMatch) {
|
||||
return titleMatch[1].trim().replace(/^["']|["']$/g, "");
|
||||
}
|
||||
}
|
||||
|
||||
// Try to extract title from first H1 heading
|
||||
// Example: `# My Page`
|
||||
const h1Match = content.match(/^#\s+(.+)$/m);
|
||||
|
||||
if (h1Match) {
|
||||
return h1Match[1].trim();
|
||||
}
|
||||
|
||||
// Fallback to filename, e.g. `my-page.md`
|
||||
const filename = path.basename(filePath, ".md");
|
||||
|
||||
// Convert kebab-case or snake_case to Title Case
|
||||
// `my-page.md` would become "My Page"
|
||||
return filename
|
||||
.split(/[-_]/)
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and clean markdown file content
|
||||
*
|
||||
* @param {string} filePath - full path to the markdown file
|
||||
* @returns {string} cleaned markdown content with separator
|
||||
*/
|
||||
function readAndCleanMarkdown(filePath) {
|
||||
let content = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
// Strip YAML frontmatter
|
||||
content = content.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, "");
|
||||
|
||||
// Strip Jekyll/Liquid template syntax
|
||||
content = content.replace(/\{%.*?%\}/g, "");
|
||||
content = content.replace(/\{\{.*?\}\}/g, "");
|
||||
|
||||
return `${content.trim()}\n\n---\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if branch version is above target version
|
||||
* @param {string} branch - Branch name (e.g., 'master', 'release-x.58.x')
|
||||
* @param {number} targetVersion - Target version number
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function aboveVersion(branch, targetVersion) {
|
||||
if (branch === "master") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse version from release branch like "release-x.58.x" -> 58
|
||||
const match = branch.match(/release-x\.(\d+)\.x/);
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const version = parseInt(match[1], 10);
|
||||
|
||||
return version >= targetVersion;
|
||||
}
|
||||
|
||||
// Main execution
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
const docsPath = args[0] || "docs";
|
||||
const branch = args[1] || "master";
|
||||
|
||||
const resolvedDocsPath = path.resolve(docsPath);
|
||||
const markdownFiles = getMarkdownFilesList(resolvedDocsPath);
|
||||
|
||||
// Sort all files upfront for consistent ordering across all generated files
|
||||
markdownFiles.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
||||
|
||||
generateLlmsTxt(branch, markdownFiles, LLMS_FULL_TO_GENERATE);
|
||||
generateLlmsFullFiles(branch, markdownFiles, LLMS_FULL_TO_GENERATE);
|
||||
}
|
||||
3
.github/scripts/setup-python-runner.sh
vendored
@@ -19,11 +19,12 @@ while [[ $# -gt 0 ]]; do
|
||||
done
|
||||
|
||||
echo "Cloning python-runner repository..."
|
||||
git clone --depth 1 https://${METABASE_AUTOMATION_USER_TOKEN}@github.com/metabase/python-runner-container.git python-runner
|
||||
git clone https://${METABASE_AUTOMATION_USER_TOKEN}@github.com/metabase/python-runner-container.git python-runner
|
||||
|
||||
if [ "$SKIP_START" = false ]; then
|
||||
echo "Starting python-runner..."
|
||||
cd python-runner
|
||||
git checkout origin/memory_improvements_2
|
||||
make run-ci 1> ../python-runner.out.log 2> ../python-runner.err.log
|
||||
make logs 1>> ../python-runner.out.log 2>> ../python-runner.err.log &
|
||||
cd ..
|
||||
|
||||
4
.github/team.json
vendored
@@ -93,7 +93,9 @@
|
||||
"alxnddr",
|
||||
"bpander",
|
||||
"andsalves",
|
||||
"gantoreno"
|
||||
"gantoreno",
|
||||
"kevinkelleher12",
|
||||
"undrfined"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
30
.github/workflows/claude-code-bug-fix.yml
vendored
@@ -70,6 +70,31 @@ jobs:
|
||||
core.setFailed(error.message);
|
||||
}
|
||||
|
||||
- name: Find linked Linear issue
|
||||
id: find_linear
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
|
||||
ISSUE_NUMBER: ${{ steps.get_issue.outputs.issue_number }}
|
||||
with:
|
||||
script: |
|
||||
const { findLinearIssue } = require('${{ github.workspace }}/.github/scripts/find-linear-issue.js');
|
||||
|
||||
const githubIssueUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/issues/${process.env.ISSUE_NUMBER}`;
|
||||
|
||||
const result = await findLinearIssue({
|
||||
githubIssueUrl,
|
||||
linearApiKey: process.env.LINEAR_API_KEY
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log(`Found Linear issue: ${result.issue}`);
|
||||
core.setOutput('linear_issue', result.issue);
|
||||
} else {
|
||||
console.log(`Could not find Linear issue: ${result.message}`);
|
||||
}
|
||||
|
||||
- name: Set up git configuration
|
||||
run: |
|
||||
git config --global user.name "Claude Code[bot]"
|
||||
@@ -259,6 +284,7 @@ jobs:
|
||||
core.setOutput('reviewers', issueCreator);
|
||||
|
||||
- name: Create Pull Request
|
||||
id: create_pr
|
||||
if: steps.check_changes.outputs.has_changes == 'true'
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
@@ -279,6 +305,8 @@ jobs:
|
||||
body: |
|
||||
This PR automatically fixes the bug reported in #${{ env.ISSUE_NUMBER }}.
|
||||
|
||||
${{ steps.find_linear.outputs.linear_issue && format('Closes {0}', steps.find_linear.outputs.linear_issue) || '' }}
|
||||
|
||||
## Bug Report
|
||||
**Original Issue:** #${{ env.ISSUE_NUMBER }}
|
||||
**Reporter:** @${{ env.ISSUE_AUTHOR }}
|
||||
@@ -299,7 +327,6 @@ jobs:
|
||||
- If issues are found, either fix them or close this PR
|
||||
|
||||
---
|
||||
Closes #${{ env.ISSUE_NUMBER }}
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
|
||||
@@ -309,6 +336,7 @@ jobs:
|
||||
automated-fix
|
||||
ci skip
|
||||
reviewers: ${{ steps.prepare_reviewers.outputs.reviewers }}
|
||||
assignees: ${{ github.actor }}
|
||||
|
||||
- name: Log existing branch update
|
||||
if: steps.check_changes.outputs.has_changes == 'true' && env.BRANCH_EXISTS == 'true'
|
||||
|
||||
2
.github/workflows/claude-docs-review.yml
vendored
@@ -95,6 +95,8 @@ jobs:
|
||||
|
||||
Use the docs-review skill to review all changed documentation files for compliance with the Metabase writing style guide.
|
||||
|
||||
IMPORTANT: Only review markdown files (.md and .mdx). Skip any .txt files or other non-markdown files.
|
||||
|
||||
DO NOT post any comments confirming that something is correct - only post comments for style violations or concerns.
|
||||
|
||||
claude_args: |
|
||||
|
||||
33
.github/workflows/clean-cache.yml
vendored
@@ -1,33 +0,0 @@
|
||||
name: Cleanup caches by a branch
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cleanup
|
||||
run: |
|
||||
gh extension install actions/gh-actions-cache
|
||||
|
||||
REPO=${{ github.repository }}
|
||||
BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge"
|
||||
|
||||
echo "Fetching list of cache key"
|
||||
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 )
|
||||
|
||||
## Setting this to not fail the workflow while deleting cache keys.
|
||||
set +e
|
||||
echo "Deleting caches..."
|
||||
for cacheKey in $cacheKeysForPR
|
||||
do
|
||||
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
|
||||
done
|
||||
echo "Done"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
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
|
||||
|
||||
4
.github/workflows/docs-release-generate.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
target_branch:
|
||||
description: "Target branch for documentation update"
|
||||
required: false
|
||||
default: "release-x.57.x"
|
||||
default: "release-x.58.x"
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
@@ -16,6 +16,6 @@ jobs:
|
||||
name: Call documentation update for release branch
|
||||
uses: ./.github/workflows/docs-generate-base.yml
|
||||
with:
|
||||
target_branch: ${{ inputs.target_branch || 'release-x.57.x' }}
|
||||
target_branch: ${{ inputs.target_branch || 'release-x.58.x' }}
|
||||
secrets:
|
||||
automation_token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
|
||||
325
.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,25 @@ 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_name == 'master' || startsWith(github.ref_name, '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 || 'master' }}"
|
||||
--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') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -65,6 +101,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 +122,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 }}
|
||||
@@ -88,7 +134,6 @@ jobs:
|
||||
|
||||
be-tests-bigquery-cloud-sdk-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -99,6 +144,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 +162,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 +187,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 }}
|
||||
@@ -149,7 +205,6 @@ jobs:
|
||||
|
||||
be-tests-clickhouse-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -173,6 +228,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 +268,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 }}
|
||||
@@ -221,7 +286,6 @@ jobs:
|
||||
|
||||
be-tests-druid-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -236,6 +300,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 +321,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 }}
|
||||
@@ -259,7 +333,6 @@ jobs:
|
||||
|
||||
be-tests-druid-jdbc-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -274,6 +347,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 +368,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 }}
|
||||
@@ -297,7 +380,6 @@ jobs:
|
||||
|
||||
be-tests-databricks-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -312,6 +394,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 +415,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 }}
|
||||
@@ -335,7 +427,6 @@ jobs:
|
||||
|
||||
be-tests-databricks-multi-catalog-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -351,6 +442,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 +463,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 }}
|
||||
@@ -373,7 +474,7 @@ jobs:
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
|
||||
be-tests-h2:
|
||||
if: ${{ !inputs.skip }}
|
||||
needs: determine-driver-skips
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -391,7 +492,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 }}
|
||||
@@ -403,7 +504,6 @@ jobs:
|
||||
|
||||
be-tests-mysql-mariadb:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -521,7 +621,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 }}
|
||||
@@ -539,7 +639,6 @@ jobs:
|
||||
|
||||
be-tests-mongo:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -576,6 +675,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 +700,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 }}
|
||||
@@ -609,7 +718,6 @@ jobs:
|
||||
|
||||
be-tests-mongo-ssl:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -627,6 +735,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 +781,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 }}
|
||||
@@ -675,7 +793,7 @@ jobs:
|
||||
|
||||
be-tests-mongo-sharded-cluster-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -688,6 +806,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 +826,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 }}
|
||||
@@ -710,7 +838,6 @@ jobs:
|
||||
|
||||
be-tests-oracle:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -761,6 +888,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 +910,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 }}
|
||||
@@ -785,7 +922,6 @@ jobs:
|
||||
|
||||
be-tests-postgres:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -876,7 +1012,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 }}
|
||||
@@ -894,7 +1030,6 @@ jobs:
|
||||
|
||||
be-tests-presto-jdbc-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -917,6 +1052,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 +1098,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 }}
|
||||
@@ -965,7 +1110,6 @@ jobs:
|
||||
|
||||
be-tests-redshift-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -1010,6 +1154,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 +1181,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 }}
|
||||
@@ -1045,7 +1199,6 @@ jobs:
|
||||
|
||||
be-tests-snowflake-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -1056,6 +1209,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 +1237,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 +1262,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 }}
|
||||
@@ -1116,7 +1280,6 @@ jobs:
|
||||
|
||||
be-tests-sparksql-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -1129,6 +1292,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 +1313,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 }}
|
||||
@@ -1152,7 +1325,6 @@ jobs:
|
||||
|
||||
be-tests-sqlite-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -1160,6 +1332,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 +1353,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 }}
|
||||
@@ -1183,7 +1365,6 @@ jobs:
|
||||
|
||||
be-tests-sqlserver:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
@@ -1222,6 +1403,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 +1428,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 }}
|
||||
@@ -1294,7 +1485,6 @@ jobs:
|
||||
|
||||
login-to-ecr:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
@@ -1324,7 +1514,6 @@ jobs:
|
||||
|
||||
be-tests-vertica-ee:
|
||||
needs: [determine-driver-skips, login-to-ecr]
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -1340,6 +1529,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 +1552,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 }}
|
||||
|
||||
25
.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
|
||||
|
||||
@@ -125,13 +120,23 @@ jobs:
|
||||
- name: Prepare Cypress environment
|
||||
id: cypress-prep
|
||||
uses: ./.github/actions/prepare-cypress
|
||||
|
||||
- name: Compile CLJS
|
||||
run: yarn build-pure:cljs
|
||||
|
||||
- name: Run Metabase
|
||||
run: node e2e/runner/run_cypress_ci.js start
|
||||
|
||||
- name: Generate database snapshots
|
||||
- name: Generate default database snapshot
|
||||
if: inputs.qa_db == false # true boolean, do not use string here!
|
||||
run: |
|
||||
node e2e/runner/run_cypress_ci.js snapshot \
|
||||
--env grepTags="${{inputs.qa_db && '' || '-@external'}}"
|
||||
--spec "e2e/snapshot-creators/default.cy.snap.js"
|
||||
|
||||
- name: Generate all database snapshots
|
||||
if: inputs.qa_db
|
||||
run: |
|
||||
node e2e/runner/run_cypress_ci.js snapshot
|
||||
|
||||
# For all options and fine-grained control, take a look at the documentation
|
||||
# https://github.com/cypress-io/cypress/tree/develop/npm/grep
|
||||
|
||||
30
.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:
|
||||
@@ -104,6 +104,9 @@ jobs:
|
||||
if: github.run_attempt != '1'
|
||||
run: echo "specs=$(cat failed-specs)" >> $GITHUB_ENV
|
||||
|
||||
- name: Compile CLJS
|
||||
run: yarn build-pure:cljs
|
||||
|
||||
- name: Run Metabase
|
||||
run: node e2e/runner/run_cypress_ci.js start
|
||||
|
||||
@@ -113,6 +116,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 +140,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run Tagged EE Cypress tests on ${{ inputs.name }}
|
||||
id: tagged-e2e
|
||||
if: ${{ inputs.specs }}
|
||||
shell: bash
|
||||
env:
|
||||
@@ -168,7 +173,8 @@ jobs:
|
||||
|
||||
- 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 +184,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
|
||||
@@ -149,8 +150,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 +158,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
|
||||
|
||||
@@ -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
|
||||
@@ -237,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
|
||||
|
||||
198
.github/workflows/llms-txt-generate.yml
vendored
Normal file
@@ -0,0 +1,198 @@
|
||||
name: Generate llms.txt file
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Metabase version (e.g. v0.57.0)'
|
||||
type: string
|
||||
required: true
|
||||
branch:
|
||||
description: 'Target branch (e.g. release-x.57.x)'
|
||||
type: string
|
||||
required: true
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Metabase version (e.g. v0.57.0)'
|
||||
type: string
|
||||
required: true
|
||||
branch:
|
||||
description: 'Target branch (e.g. release-x.57.x)'
|
||||
type: string
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
check-version:
|
||||
name: Check if minor or major release
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
should_generate: ${{ steps.check.outputs.should_generate }}
|
||||
target_branch: ${{ steps.check.outputs.target_branch }}
|
||||
version: ${{ steps.check.outputs.version }}
|
||||
steps:
|
||||
- name: Check version
|
||||
id: check
|
||||
run: |
|
||||
VERSION="${{ inputs.version }}"
|
||||
BRANCH="${{ inputs.branch }}"
|
||||
|
||||
# Extract version number (e.g., v0.57.0 -> 0.57.0)
|
||||
VERSION_NUM="${VERSION#v}"
|
||||
|
||||
# Count dots to determine version type
|
||||
# Metabase format: v0.MAJOR.MINOR (3 segments) or v0.MAJOR.MINOR.PATCH (4 segments)
|
||||
DOT_COUNT=$(echo "$VERSION_NUM" | tr -cd '.' | wc -c | tr -d ' ')
|
||||
|
||||
# Only proceed if this is a minor or major release (3 segments)
|
||||
# Examples of minor/major releases: v0.57.8, v0.59.0
|
||||
# Skip for patch releases (4 segments): v0.57.8.1
|
||||
# Note: This is a safety check even though the workflow should only be called
|
||||
# for minor/major releases, just in case the workflow is dispatched manually.
|
||||
if [ "$DOT_COUNT" -eq 3 ]; then
|
||||
echo "⏭️ This is a patch release ($VERSION). Skipping llms.txt generation."
|
||||
echo "should_generate=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "✅ This is a minor or major release ($VERSION). Proceeding with llms.txt generation for branch: $BRANCH"
|
||||
echo "should_generate=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
echo "target_branch=$BRANCH" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
generate-llms-txt:
|
||||
name: Generate llms.txt
|
||||
needs: check-version
|
||||
if: needs.check-version.outputs.should_generate == 'true'
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
|
||||
- name: Get the generate-llms-txt.js node script via sparse checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
ref: ${{ github.ref }}
|
||||
sparse-checkout: |
|
||||
.github/scripts
|
||||
path: workflow-scripts
|
||||
|
||||
- name: Checkout target branch for docs
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
ref: ${{ needs.check-version.outputs.target_branch }}
|
||||
path: metabase
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Generate llms.txt
|
||||
id: generate
|
||||
run: |
|
||||
node workflow-scripts/.github/scripts/generate-llms-txt.js metabase/docs ${{ needs.check-version.outputs.target_branch }}
|
||||
|
||||
- name: Sanity check generated files
|
||||
run: |
|
||||
if [ ! -f "llms.txt" ]; then
|
||||
echo "❌ Error: llms.txt was not generated"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "llms-embedding-full.txt" ]; then
|
||||
echo "❌ Error: llms-embedding-full.txt was not generated"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check that llms.txt contains URLs with the expected GitHub raw URL prefix
|
||||
URL_COUNT=$(grep -o "https://raw.githubusercontent.com/metabase/metabase/refs/heads/${{ needs.check-version.outputs.target_branch }}/docs" llms.txt | wc -l)
|
||||
if [ "$URL_COUNT" -lt 10 ]; then
|
||||
echo "❌ Error: llms.txt appears to be malformed. Found only $URL_COUNT URLs (expected at least 10)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Both llms.txt and llms-embedding-full.txt were generated successfully"
|
||||
echo "✅ llms.txt contains $URL_COUNT documentation URLs"
|
||||
|
||||
- name: Set up git configuration
|
||||
working-directory: metabase
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
BRANCH_NAME="llms-txt/auto-update-${{ needs.check-version.outputs.target_branch }}-$(date +%Y%m%d-%H%M%S)"
|
||||
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
|
||||
echo "Branch name will be: $BRANCH_NAME"
|
||||
|
||||
- name: Copy generated files to metabase repo
|
||||
run: |
|
||||
cp llms.txt metabase/
|
||||
cp llms-embedding-full.txt metabase/
|
||||
|
||||
- name: Create Pull Request
|
||||
id: create_pr
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
path: metabase
|
||||
branch: ${{ env.BRANCH_NAME }}
|
||||
base: ${{ needs.check-version.outputs.target_branch }}
|
||||
commit-message: |
|
||||
Update llms.txt and llms-full.txt (${{ needs.check-version.outputs.target_branch }})
|
||||
|
||||
Regenerated llms.txt to include all markdown files from the docs/ directory.
|
||||
Also generated llms-full.txt files for the embedding section.
|
||||
|
||||
🤖 Generated automatically by llms-txt-generate workflow
|
||||
|
||||
Co-Authored-By: GitHub Actions <actions@github.com>
|
||||
committer: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
|
||||
author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
|
||||
title: "[chore] Update llms.txt for ${{ needs.check-version.outputs.target_branch }}"
|
||||
body: |
|
||||
## Summary
|
||||
This PR contains automatically generated updates to `llms.txt` and `llms-embedding-full.txt` files, triggered by a minor or major release.
|
||||
|
||||
## Changes
|
||||
- The `llms.txt` file has been regenerated to include all markdown files from the `docs/` directory.
|
||||
- The `llms-embedding-full.txt` file has been generated for the embedding section (concatenated documentation for RAG).
|
||||
|
||||
## Generation
|
||||
These files were generated using:
|
||||
- Script: `.github/scripts/generate-llms-txt.js`
|
||||
- Branch: `${{ needs.check-version.outputs.target_branch }}`
|
||||
- Base URL: `https://raw.githubusercontent.com/metabase/metabase/refs/heads/${{ needs.check-version.outputs.target_branch }}`
|
||||
|
||||
Please review the changes and merge if they look correct.
|
||||
|
||||
🤖 Generated automatically by the `llms-txt-generate` workflow.
|
||||
|
||||
Co-Authored-By: GitHub Actions <actions@github.com>
|
||||
labels: |
|
||||
Type:Documentation
|
||||
no-backport
|
||||
add-paths: |
|
||||
llms.txt
|
||||
llms-embedding-full.txt
|
||||
|
||||
- name: Auto approve PR
|
||||
if: steps.create_pr.outputs.pull-request-number != ''
|
||||
working-directory: metabase
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh pr review "${{ steps.create_pr.outputs.pull-request-number }}" --approve --body "Auto-approved by llms-txt-generate workflow"
|
||||
|
||||
- name: Auto merge PR
|
||||
if: steps.create_pr.outputs.pull-request-number != ''
|
||||
working-directory: metabase
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
run: |
|
||||
gh pr merge "${{ steps.create_pr.outputs.pull-request-number }}" --auto --squash
|
||||
114
.github/workflows/loki-stress-test-flake-fix.yml
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
name: Loki Visual Stress Test Flake Fix
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
stories_filter:
|
||||
description: "Regex pattern to filter stories (e.g. 'DataGrid' or 'static-viz|viz')"
|
||||
type: string
|
||||
required: true
|
||||
burn_in:
|
||||
description: "Number of times to run the test (e.g. 50)"
|
||||
type: string
|
||||
required: true
|
||||
default: "50"
|
||||
|
||||
jobs:
|
||||
workflow-summary:
|
||||
name: Stress test inputs
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Generate workflow summary
|
||||
run: |
|
||||
echo '**Inputs:**' >> $GITHUB_STEP_SUMMARY
|
||||
echo '' >> $GITHUB_STEP_SUMMARY
|
||||
echo '- `branch`: ${{ github.ref_name }}' >> $GITHUB_STEP_SUMMARY
|
||||
echo '- `stories_filter`: ${{ inputs.stories_filter }}' >> $GITHUB_STEP_SUMMARY
|
||||
echo '- `burn_in`: ${{ inputs.burn_in }}' >> $GITHUB_STEP_SUMMARY
|
||||
echo '' >> $GITHUB_STEP_SUMMARY
|
||||
echo 'triggered by: @${{ github.event.sender.login }}' >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
stress-test-loki:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
name: Stress test Loki visual tests
|
||||
services:
|
||||
docker:
|
||||
image: docker:19.03.12
|
||||
options: --privileged
|
||||
ports:
|
||||
- 2376:2376
|
||||
env:
|
||||
DOCKER_TLS_CERTDIR: /certs
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /tmp/docker-certs:/certs/client
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Prepare frontend environment
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
|
||||
- name: Compile CLJS
|
||||
run: NODE_ENV=development yarn build-pure:cljs
|
||||
|
||||
- name: Build Storybook (once)
|
||||
run: yarn build-storybook
|
||||
|
||||
- name: Stress-test Loki "${{ inputs.stories_filter }}" ${{ inputs.burn_in }} times
|
||||
run: |
|
||||
BURN_IN=${{ inputs.burn_in }}
|
||||
STORIES_FILTER='${{ inputs.stories_filter }}'
|
||||
|
||||
echo "Running Loki visual tests $BURN_IN times for stories matching: $STORIES_FILTER"
|
||||
echo ""
|
||||
|
||||
FAILED=0
|
||||
for i in $(seq 1 $BURN_IN); do
|
||||
echo "=========================================="
|
||||
echo "Run $i of $BURN_IN"
|
||||
echo "=========================================="
|
||||
|
||||
if yarn test-visual:loki \
|
||||
--reactUri file:./storybook-static \
|
||||
--verboseRenderer \
|
||||
--storiesFilter "$STORIES_FILTER"; then
|
||||
echo "✅ Run $i passed"
|
||||
else
|
||||
echo "❌ Run $i FAILED"
|
||||
FAILED=$((FAILED + 1))
|
||||
|
||||
mkdir -p .loki/failures/run-$i
|
||||
cp -r .loki/difference/* .loki/failures/run-$i/ 2>/dev/null || true
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "=========================================="
|
||||
echo "Summary: $((BURN_IN - FAILED))/$BURN_IN passed, $FAILED failed"
|
||||
echo "=========================================="
|
||||
|
||||
if [ $FAILED -gt 0 ]; then
|
||||
echo "::error::$FAILED out of $BURN_IN runs failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Generate Visual Report on Failure
|
||||
if: failure()
|
||||
run: yarn test-visual:loki-report
|
||||
|
||||
- name: Upload Artifact
|
||||
id: artifact-upload-step
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: loki-stress-test-report
|
||||
include-hidden-files: true
|
||||
path: .loki/
|
||||
if-no-files-found: ignore
|
||||
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
|
||||
13
.github/workflows/release.yml
vendored
@@ -424,6 +424,19 @@ jobs:
|
||||
}
|
||||
});
|
||||
|
||||
# updates the llms.txt asynchronously.
|
||||
# release does not need to wait for this PR to be merged,
|
||||
# because the llms.txt is in the main git repo.
|
||||
trigger-update-llms-txt:
|
||||
# do not trigger llms.txt update on automatic patch releases
|
||||
if: ${{ !inputs.auto }}
|
||||
needs: add-extra-tags
|
||||
uses: ./.github/workflows/llms-txt-generate.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
version: ${{ inputs.version }}
|
||||
branch: ${{ github.ref_name }}
|
||||
|
||||
draft-release-notes:
|
||||
if: ${{ !inputs.auto }}
|
||||
needs: add-extra-tags
|
||||
|
||||
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
|
||||
|
||||
6
.github/workflows/translation-update.yml
vendored
@@ -15,8 +15,8 @@ jobs:
|
||||
BRANCH_NAME: update-translations-${{ github.run_id }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GITHUB_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
- name: Auto approve PR
|
||||
uses: juliangruber/approve-pull-request-action@v1
|
||||
with:
|
||||
github-token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
github-token: ${{ github.token }}
|
||||
number: ${{ steps.create_pr.outputs.pr_number }}
|
||||
|
||||
- name: Enable Pull Request Automerge
|
||||
|
||||
2
.github/workflows/whitespace.yml
vendored
@@ -16,5 +16,5 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
- run: clojure -T:whitespace-linter lint
|
||||
- run: clojure -T:whitespace-linter lint :whitespace-linter/progress-bar false
|
||||
name: Run Whitespace Linter
|
||||
|
||||
4
.gitignore
vendored
@@ -21,7 +21,8 @@
|
||||
/*.mv.db
|
||||
/*.trace.db
|
||||
/.babel_cache
|
||||
/.env
|
||||
/.env*
|
||||
!/.env.example
|
||||
/.envrc
|
||||
/.lein-env
|
||||
/.lein-failures
|
||||
@@ -105,7 +106,6 @@ dev/serialization_deltas/
|
||||
/trace-output/
|
||||
/temp-types/
|
||||
/frontend/src/metabase-types/openapi
|
||||
/resources/openapi/openapi.json
|
||||
|
||||
**/cypress_sample_database.json
|
||||
**/cypress_sample_instance_data.json
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn precommit
|
||||
|
||||
# Run the formatter
|
||||
@@ -9,4 +6,4 @@ yarn precommit
|
||||
# ATM, you can manually run ./bin/cljfmt_staged.sh if you want to format locally staged files and inspect after.
|
||||
# ./bin/cljfmt_staged.sh
|
||||
|
||||
./hooks/pre-commit.nocommit
|
||||
./hooks/pre-commit.nocommit
|
||||
|
||||
|
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: 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: 30 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
BIN
.loki/reference/chrome_laptop_viz_PieChart_Dark_Theme.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
.loki/reference/chrome_laptop_viz_PieChart_Embedded_Dashcard.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
.loki/reference/chrome_laptop_viz_PieChart_Embedded_Question.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -1,4 +1,5 @@
|
||||
import React, { useMemo, useEffect } from "react";
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
|
||||
import { ThemeProvider } from "metabase/ui";
|
||||
|
||||
// @ts-expect-error: See metabase/lib/delay
|
||||
@@ -15,10 +16,13 @@ import "@mantine/dates/styles.css";
|
||||
|
||||
import { EmotionCacheProvider } from "metabase/styled-components/components/EmotionCacheProvider";
|
||||
import { getMetabaseCssVariables } from "metabase/styled-components/theme/css-variables";
|
||||
import { css, Global, useTheme } from "@emotion/react";
|
||||
|
||||
import { Global, css, useTheme } from "@emotion/react";
|
||||
|
||||
import { baseStyle, rootStyle } from "metabase/css/core/base.styled";
|
||||
import { defaultFontFiles } from "metabase/css/core/fonts.styled";
|
||||
import { saveDomImageStyles } from "metabase/visualizations/lib/image-exports";
|
||||
|
||||
import { initialize, mswLoader } from "msw-storybook-addon";
|
||||
|
||||
// Note: Changing the names of the stories may impact loki visual testing. Please ensure that
|
||||
@@ -107,7 +111,9 @@ function CssVariables() {
|
||||
}, []);
|
||||
|
||||
// This can get expensive so we should memoize it separately
|
||||
const cssVariables = useMemo(() => getMetabaseCssVariables(theme), [theme]);
|
||||
const cssVariables = useMemo(() => {
|
||||
return getMetabaseCssVariables({ theme });
|
||||
}, [theme]);
|
||||
|
||||
const styles = useMemo(() => {
|
||||
return css`
|
||||
@@ -120,8 +126,8 @@ function CssVariables() {
|
||||
Theming-specific CSS variables.
|
||||
These CSS variables are not part of the core design system colors.
|
||||
**/
|
||||
--mb-color-bg-dashboard: var(--mb-color-bg-white);
|
||||
--mb-color-bg-dashboard-card: var(--mb-color-bg-white);
|
||||
--mb-color-bg-dashboard: var(--mb-color-background-primary);
|
||||
--mb-color-bg-dashboard-card: var(--mb-color-background-primary);
|
||||
}
|
||||
|
||||
/* For Embed frame questions to render properly */
|
||||
|
||||
@@ -19,6 +19,10 @@ For detailed guidance on writing and reviewing code and documentation, see the s
|
||||
- **[docs-write](.claude/skills/docs-write/SKILL.md)** - Documentation writing with Metabase style guide
|
||||
- **[docs-review](.claude/skills/docs-review/SKILL.md)** - Documentation review checklist
|
||||
|
||||
## Frontend
|
||||
|
||||
- **[analytics-events](.claude/skills/analytics-events/SKILL.md)** - Add product analytics events to track user interactions
|
||||
|
||||
## Tool Preferences
|
||||
|
||||
If `clojure-mcp` tools are available, prefer them over shell-based alternatives for Clojure development.
|
||||
|
||||
@@ -97,7 +97,7 @@ This repository contains the source code for both the Open Source edition of Met
|
||||
|
||||
See [LICENSE.txt](./LICENSE.txt) for details.
|
||||
|
||||
Unless otherwise noted, all files © 2025 Metabase, Inc.
|
||||
Unless otherwise noted, all files © 2026 Metabase, Inc.
|
||||
|
||||
## Community
|
||||
|
||||
|
||||
435
bb.edn
@@ -1,15 +1,15 @@
|
||||
{:min-bb-version "1.12.212"
|
||||
;; we put path as bin, and everything is in the ./mage subdirectory,
|
||||
;; so the namespaces are mage.cli, mage.format, etc.
|
||||
:paths ["mage/src" "mage/test" "bin/lint-migrations-file/src"]
|
||||
:deps {dev.weavejester/cljfmt {:mvn/version "0.15.3"} ;; run cljfmt from bb
|
||||
io.github.paintparty/bling {:mvn/version "0.8.8"} ;; printing bells and whistles
|
||||
medley/medley {:mvn/version "1.3.0"} ;; utilities
|
||||
metosin/malli {:mvn/version "0.17.0"} ;; data validation
|
||||
mvxcvi/puget {:mvn/version "1.3.4"} ;; pretty printing
|
||||
org.babashka/json {:mvn/version "0.1.6"} ;; json parsing
|
||||
table/table {:mvn/version "0.5.0"} ;; table printing
|
||||
}
|
||||
:paths ["mage/src" "mage/test" "bin/lint-migrations-file/src"]
|
||||
:deps {dev.weavejester/cljfmt {:mvn/version "0.15.3"} ;; run cljfmt from bb
|
||||
io.github.paintparty/bling {:mvn/version "0.8.8"} ;; printing bells and whistles
|
||||
medley/medley {:mvn/version "1.3.0"} ;; utilities
|
||||
metosin/malli {:mvn/version "0.17.0"} ;; data validation
|
||||
mvxcvi/puget {:mvn/version "1.3.4"} ;; pretty printing
|
||||
org.babashka/json {:mvn/version "0.1.6"} ;; json parsing
|
||||
table/table {:mvn/version "0.5.0"} ;; table printing
|
||||
}
|
||||
:tasks
|
||||
{:requires [[mage.cli :as cli]
|
||||
[mage.color :as c]]
|
||||
@@ -21,214 +21,229 @@
|
||||
(cons 'do body))))
|
||||
|
||||
alias
|
||||
{:doc "Installs 'mage' alias and autocomplete"
|
||||
:examples [["./bin/mage alias" "Prints instructions to install the 'mage' alias and autocomplete"]
|
||||
["./bin/mage alias install fish" "Installs mage alias for fish shell"]
|
||||
["./bin/mage alias print zsh" "Prints shell script to put into .zshrc for zsh users"]]
|
||||
:requires [[mage.alias :as alias]]
|
||||
{:doc "Installs 'mage' alias and autocomplete"
|
||||
:examples [["./bin/mage alias" "Prints instructions to install the 'mage' alias and autocomplete"]
|
||||
["./bin/mage alias install fish" "Installs mage alias for fish shell"]
|
||||
["./bin/mage alias print zsh" "Prints shell script to put into .zshrc for zsh users"]]
|
||||
:requires [[mage.alias :as alias]]
|
||||
:arg-schema [:or
|
||||
[:tuple [:enum {:name "shell-type"} "bash" "zsh" "fish"]]
|
||||
[:tuple [:enum {:name "mode"} "install" "print"]
|
||||
[:enum {:name "shell-type"} "bash" "zsh" "fish"]]]
|
||||
:task (task! (alias/instructions parsed))}
|
||||
[:enum {:name "shell-type"} "bash" "zsh" "fish"]]]
|
||||
:task (task! (alias/instructions parsed))}
|
||||
|
||||
cljfmt-staged
|
||||
{:doc "Runs cljfmt on staged files"
|
||||
{:doc "Runs cljfmt on staged files"
|
||||
:requires [[mage.format :as format]]
|
||||
:examples [["./bin/mage cljfmt-staged" "Format staged files"]
|
||||
["./bin/mage cljfmt-staged -c" "Check staged files"]]
|
||||
:options [["-c" "--force-check" "Check staged files"]]
|
||||
:task (task! (format/staged parsed))}
|
||||
:options [["-c" "--force-check" "Check staged files"]]
|
||||
:task (task! (format/staged parsed))}
|
||||
|
||||
cljfmt-files
|
||||
{:doc "Runs cljfmt on the given files/directories"
|
||||
:requires [[mage.format :as format]]
|
||||
:examples [["./bin/mage cljfmt-files src/metabase/events.clj" "Format events.clj"]
|
||||
["./bin/mage cljfmt-files src" "Format all files in src"]
|
||||
["./bin/mage cljfmt-files -c src" "Check all files in src"]]
|
||||
:options [["-c" "--force-check" "Check staged files"]]
|
||||
{:doc "Runs cljfmt on the given files/directories"
|
||||
:requires [[mage.format :as format]]
|
||||
:examples [["./bin/mage cljfmt-files src/metabase/events.clj" "Format events.clj"]
|
||||
["./bin/mage cljfmt-files src" "Format all files in src"]
|
||||
["./bin/mage cljfmt-files -c src" "Check all files in src"]]
|
||||
:options [["-c" "--force-check" "Check staged files"]]
|
||||
:arg-schema [:sequential [:string {:name "file-or-directory"
|
||||
:description "Files or directories to format."}]]
|
||||
:task (task! (format/files parsed))}
|
||||
:task (task! (format/files parsed))}
|
||||
|
||||
cljfmt-all
|
||||
{:doc "Runs cljfmt on all (clojure) files"
|
||||
{:doc "Runs cljfmt on all (clojure) files"
|
||||
:requires [[mage.format :as format]]
|
||||
:examples [["./bin/mage cljfmt-all" "Format all files"]]
|
||||
:options [["-c" "--force-check" "Check staged files"]]
|
||||
:task (task! (format/all parsed))}
|
||||
:options [["-c" "--force-check" "Check staged files"]]
|
||||
:task (task! (format/all parsed))}
|
||||
|
||||
cljfmt-updated
|
||||
{:doc "Runs cljfmt on all (Clojure) files relative to a git ref (default HEAD)"
|
||||
:requires [[mage.format :as format]]
|
||||
:examples [["./bin/mage cljfmt-updated" "Format updated files relative to HEAD"]
|
||||
["./bin/mage cljfmt-updated master" "Format updated files relative to master"]]
|
||||
:options [["-c" "--force-check" "Check staged files"]]
|
||||
{:doc "Runs cljfmt on all (Clojure) files relative to a git ref (default HEAD)"
|
||||
:requires [[mage.format :as format]]
|
||||
:examples [["./bin/mage cljfmt-updated" "Format updated files relative to HEAD"]
|
||||
["./bin/mage cljfmt-updated master" "Format updated files relative to master"]]
|
||||
:options [["-c" "--force-check" "Check staged files"]]
|
||||
:arg-schema [:or [:tuple] [:tuple [:string {:name "git-ref" :description "Git ref to compare against"}]]]
|
||||
:task (task! (format/updated parsed))}
|
||||
:task (task! (format/updated parsed))}
|
||||
|
||||
kondo
|
||||
{:doc "Runs Kondo against a file, directory, or everything we usually lint"
|
||||
{:doc "Runs Kondo against a file, directory, or everything we usually lint"
|
||||
:examples [["./bin/mage kondo" "run Kondo against everything we usually lint"]
|
||||
["./bin/mage kondo src/metabase/db.clj" "run Kondo against a file"]
|
||||
["./bin/mage kondo src/metabase/db.clj src/metabase/config.clj" "run Kondo against 2 files"]
|
||||
["./bin/mage kondo src/metabase/api/macros" "run Kondo against a directory"]]
|
||||
:requires [[mage.kondo :as kondo]]
|
||||
:task (task! (kondo/kondo (:arguments parsed)))}
|
||||
:task (task! (kondo/kondo (:arguments parsed)))}
|
||||
|
||||
kondo-updated
|
||||
{:doc "Runs kondo against files changed compared to a git ref"
|
||||
{:doc "Runs kondo against files changed compared to a git ref"
|
||||
:examples [["./bin/mage kondo-updated" "run Kondo on files with changes relative to HEAD"]
|
||||
["./bin/mage kondo-updated master" "run Kondo on files with changes relative to master"]]
|
||||
:requires [[mage.kondo :as kondo]]
|
||||
:task (task! (kondo/kondo-updated (:arguments parsed)))}
|
||||
:task (task! (kondo/kondo-updated (:arguments parsed)))}
|
||||
|
||||
fix-unused-requires
|
||||
{:doc "Removes unused requires flagged by clj-kondo"
|
||||
:examples [["./bin/mage fix-unused-requires src/metabase/lib/validate.cljc" "Fix unused requires in a specific file"]
|
||||
["./bin/mage fix-unused-requires src" "Fix unused requires in all files in src"]]
|
||||
:requires [[mage.fix-unused-requires :as fix]]
|
||||
:arg-schema [:sequential [:string {:name "file-or-directory"
|
||||
:description "Files or directories to fix unused requires in"}]]
|
||||
:task (task! (fix/fix-files parsed))}
|
||||
|
||||
check
|
||||
{:doc "Checks whether we can compile all source files (and finds circular dependencies)"
|
||||
{:doc "Checks whether we can compile all source files (and finds circular dependencies)"
|
||||
:examples [["./bin/mage check"]]
|
||||
:requires [[mage.check]]
|
||||
:task (task! (mage.check/check (:arguments parsed)))}
|
||||
:task (task! (mage.check/check (:arguments parsed)))}
|
||||
|
||||
codespell
|
||||
{:doc "Check for misspellings in the source code."
|
||||
{:doc "Check for misspellings in the source code."
|
||||
:examples [["./bin/mage codespell"]]
|
||||
:requires [[mage.codespell]]
|
||||
:task (task! (mage.codespell/codespell (:arguments parsed)))}
|
||||
:task (task! (mage.codespell/codespell (:arguments parsed)))}
|
||||
|
||||
codespell-interactive
|
||||
{:doc "Check for misspellings in the source code, and interactively prompt to fix them."
|
||||
{:doc "Check for misspellings in the source code, and interactively prompt to fix them."
|
||||
:examples [["./bin/mage codespell-interactive"]]
|
||||
:requires [[mage.codespell]]
|
||||
:task (task! (mage.codespell/codespell-interactive (:arguments parsed)))}
|
||||
:task (task! (mage.codespell/codespell-interactive (:arguments parsed)))}
|
||||
|
||||
start-maildev
|
||||
{:doc "Start Maildev"
|
||||
{:doc "Start Maildev"
|
||||
:examples [["./bin/mage start-maildev -h" "print help for start-maildev"]]
|
||||
:requires [[mage.start-maildev :as start-maildev]]
|
||||
:task (task! (start-maildev/start-maildev!))}
|
||||
:task (task! (start-maildev/start-maildev!))}
|
||||
|
||||
start-python-runner
|
||||
{:doc "Start python-runner container with internal Moto S3 server for Python transforms"
|
||||
:examples [["./bin/mage start-python-runner" "start python-runner with internal S3"]]
|
||||
:requires [[mage.start-python-runner :as start-python-runner]]
|
||||
:task (task! (start-python-runner/start-python-runner!))}
|
||||
|
||||
start-db
|
||||
{:doc "Start a db on a default port in docker"
|
||||
:examples [["./bin/mage start-db postgres latest" "start the latest postgres db we support"]
|
||||
["./bin/mage start-db mysql oldest" "start the oldest mysql db we support"]]
|
||||
:requires [[mage.start-db :as start-db]]
|
||||
{:doc "Start a db on a default port in docker"
|
||||
:examples [["./bin/mage start-db postgres latest" "start the latest postgres db we support"]
|
||||
["./bin/mage start-db mysql oldest" "start the oldest mysql db we support"]]
|
||||
:requires [[mage.start-db :as start-db]]
|
||||
:arg-schema [:tuple
|
||||
[:enum {:name "db"} :postgres :mysql :mariadb :mongo]
|
||||
[:enum {:name "relative-version"} :oldest :latest]]
|
||||
:db-info {:postgres {:ports {:oldest 5432 :latest 5433} :eol-url "https://endoflife.date/api/postgres.json"}
|
||||
:mysql {:ports {:oldest 3308 :latest 3309} :eol-url "https://endoflife.date/api/mysql.json"}
|
||||
:mariadb {:ports {:oldest 3306 :latest 3307} :eol-url "https://endoflife.date/api/mariadb.json"}
|
||||
:mongo {:ports {:oldest 27017 :latest 27018} :eol-url "https://endoflife.date/api/mongodb.json"}}
|
||||
:usage-fn start-db/usage
|
||||
:task (task!
|
||||
(let [[db version] (:arguments parsed)]
|
||||
(start-db/start-db (:db-info (current-task)) db version)))}
|
||||
:db-info {:postgres {:ports {:oldest 5432 :latest 5433} :eol-url "https://endoflife.date/api/postgres.json"}
|
||||
:mysql {:ports {:oldest 3308 :latest 3309} :eol-url "https://endoflife.date/api/mysql.json"}
|
||||
:mariadb {:ports {:oldest 3306 :latest 3307} :eol-url "https://endoflife.date/api/mariadb.json"}
|
||||
:mongo {:ports {:oldest 27017 :latest 27018} :eol-url "https://endoflife.date/api/mongodb.json"}}
|
||||
:usage-fn start-db/usage
|
||||
:task (task!
|
||||
(let [[db version] (:arguments parsed)]
|
||||
(start-db/start-db (:db-info (current-task)) db version)))}
|
||||
|
||||
nrepl
|
||||
{:doc "Starts the babashka nrepl: helpful for mage development"
|
||||
{:doc "Starts the babashka nrepl: helpful for mage development"
|
||||
:requires [[babashka.nrepl.server :as nrepl.server]
|
||||
[mage.color :as c]]
|
||||
:examples [["./bin/mage nrepl" "Starts the nrepl server"]
|
||||
["./bin/mage nrepl --port 50506" "Starts the nrepl server"]
|
||||
["./bin/mage nrepl -p 50506" "Starts the nrepl server"]]
|
||||
:options [["-p" "--port PORT" "optionally provide a port to run the nrepl server on" :default 1667 :parse-fn Integer/parseInt]]
|
||||
:task (task!
|
||||
(let [{:keys [port]} (:options parsed)]
|
||||
(spit ".nrepl-port" port)
|
||||
(nrepl.server/start-server! {:port port})
|
||||
(deref (promise))))}
|
||||
:options [["-p" "--port PORT" "optionally provide a port to run the nrepl server on" :default 1667 :parse-fn Integer/parseInt]]
|
||||
:task (task!
|
||||
(let [{:keys [port]} (:options parsed)]
|
||||
(spit ".nrepl-port" port)
|
||||
(nrepl.server/start-server! {:port port})
|
||||
(deref (promise))))}
|
||||
|
||||
lint-migrations
|
||||
{:doc "Lint migrations files"
|
||||
:requires [[lint-migrations-file]
|
||||
[babashka.process :as p]]
|
||||
:examples [["./bin/mage lint-migrations" "Lint all migrations and print report"]]
|
||||
:task (do (cli/parse! (current-task))
|
||||
(p/shell "bin/pre-lint-migrations-file.sh")
|
||||
(lint-migrations-file/-main))}
|
||||
{:doc "Lint migrations files"
|
||||
:requires [[lint-migrations-file]
|
||||
[babashka.process :as p]]
|
||||
:examples [["./bin/mage lint-migrations" "Lint all migrations and print report"]]
|
||||
:task (do (cli/parse! (current-task))
|
||||
(p/shell "bin/pre-lint-migrations-file.sh")
|
||||
(lint-migrations-file/-main))}
|
||||
|
||||
jar-download
|
||||
{:doc "Download (and optionally run) jar for a metabase version or branch"
|
||||
:examples [["./bin/mage jar-download 50" "Download the latest enterprise version of release 50 to ~/path/to/my/metabase/jars"]
|
||||
["./bin/mage jar-download 1.45.2 -r" "Download and run metabase_1.45.2.jar"]
|
||||
["./bin/mage jar-download 1.45.2 -d ~/path/to/my/jars" "Download metabase_1.45.2.jar to ~/path/to/my/jars, deleting if it exists"]
|
||||
["./bin/mage jar-download master --run --port 3001" "Download and run the latest master branch jar on port 3001"]
|
||||
["JARS=~/my-stuff ./bin/mage jar-download 1.45.2" "Download metabase_1.45.2.jar to ~/my-stuff"]
|
||||
["./bin/mage jar-download v1.56.0-beta -r" "Download and run metabase_1.56.0-beta.jar"]]
|
||||
:requires [[mage.jar-download :as jar-download]]
|
||||
:options [["-r" "--run" "run the new jar after downloading it"]
|
||||
["-e" "--env-file FILE" "you can optionally provide a lein-env file to specify the environment to use when running the jar."]
|
||||
["-d" "--delete" "delete the old jar if found, by default does not re-download it"]
|
||||
["-p" "--port PORT" "you can optionally provide a port to run the jar on, if not, we use a sane port."]]
|
||||
{:doc "Download (and optionally run) jar for a metabase version or branch"
|
||||
:examples [["./bin/mage jar-download 50" "Download the latest enterprise version of release 50 to ~/path/to/my/metabase/jars"]
|
||||
["./bin/mage jar-download 1.45.2 -r" "Download and run metabase_1.45.2.jar"]
|
||||
["./bin/mage jar-download 1.45.2 -d ~/path/to/my/jars" "Download metabase_1.45.2.jar to ~/path/to/my/jars, deleting if it exists"]
|
||||
["./bin/mage jar-download master --run --port 3001" "Download and run the latest master branch jar on port 3001"]
|
||||
["JARS=~/my-stuff ./bin/mage jar-download 1.45.2" "Download metabase_1.45.2.jar to ~/my-stuff"]
|
||||
["./bin/mage jar-download v1.56.0-beta -r" "Download and run metabase_1.56.0-beta.jar"]]
|
||||
:requires [[mage.jar-download :as jar-download]]
|
||||
:options [["-r" "--run" "run the new jar after downloading it"]
|
||||
["-e" "--env-file FILE" "you can optionally provide a lein-env file to specify the environment to use when running the jar."]
|
||||
["-d" "--delete" "delete the old jar if found, by default does not re-download it"]
|
||||
["-p" "--port PORT" "you can optionally provide a port to run the jar on, if not, we use a sane port."]]
|
||||
:arg-schema [:or
|
||||
[:tuple [:string {:name "version"}]]
|
||||
[:tuple [:string {:name "version"}] [:string {:name "path"}]]]
|
||||
:usage-fn (fn [_]
|
||||
(-> "Version can have the following forms:"
|
||||
(str "\n -" (c/green "50") " for the latest enterprise version of release 50")
|
||||
(str "\n -" (c/green "1.45.2") " for a specific version")
|
||||
(str "\n -" (c/green "master") " for the latest jar created for master")
|
||||
(str "\n -" (c/green "any-branch-with-a-pr") " for the latest uberjar created for that branch")
|
||||
(str "\n -" (c/green "1.56.0-beta") " for a specific beta version")
|
||||
(str "\n\nnote: If you get an error about 'Invalid or corrupt jarfile', run this command again with --delete.")))
|
||||
:task (task! (jar-download/jar-download parsed))}
|
||||
:usage-fn (fn [_]
|
||||
(-> "Version can have the following forms:"
|
||||
(str "\n -" (c/green "50") " for the latest enterprise version of release 50")
|
||||
(str "\n -" (c/green "1.45.2") " for a specific version")
|
||||
(str "\n -" (c/green "master") " for the latest jar created for master")
|
||||
(str "\n -" (c/green "any-branch-with-a-pr") " for the latest uberjar created for that branch")
|
||||
(str "\n -" (c/green "1.56.0-beta") " for a specific beta version")
|
||||
(str "\n\nnote: If you get an error about 'Invalid or corrupt jarfile', run this command again with --delete.")))
|
||||
:task (task! (jar-download/jar-download parsed))}
|
||||
|
||||
setup-stats-repl
|
||||
{:doc "Connect to the stats repl"
|
||||
:examples [["./bin/mage connect-stats-repl" "Connects to the stats repl"]]
|
||||
:requires [[mage.stats-repl :as stats-repl]]
|
||||
:task (stats-repl/connect (cli/parse! (current-task)))}
|
||||
:task (stats-repl/connect (cli/parse! (current-task)))}
|
||||
|
||||
notify
|
||||
{:doc "Send notification for a comment (that already exists)"
|
||||
{:doc "Send notification for a comment (that already exists)"
|
||||
:requires [[mage.be-dev :refer [nrepl-eval]]]
|
||||
:options [["-c" "--comment-id ID" "Notify about this comment id"]
|
||||
["-p" "--port PORT" "Port to use for the task, defaults to value in .nrepl-port"]]
|
||||
:options [["-c" "--comment-id ID" "Notify about this comment id"]
|
||||
["-p" "--port PORT" "Port to use for the task, defaults to value in .nrepl-port"]]
|
||||
:examples [["./bin/mage notify -c 1234" "Notify about comment with id 1234"]
|
||||
["./bin/mage notify --comment-id 5678 --port 59498" "Notify about comment with id 5678 on nrepl port 59498"]]
|
||||
:task (let [opts (:options (cli/parse! (current-task)))
|
||||
:task (let [opts (:options (cli/parse! (current-task)))
|
||||
;; note: no quoting -> expect it to be int
|
||||
code (format "(notify-comment-id! %d)" (parse-long (:comment-id opts)))]
|
||||
(if (:port opts)
|
||||
(nrepl-eval "metabase.comments.core" code (:port opts))
|
||||
(nrepl-eval "metabase.comments.core" code)))}
|
||||
code (format "(notify-comment-id! %d)" (parse-long (:comment-id opts)))]
|
||||
(if (:port opts)
|
||||
(nrepl-eval "metabase.comments.core" code (:port opts))
|
||||
(nrepl-eval "metabase.comments.core" code)))}
|
||||
|
||||
run-tests
|
||||
{:doc "Quickly run tests locally. Pass dirs/files as arguments to run them."
|
||||
:examples [["./bin/mage run-tests" "launch interactive test picker"]
|
||||
["./bin/mage run-tests test/metabase/db" "run tests in test/metabase/db"]
|
||||
["./bin/mage run-tests test/metabase/logger test/metabase/api_keys" "run tests in test/metabase/logger and src/metabase/api_keys"]
|
||||
["./bin/mage run-tests test/metabase/config_test.clj" "run all tests in the metabase.config-test namespace"]]
|
||||
{:doc "Quickly run tests locally. Pass dirs/files as arguments to run them."
|
||||
:examples [["./bin/mage run-tests" "launch interactive test picker"]
|
||||
["./bin/mage run-tests test/metabase/db" "run tests in test/metabase/db"]
|
||||
["./bin/mage run-tests test/metabase/logger test/metabase/api_keys" "run tests in test/metabase/logger and src/metabase/api_keys"]
|
||||
["./bin/mage run-tests test/metabase/config_test.clj" "run all tests in the metabase.config-test namespace"]]
|
||||
;; TODO: add option to select files as well as dirs from fzf with styles
|
||||
:options [["-s" "--selecting STYLE" "Style of test selection" :default "all"]
|
||||
:options [["-s" "--selecting STYLE" "Style of test selection" :default "all"]
|
||||
;; all, file, dir
|
||||
["-p" "--port PORT" "Port to use for the task, defaults to value in .nrepl-port"]]
|
||||
["-p" "--port PORT" "Port to use for the task, defaults to value in .nrepl-port"]]
|
||||
:arg-schema [:sequential
|
||||
{:details "If none are given, you will be prompted to select directories recursively from test/ and enterprise/backend/test/"}
|
||||
[:string {:name "file-or-dir" :description "Files or directories (recursive) to test"}]]
|
||||
:requires [[mage.quick-test-runner :as qtr]]
|
||||
:task (task! (qtr/go parsed))}
|
||||
:requires [[mage.quick-test-runner :as qtr]]
|
||||
:task (task! (qtr/go parsed))}
|
||||
|
||||
affected-modules
|
||||
{:doc "Which backend modules have changes?"
|
||||
{:doc "Which backend modules have changes?"
|
||||
:examples [["./bin/mage affected-modules master" "prints changes compared to `master`"]
|
||||
["./bin/mage affected-modules"]]
|
||||
:arg-schema [:or [:tuple] [:tuple [:string {:name "git-ref"}]]]
|
||||
:requires [[mage.modules]]
|
||||
:task (task! (mage.modules/cli-print-affected-modules (:arguments parsed)))}
|
||||
:task (task! (mage.modules/cli-print-affected-modules (:arguments parsed)))}
|
||||
|
||||
can-skip-driver-tests
|
||||
{:doc "Can we skip driver tests for this branch?"
|
||||
{:doc "Can we skip driver tests for this branch?"
|
||||
:examples [["./bin/mage can-skip-driver-tests master" "Returns zero (success) if we can skip driver tests based on changes related to `master`"]
|
||||
["./bin/mage can-skip-driver-tests" "Returns zero (success) if we can skip driver tests based on changes? (defaults to master)"]]
|
||||
:requires [[mage.modules]]
|
||||
:task (task! (mage.modules/cli-can-skip-driver-tests (:arguments parsed)))}
|
||||
:task (task! (mage.modules/cli-can-skip-driver-tests (:arguments parsed)))}
|
||||
|
||||
ls
|
||||
{:doc "List all mage public and private tasks"
|
||||
{:doc "List all mage public and private tasks"
|
||||
:examples [["./bin/mage ls" "List all tasks"]]
|
||||
:requires [[mage.util :as u]]
|
||||
:task (task! (u/print-tasks parsed))}
|
||||
:task (task! (u/print-tasks parsed))}
|
||||
|
||||
analytics:setup
|
||||
{:doc "Set up usage analytics development mode"
|
||||
@@ -267,18 +282,71 @@
|
||||
:examples [["./bin/mage doctor" "Run all health checks"]]
|
||||
:requires [[mage.doctor :as doctor]]
|
||||
:task (task! (doctor/doctor!))}
|
||||
|
||||
ci-report
|
||||
{:doc "Generate a CI failure report for a PR"
|
||||
:examples [["./bin/mage ci-report" "Check current branch, find its PR, and generate report"]
|
||||
["./bin/mage ci-report 68292" "Generate report for PR #68292"]
|
||||
["./bin/mage ci-report https://github.com/metabase/metabase/pull/68292" "Generate report from PR URL"]]
|
||||
:usage-fn (fn [_]
|
||||
(str "Fetches CI check status and failed job logs from GitHub Actions.\n"
|
||||
"Parses Trunk test reports to extract test failure details.\n\n"
|
||||
"With no arguments, looks up the PR for your current git branch.\n\n"
|
||||
"Requires GitHub CLI (gh) to be installed and authenticated.\n"
|
||||
"If you see 'gh: command not found' or auth errors, run: gh auth login"))
|
||||
:requires [[mage.ci-report :as ci-report]]
|
||||
:task (task! (ci-report/generate-report! parsed))}
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Private Tasks:
|
||||
;; These tasks are more for internal tooling, e.g. for use from git hooks etc.
|
||||
;; - hidden from `./bin/mage` listing and `bb tasks`, these all - start with a `-`
|
||||
|
||||
-driver-decisions
|
||||
{:doc "Determine which driver tests should run based on PR context. Outputs decisions for all drivers."
|
||||
:examples [["./bin/mage -driver-decisions --git-ref=master --is-master-or-release=false --pr-labels= --skip=false" "Determine driver decisions for a PR"]
|
||||
["./bin/mage -driver-decisions --pr-labels=ci:all-cloud-drivers" "Run all cloud drivers via label"]
|
||||
["./bin/mage -driver-decisions --github-output-only" "Output only GITHUB_OUTPUT format (for CI)"]]
|
||||
:requires [[clojure.string :as str]
|
||||
[table.core :as table]
|
||||
[mage.modules]]
|
||||
:usage-fn
|
||||
(fn [_current-task]
|
||||
(let [driver-triggers (vec (sort mage.modules/default-modules-which-trigger-drivers))]
|
||||
(str
|
||||
"# How we decide to run a driver's tests. For each driver, we check these conditions in order (first match wins):\n"
|
||||
(with-out-str
|
||||
(table/table [["Priority" "Condition" "Result" "Reason"]
|
||||
["1" "Global skip (no backend changes)" "SKIP" "workflow skip (no backend changes)"]
|
||||
["2" "Driver is :h2 or :postgres" "RUN" "H2/Postgres always run"]
|
||||
["3" "Driver is quarantined" "SKIP" "driver is quarantined"]
|
||||
["3(a)" "...and has break-quarantine-X label" "RUN" "anti-quarantine label present"]
|
||||
["4" "On master or release branch" "RUN" "master/release branch"]
|
||||
["5" "Cloud driver + ci:all-cloud-drivers" "RUN" "ci:all-cloud-drivers label"]
|
||||
["6"
|
||||
"Cloud driver + its files changed"
|
||||
"RUN"
|
||||
(str "any of " (pr-str driver-triggers) " affected by cloud-driver module changes")]
|
||||
["7" "Cloud driver, no relevant changes" "SKIP" "no relevant changes for cloud driver"]
|
||||
["8"
|
||||
(str (pr-str driver-triggers) " module deps affected")
|
||||
"RUN"
|
||||
(str "any of " (pr-str driver-triggers) " affected by module changes")]
|
||||
["9" "Self-hosted driver, not affected" "SKIP" "driver module not affected"]])))))
|
||||
:options [[nil "--git-ref REF" "The git ref to compute changed modules against"]
|
||||
[nil "--is-master-or-release MASTER_OR_RELEASE"]
|
||||
[nil "--pr-labels PR_LABELS" "Comma delimited labels on the pr"]
|
||||
[nil "--skip SKIP"]
|
||||
[nil "--github-output-only" "Only output GITHUB_OUTPUT format (no human-readable output)"]]
|
||||
|
||||
:task (task! (mage.modules/-main parsed))}
|
||||
|
||||
-install-merge-drivers
|
||||
{:doc "Installs custom git merge drivers for YAML migration files."
|
||||
:requires [[mage.util :as u]
|
||||
[clojure.string :as str]]
|
||||
:task (task!
|
||||
(let [git-dir (u/sh "git rev-parse --git-dir")
|
||||
(let [git-dir (u/sh "git rev-parse --git-common-dir")
|
||||
git-config-file (str git-dir "/config")
|
||||
the-include (str "\n"
|
||||
"[include]\n"
|
||||
@@ -293,7 +361,7 @@
|
||||
:requires [[clojure.string :as str]
|
||||
[mage.util :as u]]
|
||||
:task (task!
|
||||
(let [git-dir (u/sh "git rev-parse --git-dir")
|
||||
(let [git-dir (u/sh "git rev-parse --git-common-dir")
|
||||
git-config-file (str git-dir "/config")
|
||||
the-include (str "\n"
|
||||
"[include]\n"
|
||||
@@ -313,38 +381,38 @@
|
||||
[:string {:name "base" :description "ancestor's version (base)"}]
|
||||
[:string {:name "ours" :description "current version (ours)"}]
|
||||
[:string {:name "theirs" :description "other branch's version (theirs)"}]
|
||||
[:int {:name "conflict-marker-size" :description "conflict marker size" :min 0}]
|
||||
[:int {:name "conflict-marker-size" :description "conflict marker size" :min 0}]
|
||||
[:string {:name "file-path" :description "file path"}]]
|
||||
:requires [[mage.merge-yaml-migrations]]
|
||||
:task (task! (mage.merge-yaml-migrations/-main parsed))}
|
||||
|
||||
|
||||
-play
|
||||
{:doc "Play sounds from mage.sound"
|
||||
:examples [["./bin/mage -play success" "play success sound"] ["./bin/mage -play s" "play success sound"]
|
||||
["./bin/mage -play error" "play error sound"] ["./bin/mage -play e" "play error sound"]
|
||||
["./bin/mage -play ping" "play ping sound"] ["./bin/mage -play p" "play ping sound"]
|
||||
["./bin/mage -play warning" "play warning sound"] ["./bin/mage -play w" "play warning sound"]]
|
||||
:requires [[mage.sound :as sound]]
|
||||
{:doc "Play sounds from mage.sound"
|
||||
:examples [["./bin/mage -play success" "play success sound"] ["./bin/mage -play s" "play success sound"]
|
||||
["./bin/mage -play error" "play error sound"] ["./bin/mage -play e" "play error sound"]
|
||||
["./bin/mage -play ping" "play ping sound"] ["./bin/mage -play p" "play ping sound"]
|
||||
["./bin/mage -play warning" "play warning sound"] ["./bin/mage -play w" "play warning sound"]]
|
||||
:requires [[mage.sound :as sound]]
|
||||
:arg-schema [:tuple [:enum "s" "success" "e" "error" "p" "ping" "w" "warning"]]
|
||||
:task (task! (case (first (:arguments parsed))
|
||||
("s" "success") (sound/success)
|
||||
("e" "error") (sound/error)
|
||||
("p" "ping") (sound/ping)
|
||||
("w" "warning") (sound/warning)))}
|
||||
|
||||
:task (task! (case (first (:arguments parsed))
|
||||
("s" "success") (sound/success)
|
||||
("e" "error") (sound/error)
|
||||
("p" "ping") (sound/ping)
|
||||
("w" "warning") (sound/warning)))}
|
||||
|
||||
-token-scan
|
||||
{:doc "Security token scanner for detecting leaked API keys and secrets"
|
||||
:examples [["./bin/mage -token-scan deps.edn bb.edn" "Scan specific files"]
|
||||
["./bin/mage -token-scan -v deps.edn" "Scan with verbose output"]
|
||||
["./bin/mage -token-scan -a" "Scan all files in the project"]]
|
||||
:requires [[mage.token-scan :as token-scan]]
|
||||
:options [["-a" "--all-files" "Scan all files in the project"]
|
||||
["-v" "--verbose" "Verbose output"]]
|
||||
{:doc "Security token scanner for detecting leaked API keys and secrets"
|
||||
:examples [["./bin/mage -token-scan deps.edn bb.edn" "Scan specific files"]
|
||||
["./bin/mage -token-scan -v deps.edn" "Scan with verbose output"]
|
||||
["./bin/mage -token-scan -a" "Scan all files in the project"]]
|
||||
:requires [[mage.token-scan :as token-scan]]
|
||||
:options [["-a" "--all-files" "Scan all files in the project"]
|
||||
["-v" "--verbose" "Verbose output"]]
|
||||
:arg-schema [:sequential [:string {:description "Files to scan"}]]
|
||||
:task (task! (token-scan/run-scan parsed))}
|
||||
:task (task! (token-scan/run-scan parsed))}
|
||||
|
||||
-repl
|
||||
{:doc "Eval a string of clojure code in the backend dev server"
|
||||
{:doc "Eval a string of clojure code in the backend dev server"
|
||||
:requires [[babashka.process :as process]
|
||||
[mage.color :as c]
|
||||
[mage.be-dev :refer [nrepl-eval]]]
|
||||
@@ -356,16 +424,16 @@
|
||||
"Evaluate a keyword inside a namespace"]]
|
||||
:options [["-n" "--namespace NAMESPACE" "Namespace to use for the task"]
|
||||
["-p" "--port PORT" "Port to use for the task, defaults to value in .nrepl-port"]]
|
||||
:task (let [parsed (cli/parse! (current-task))
|
||||
code (first (:arguments parsed))
|
||||
port (or (:port (:options parsed))
|
||||
(slurp ".nrepl-port")
|
||||
(throw (ex-info (c/red "No port specified, and no .nrepl-port file found.") {:babashka/exit 1})))
|
||||
nns (or (:namespace (:options parsed)) "user")]
|
||||
(nrepl-eval nns code port))}
|
||||
:task (let [parsed (cli/parse! (current-task))
|
||||
code (first (:arguments parsed))
|
||||
port (or (:port (:options parsed))
|
||||
(slurp ".nrepl-port")
|
||||
(throw (ex-info (c/red "No port specified, and no .nrepl-port file found.") {:babashka/exit 1})))
|
||||
nns (or (:namespace (:options parsed)) "user")]
|
||||
(nrepl-eval nns code port))}
|
||||
|
||||
-check-readable
|
||||
{:doc "Check that code is readable parens a file"
|
||||
{:doc "Check that code is readable parens a file"
|
||||
:requires [[mage.readability-check :as readability]]
|
||||
:arg-schema [:or
|
||||
[:tuple [:string {:name "file"
|
||||
@@ -379,60 +447,59 @@
|
||||
"Check file for readability"]
|
||||
["./bin/mage -check-readable dev/src/dev.clj 300"
|
||||
"Check top level form containing line 300 for readability"]]
|
||||
:task (let [{:keys [arguments]} (cli/parse! (current-task))]
|
||||
(apply readability/check arguments))}
|
||||
:task (let [{:keys [arguments]} (cli/parse! (current-task))]
|
||||
(apply readability/check arguments))}
|
||||
|
||||
;; Uncomment this if you want to play with the example calculator task:
|
||||
#_#_
|
||||
-example-calculator
|
||||
{;; `:doc` is a short docstring for the task, will be listed in `./bin/mage -h` and other help info.
|
||||
#_#_-example-calculator
|
||||
{;; `:doc` is a short docstring for the task, will be listed in `./bin/mage -h` and other help info.
|
||||
;; Keep these short, they need to fit on one line in the output from `./bin/mage -h`.
|
||||
:doc "The example calculator's task docstring"
|
||||
:doc "The example calculator's task docstring"
|
||||
;; examples get printed out when you pass -h or --help.
|
||||
:examples [["./bin/mage -example-calculator 1 + 99" "evaluates to 100"]
|
||||
:examples [["./bin/mage -example-calculator 1 + 99" "evaluates to 100"]
|
||||
["./bin/mage -example-calculator 100 - 99" "evaluates to 1"]]
|
||||
;; The task is the actual code that runs when you run the task.
|
||||
:task (task!
|
||||
(println "Parsed input:") (u/pp parsed)
|
||||
(let [{:keys [arguments data]} parsed
|
||||
[a op b] arguments]
|
||||
(println a (name op) b "=" (c/blue (({:+ + :- -} op) a b)))))
|
||||
:task (task!
|
||||
(println "Parsed input:") (u/pp parsed)
|
||||
(let [{:keys [arguments data]} parsed
|
||||
[a op b] arguments]
|
||||
(println a (name op) b "=" (c/blue (({:+ + :- -} op) a b)))))
|
||||
;; (optional) `:require` lazily libraries for just your task:
|
||||
:requires [[mage.color :as c]
|
||||
:requires [[mage.color :as c]
|
||||
[mage.util :as u]]
|
||||
;; (optional) `:options` are passed to [[clojure.tools.cli/parse-opts]].
|
||||
;; See: https://clojure.github.io/tools.cli/index.html#clojure.tools.cli/parse-opts
|
||||
:options [["-n" "--negate" "Negate the result of the calculation"]
|
||||
["-a" "--also-add N" "Add N to the result of the calculation"]]
|
||||
:options [["-n" "--negate" "Negate the result of the calculation"]
|
||||
["-a" "--also-add N" "Add N to the result of the calculation"]]
|
||||
;; (optional) `:arg-schema` is a malli schema for the arguments passed to the task, after the options
|
||||
:arg-schema [:tuple [:int {:name "number-a"}] [:enum {:name "operation"} :+ :-] [:int {:name "number-b"}]]
|
||||
:arg-schema [:tuple [:int {:name "number-a"}] [:enum {:name "operation"} :+ :-] [:int {:name "number-b"}]]
|
||||
;; (optional) `:usage-fn` is a function called with the current-task map.
|
||||
;; Returns a string containing extra-detailed usage information.
|
||||
:usage-fn (fn [{:keys [doc] :as _task}]
|
||||
:usage-fn (fn [{:keys [doc] :as _task}]
|
||||
(str "Optional extra usage information, for explicit callouts.\n"
|
||||
"Reversed docstring: '" (c/green (apply str (reverse doc))) "'"))
|
||||
;; (optional) Any other keys are completely allowed. Put things to lookup or that you want to be easy to change
|
||||
;; here. These will be returned from `(current-task)`. See: start-db for an example of using `:ports` to define a
|
||||
;; top-level map of ports. If something should be easily accessible in the task, put it here.
|
||||
:data {:a [:b :c] :b [:d]}}
|
||||
:data {:a [:b :c] :b [:d]}}
|
||||
|
||||
-test {:doc "run all mage tests"
|
||||
-test {:doc "run all mage tests"
|
||||
:requires [[clojure.string :as str]
|
||||
[clojure.test]
|
||||
[mage.core-test] ;; loads the actual tests
|
||||
[mage.util :as u]]
|
||||
:arg-schema [:sequential [:string {:name "test-matching-pattern"}]]
|
||||
:task (let [{:keys [arguments]} (task! parsed)
|
||||
pattern (if (seq arguments)
|
||||
(str "^mage\\..*(" (str/join "|" arguments) ").*")
|
||||
"^mage\\..*")
|
||||
_ (u/debug ["test regex matching pattern" pattern])
|
||||
results (clojure.test/run-all-tests (re-pattern pattern))]
|
||||
(if (zero? (+ (:fail results) (:error results)))
|
||||
(println (c/green "All mage tests passed!"))
|
||||
(do
|
||||
(println (c/red "Some mage tests failed."))
|
||||
(u/exit 1))))}
|
||||
:task (let [{:keys [arguments]} (task! parsed)
|
||||
pattern (if (seq arguments)
|
||||
(str "^mage\\..*(" (str/join "|" arguments) ").*")
|
||||
"^mage\\..*")
|
||||
_ (u/debug ["test regex matching pattern" pattern])
|
||||
results (clojure.test/run-all-tests (re-pattern pattern))]
|
||||
(if (zero? (+ (:fail results) (:error results)))
|
||||
(println (c/green "All mage tests passed!"))
|
||||
(do
|
||||
(println (c/red "Some mage tests failed."))
|
||||
(u/exit 1))))}
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Personal Tasks:
|
||||
@@ -441,14 +508,14 @@
|
||||
;; `./bin/mage` listing.
|
||||
|
||||
-escherize-pr-summary
|
||||
{:doc "Print a day's PR summary for the metabase/metabase repo"
|
||||
{:doc "Print a day's PR summary for the metabase/metabase repo"
|
||||
:requires [[mage.escherize.pr-summary :as eps]]
|
||||
:examples [["./bin/mage -escherize-pr-summary | pbcopy" "Copy your daily"]]
|
||||
:task (task! (eps/summarize-prs parsed))}
|
||||
:task (task! (eps/summarize-prs parsed))}
|
||||
|
||||
-escherize-fix-whitespace
|
||||
{:doc "Removes all lines like '^<space>+$' from files in glob pattern."
|
||||
:requires [[mage.escherize.fix-whitespace :as fix-whitespace]]
|
||||
:examples [["mage -escherize-fix-whitespace '**/*.clj{,c,s}'" "Removes all lines like ' ' from all clj files"]]
|
||||
:arg-schema [:sequential [:string {:name "glob-pattern" :description "Glob pattern to match files"}]]
|
||||
:task (task! (fix-whitespace/run-clean parsed))}}}
|
||||
:task (task! (fix-whitespace/run-clean parsed))}}}
|
||||
|
||||