Compare commits

..

1 Commits

Author SHA1 Message Date
Jeff Bruemmer
1f473e1322 add junk 2025-11-20 15:13:52 -05:00
10304 changed files with 594735 additions and 1129296 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -54,6 +54,8 @@ Run through the diff looking for these issues:
**Formatting:**
- [ ] Backticks on UI elements (should use **bold**)
- [ ] Backticks used as quotation marks
- [ ] Ampersands as "and" substitute (except proper nouns)
- [ ] Inconsistent list formatting
@@ -72,6 +74,7 @@ Run through the diff looking for these issues:
| Pattern | Issue |
| ----------------------------- | --------------------------------------------- |
| `Button name` or `UI element` | Should use **bold** not backticks |
| we can do X, our feature | Should be "Metabase" or "it" |
| click here, read more here | Need descriptive link text |
| easy, simple, just | Remove condescending qualifiers |
@@ -105,10 +108,13 @@ Suggested fix or explanation
**Examples:**
> **Issue 1: Formal tone**
> **Issue 1: Backticks on UI elements**
> Line 42: This uses backticks for the UI element. Use **bold** instead: **Filter** not `Filter`.
> **Issue 2: Formal tone**
> Line 15: This could be more conversational. Consider: "You can't..." instead of "You cannot..."
> **Issue 2: Vague heading**
> **Issue 3: Vague heading**
> Line 8: The heading could be more specific. Try stating the point directly: "Run migrations before upgrading" vs "Upgrade process"
### PR review mode format
@@ -135,15 +141,13 @@ When posting to GitHub (PR mode), use the **pending review workflow**:
5. **Submit the review**: Use `mcp__github__submit_pending_pull_request_review` to publish all comments at once
- Use event type `"COMMENT"` (NOT "REQUEST_CHANGES") to make it non-blocking
- **Do NOT include a body message** - Leave the body empty or omit it entirely
- All comments will appear together as one cohesive review
**Comment format example:**
```
**Issue 1: Formal tone**
**Issue 1: Backticks on UI elements**
This could be more conversational. Consider: "You can't..." instead of "You cannot..."
This uses backticks for the UI element. Use **bold** instead: **Filter** not `Filter`.
```
**IMPORTANT**:
@@ -152,7 +156,6 @@ This could be more conversational. Consider: "You can't..." instead of "You cann
- Always start the comment body with `**Issue N: [Brief title]**`
- **MUST add all comments in parallel in a single response** - Do NOT add them one after another in separate responses
- Do NOT output a summary message to the conversation - only post GitHub review comments
- When submitting the review, do NOT include a body parameter (or leave it empty) to avoid cluttering the PR with summary text
- The review will appear as a single review with multiple comments when submitted
## Final check

View File

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

View File

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

View File

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

View File

@@ -1,114 +0,0 @@
---
name: mutation-testing
description: Run mutation testing on a Clojure namespace, generate tests to kill surviving mutations, and open draft PRs with Linear issue tracking.
---
# Mutation Testing Skill
This skill runs mutation testing end-to-end: generates a coverage report, groups functions, shells out to `claude -p` to write tests, verifies mutations are killed, and creates draft PRs with Linear issues.
## Prerequisites
- A running nREPL connected to the Metabase dev environment
- `LINEAR_API_KEY` environment variable set with a valid Linear personal API key
- `gh` CLI authenticated with GitHub
- `claude` CLI available on PATH
## Reference Files
- `dev/src/dev/coverage.clj` — mutation testing engine (generates reports, runs mutations)
- `dev/src/dev/mutation_testing.clj` — orchestration, Linear API, PR helpers
## Invocation
The argument is a Clojure namespace, optionally followed by a base branch:
```
/mutation-testing metabase.lib.order-by
/mutation-testing metabase.lib.order-by --base-branch release-x.52.x
/mutation-testing metabase.lib.order-by --project-id abc-123
/mutation-testing metabase.lib.order-by --base-branch release-x.52.x --project-id abc-123
```
- `--base-branch`: defaults to the value in config (set via `set-config!`), or `"master"` if not configured.
- `--project-id`: if provided, issues are added to this existing Linear project instead of creating a new one.
## Steps
### 1. Parse arguments
Parse `$ARGUMENTS` to extract:
- **namespace** (required) — the first positional argument
- **`--base-branch`** (optional) — if present, the next argument is the base branch name
- **`--project-id`** (optional) — if present, the next argument is an existing Linear project ID
### 2. Load and configure
```clojure
(require '[dev.mutation-testing :as mut-test] :reload)
(require '[dev.coverage :as cov] :reload)
```
If this is the first invocation (or the REPL was restarted), set up the Linear team:
```clojure
(mut-test/list-teams!)
(mut-test/set-config! {:team-id "<id>"})
```
### 3. Run
```clojure
(mut-test/run! '<target-ns>)
;; Or with options:
(mut-test/run! '<target-ns> {:base-branch "release-x.52.x"})
(mut-test/run! '<target-ns> {:project-id "abc-123"})
(mut-test/run! '<target-ns> {:base-branch "release-x.52.x" :project-id "abc-123"})
```
Pass the opts map with `:base-branch` and/or `:project-id` if those flags were provided in the arguments.
This will:
1. Generate a baseline mutation testing report
2. Create a Linear project for the namespace
3. Group functions by coverage relationships
4. For each group: create branch → invoke Claude to write tests → verify → retry if needed → commit & push → create Linear issue → create draft PR
5. Print a summary with PR links
### 4. Handle failures
If a group fails mid-processing, `run!` catches the error and continues to the next group. To retry a single group manually:
```clojure
;; Get the parsed namespace info
(def parsed (mut-test/parse-namespace '<target-ns>))
;; Run coverage to get the data
(def coverage-results (cov/test-namespace (:target-ns parsed) [(:test-ns parsed)]))
;; Group and find the one you want
(def groups (mut-test/group-functions coverage-results))
;; Process just that group
(mut-test/process-group! parsed (nth groups <index>))
```
### 5. Verify the prompt (dry run)
To inspect what Claude will see without invoking it:
```clojure
(def parsed (mut-test/parse-namespace '<target-ns>))
(def coverage-results (cov/test-namespace (:target-ns parsed) [(:test-ns parsed)]))
(def groups (mut-test/group-functions coverage-results))
(println (mut-test/build-test-prompt
(merge (select-keys parsed [:target-ns :test-ns :source-path :test-path])
(select-keys (first groups) [:fn-names :mutations]))))
```
## Configuration
- **Team ID**: Set once per REPL session via `(mut-test/list-teams!)` and `(mut-test/set-config! {:team-id "..."})`
- **Project ID**: Set automatically when `run!` calls `create-project-for-namespace!`
- **Base Branch**: Defaults to `"master"`. Override via `(mut-test/set-config! {:base-branch "release-x.52.x"})` or pass `{:base-branch "..."}` as the second arg to `run!`
- **Project ID**: If set (via `set-config!` or `run!` opts), `run!` reuses the existing Linear project instead of creating a new one

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -55,6 +55,7 @@
help-link-custom-destination
instance-creation
is-hosted?
is-metabot-enabled
jdbc-data-warehouse-max-connection-pool-size
jdbc-data-warehouse-unreturned-connection-timeout-seconds
jwt-attribute-email
@@ -96,6 +97,7 @@
metabot-get-prompt-templates-url
metabot-prompt-generator-token-limit
multi-setting-read-only
notebook-native-preview-shown
notebook-native-preview-sidebar-width
notification-link-base-url
num-metabot-choices
@@ -117,10 +119,10 @@
reset-token-ttl-hours
retired-setting
retry-initial-interval
retry-max-retries
retry-max-attempts
retry-max-interval-millis
retry-multiplier
retry-jitter-factor
retry-randomization-factor
saml-application-name
saml-attribute-email
saml-attribute-firstname

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

5
.eslintignore Normal file
View File

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

354
.eslintrc.js Normal file
View File

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

9
.gitattributes vendored
View File

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

View File

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

268
.github/CODEOWNERS vendored
View File

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

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

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
name: Prepare front-end environment
description: "Prepare the front-end environment for the CI build"
inputs:
node-version:
description: "The version of Node.js to use, overriding .nvmrc"
@@ -11,10 +10,11 @@ inputs:
runs:
using: "composite"
steps:
- name: Prepare Node + Bun environment
uses: ./.github/actions/prepare-node-bun
- name: Prepare Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
node-version: ${{ inputs.node-version }} # this overrides, if present
node-version-file: ".nvmrc"
- name: Get cache keys
uses: ./.github/actions/get-cache-keys
@@ -27,15 +27,13 @@ runs:
~/.m2
~/.gitlibs
key: ${{ steps.get-cache-keys.outputs.m2-cache-key }}
restore-keys: |
${{ steps.get-cache-keys.outputs.m2-restore-key }}
- name: Get bun cache directory
id: bun-cache
- name: Get yarn cache directory
id: yarn-cache-dir
run: |
BUN_CACHE_DIR=$(bun pm cache)
echo "Bun cache directory: $BUN_CACHE_DIR"
echo "dir=$BUN_CACHE_DIR" >> $GITHUB_OUTPUT
YARN_CACHE_DIR=$(yarn cache dir)
echo "Yarn cache directory: $YARN_CACHE_DIR"
echo "dir=$YARN_CACHE_DIR" >> $GITHUB_OUTPUT
shell: bash
- name: Restore node_modules cache
@@ -43,19 +41,17 @@ runs:
id: node-modules-cache
with:
path: |
${{ steps.bun-cache.outputs.dir }}
${{ steps.yarn-cache-dir.outputs.dir }}
node_modules
key: ${{ steps.get-cache-keys.outputs.node-cache-key }}
restore-keys: |
${{ steps.get-cache-keys.outputs.node-restore-key }}
# postinstall lifecycle hook can be dangerous in CI so we only run it locally (see package.json)
# If a job wants to patch packages, it does so explicitly in the CI. This setting is opt-out.
# For example, we never want to cache node_modules with patches already applied so cache-generator.yml skips that step.
- name: Clean install
run: bun install --frozen-lockfile
run: yarn install --frozen-lockfile --prefer-offline
shell: bash
- name: Patch frontend dependencies
if: inputs.apply-patches == 'true'
run: bun run patch-package
run: yarn patch-package
shell: bash

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,18 +9,6 @@ inputs:
build-static-viz:
required: true
default: false
logs-artifact-suffix:
required: false
description: Suffix to append to the test logs artifact name to avoid conflicts in matrix jobs
edition:
required: false
default: 'ee'
description: Edition to test (oss or ee)
outputs:
test-outcome:
description: "The outcome of the test run (success, failure, cancelled, skipped)"
value: ${{ steps.run-test.outcome }}
runs:
using: "composite"
@@ -28,16 +16,16 @@ runs:
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Prepare front-end environment
if: ${{ inputs.build-static-viz == 'true' }}
uses: ./.github/actions/prepare-frontend
- name: Build static viz frontend
if: ${{ inputs.build-static-viz == 'true' }}
run: bun run build-static-viz
- name: Build static viz frontend
run: yarn build-static-viz
shell: bash
if: ${{ inputs.build-static-viz == 'true' }}
env:
MB_EDITION: ee
- name: Test database driver
run: clojure -X:dev:ci:${{ inputs.edition }}:${{ inputs.edition }}-dev:drivers:drivers-dev:test ${{ inputs.test-args }}
run: clojure -X:dev:ci:ee:ee-dev:drivers:drivers-dev:test ${{ inputs.test-args }}
shell: bash
id: run-test
# Trunk Quarantine controls the final state of the run in the CI!
@@ -49,7 +37,7 @@ runs:
# As per the documentation: https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#steps-context
# test.outcome gives us access to the result before `continue-on-error` overrides it and sets it to 'success'.
# We want test reports even if the test step gets cancelled (e.g. times out), hence we match anything that is not a 'success'.
if: ${{ steps.run-test.outcome != 'success' }}
if: steps.run-test.outcome != 'success'
with:
path: "target/junit/**/*_test.xml"
name: JUnit Test Report ${{ inputs.junit-name }}
@@ -61,7 +49,7 @@ runs:
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 && format('-{0}', inputs.logs-artifact-suffix) || '') }}
name: test-logs-${{ github.job }}
path: logs/test-log.json
retention-days: 1
# Failing to upload the artifact should never affect the outcome of the whole test run.

View File

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

View File

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

View File

@@ -11,26 +11,22 @@ shared_specs: &shared_specs
- "test/**/*.cljc"
- "test/**/*.cljs"
shared_ci: &shared_ci
frontend_ci: &frontend_ci
- ".github/actions/prepare-frontend/**"
- ".github/actions/prepare-backend/**"
- ".github/workflows/run-tests.yml"
shared_e2e: &shared_e2e
- "e2e/runner/**"
- "e2e/snapshot-creators/**"
- "e2e/support/**"
frontend_ci: &frontend_ci
- *shared_ci
- ".github/workflows/frontend.yml"
frontend_embedding_sdk_ci: &frontend_embedding_sdk_ci
- ".github/actions/prepare-frontend/**"
- ".github/actions/prepare-backend/**"
- ".github/workflows/*embedding-sdk*.yml"
frontend_configs: &frontend_configs
- "bun.lock"
- "yarn.lock"
- "**/tsconfig*.json"
- "package.json"
- "babel.config.json"
- "eslint.config.mjs"
- ".eslintrc.js"
- "postcss.config.js"
- "rspack.config.js"
- "rspack.main.config.js"
@@ -45,12 +41,14 @@ frontend_sources: &frontend_sources
- "enterprise/frontend/!(test)**/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
- "frontend/!(test)**/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
# Used only in .github/workflows/conventional-commit-pr-title-reminder.yml
frontend_embedding_sources:
- "enterprise/frontend/src/{embedding,embedding-*}/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
- "enterprise/frontend/src/metabase-enterprise/{embedding,embedding-*}**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
# Used only in .github/workflows/embedding-sdk-labels-reminder.yml
frontend_embedding_sdk_sources:
- *frontend_configs
- "enterprise/frontend/!(test)**/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
frontend_embedding_sdk_package_sources:
- "enterprise/frontend/src/embedding-sdk-package/**/!(*.spec).{js,jsx,ts,tsx,css}"
@@ -78,16 +76,15 @@ frontend_loki_ci: &frontend_loki_ci
- ".storybook/**"
backend_ci: &backend_ci
- *shared_ci
- ".github/actions/prepare-frontend/**"
- ".github/actions/prepare-backend/**"
- ".github/actions/test-driver/**"
- ".github/workflows/backend.yml"
- ".github/workflows/drivers.yml"
backend_presto_kerberos:
- "**/presto_jdbc/**"
- "**/presto-jdbc/**" # the driver module has a hyphen. underscores are classpaths
- "**/presto_jdbc.clj"
- "**/presto_jdbc_test.clj"
- ".github/workflows/presto-kerberos-integration-test.yml"
backend_sources: &backend_sources
@@ -120,9 +117,9 @@ sources: &sources
- *backend_sources
e2e_ci: &e2e_ci
- *shared_ci
- ".github/actions/prepare-frontend/**"
- ".github/actions/prepare-backend/**"
- ".github/actions/prepare-uberjar-artifact/**"
- ".github/actions/fetch-artifact/action.yml"
- ".github/actions/e2e-prepare-containers/**"
- ".github/actions/prepare-cypress/**"
- ".github/workflows/e2e-*"
@@ -130,27 +127,18 @@ e2e_ci: &e2e_ci
e2e_specs: &e2e_specs
- e2e/!(test-host-app)**/*.cy.*.{js,ts}
embedding_sdk_ci: &embedding_sdk_ci
- *shared_ci
- ".github/actions/prepare-uberjar-artifact/**"
- ".github/actions/fetch-artifact/action.yml"
- ".github/actions/prepare-cypress/**"
- ".github/workflows/embedding-sdk*.yml"
embedding_sdk_components:
- *default
- *embedding_sdk_ci
e2e_embedding_sdk:
- *frontend_embedding_sdk_ci
- *sources
- *shared_e2e
- "e2e/test-component/**/*.cy.*.{js,jsx,ts,tsx}"
- "e2e/component/**/*.cy.*.{js,ts}"
embedding_sdk_host_sample_apps:
e2e_embedding_sdk_compatibility:
- *default
- *embedding_sdk_ci
- *frontend_embedding_sdk_ci
- *frontend_sources
- *backend_sources
- *shared_e2e
- "e2e/embedding-sdk-host-apps/**"
- "e2e/runner/embedding-sdk/**"
- "e2e/test-host-app/**/*.cy.*.{js,ts}"
snowplow: &snowplow
@@ -199,15 +187,3 @@ cljs:
- *shared_specs
- ".github/workflows/cljs.yml"
- "shadow-cljs.edn"
openapi:
- ".github/workflows/openapi.yml"
- "src/metabase/api/**"
- "src/metabase/api_routes/**"
- "src/metabase/*_rest/api.clj"
- "src/metabase/*/api.clj"
- "src/metabase/*/api/*.clj"
- "enterprise/backend/src/metabase_enterprise/**/api.clj"
- "enterprise/backend/src/metabase_enterprise/**/api/*.clj"
- "resources/openapi/**"
- "package.json"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

46
.github/team.json vendored
View File

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

View File

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

View File

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

View File

@@ -36,22 +36,10 @@ jobs:
- run: clojure -X:dev:ee:ee-dev:drivers:drivers-dev:test:eastwood
name: Run Eastwood linter
be-linter-eastwood-test:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 30
name: Eastwood (test)
steps:
- uses: actions/checkout@v4
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- run: clojure -X:dev:ee:ee-dev:drivers:drivers-dev:test:eastwood/test
name: Run Eastwood linter on tests
be-tests:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 40
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
@@ -60,11 +48,13 @@ jobs:
- name: Enterprise Tests
edition: ee
test-group-name: ee
build-static-viz: 'false'
test-args: >-
:only '"enterprise/backend/test"'
- name: EE App DB Tests (Part 1)
edition: ee
test-group-name: ee-app-db-1
build-static-viz: 'true'
test-args: >-
:only '["test" ".clj-kondo/test"]'
:partition/total 2
@@ -72,6 +62,7 @@ jobs:
- name: EE App DB Tests (Part 2)
edition: ee
test-group-name: ee-app-db-2
build-static-viz: 'true'
test-args: >-
:only '["test" ".clj-kondo/test"]'
:partition/total 2
@@ -79,6 +70,7 @@ jobs:
- name: OSS App DB Tests (Part 1)
edition: oss
test-group-name: oss-app-db-1
build-static-viz: 'true'
test-args: >-
:only '["test" ".clj-kondo/test"]'
:partition/total 2
@@ -86,6 +78,7 @@ jobs:
- name: OSS App DB Tests (Part 2)
edition: oss
test-group-name: oss-app-db-2
build-static-viz: 'true'
test-args: >-
:only '["test" ".clj-kondo/test"]'
:partition/total 2
@@ -100,8 +93,8 @@ jobs:
(
github.event_name == 'push' &&
(
github.ref_name == 'master' ||
startsWith(github.ref_name, 'release-')
github.ref == 'master' ||
startsWith(github.ref, 'release-')
) &&
''
) ||
@@ -110,15 +103,25 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Prepare front-end environment
if: ${{ matrix.job.build-static-viz == 'true' }}
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
with:
java-version: "${{ matrix.java-version }}"
# Depending on which files changed, we might either have to build static-viz
# from scratch or download the previously built artifact
- name: Build static-viz frontend
run: bun run build-static-viz
if: ${{ matrix.job.build-static-viz == 'true' && !inputs.skip }}
run: yarn build-static-viz
env:
MB_EDITION: ${{ matrix.job.edition }}
- name: Download Static Viz Bundle Artifact
if: ${{ matrix.job.build-static-viz == 'true' && !inputs.skip }}
uses: actions/download-artifact@v4
with:
name: static-viz-bundle-${{ github.sha }}
path: resources/frontend_client/app/dist
- name: "Test Java ${{ matrix.java-version }} ${{ matrix.job.name }}"
id: run-java-tests
@@ -142,7 +145,6 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.run-java-tests.outcome }}
- name: Publish Test Report (JUnit)
uses: dorny/test-reporter@v1
@@ -196,7 +198,6 @@ jobs:
be-tests-result:
needs:
- be-linter-eastwood
- be-linter-eastwood-test
- be-linter-clj-kondo
- be-cljfmt
- be-check

View File

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

View File

@@ -51,19 +51,14 @@ jobs:
eslint-cache-key: ${{ steps.get-cache-keys.outputs.eslint-cache-key }}
cypress-cache-key: ${{ steps.get-cache-keys.outputs.cypress-cache-key }}
generate-caches:
generate-m2-cache:
runs-on: ubuntu-22.04
timeout-minutes: 30
timeout-minutes: 15
needs: get-cache-keys
env:
M2_CACHE_KEY: ${{ needs.get-cache-keys.outputs.m2-cache-key }}
NODE_CACHE_KEY: ${{ needs.get-cache-keys.outputs.node-cache-key }}
CYPRESS_CACHE_KEY: ${{ needs.get-cache-keys.outputs.cypress-cache-key }}
ESLINT_CACHE_KEY: ${{ needs.get-cache-keys.outputs.eslint-cache-key }}
steps:
- uses: actions/checkout@v4
# Backend preparation and cache
- name: Prepare backend
uses: ./.github/actions/prepare-backend
@@ -72,17 +67,31 @@ jobs:
- name: Pre-resolve Clojure dependencies
run: clojure -A:build:dev:ee:ee-dev:drivers:drivers-dev:cljs -P
- name: Let Mage install babashka and clojure-tools
run: ./bin/mage
- name: Update M2 cache
uses: ./.github/actions/update-cache
with:
path: |
~/.m2
~/.gitlibs
~/.deps.clj
${{ github.workspace}}/bin/bb
key: ${{ env.M2_CACHE_KEY }}
# Frontend preparation and caches
generate-frontend-caches:
runs-on: ubuntu-22.04
timeout-minutes: 15
needs: get-cache-keys
env:
NODE_CACHE_KEY: ${{ needs.get-cache-keys.outputs.node-cache-key }}
ESLINT_CACHE_KEY: ${{ needs.get-cache-keys.outputs.eslint-cache-key }}
CYPRESS_CACHE_KEY: ${{ needs.get-cache-keys.outputs.cypress-cache-key }}
steps:
- uses: actions/checkout@v4
# `prepare-frontend` action (1) prepares Node.js and (2) fetches the node_modules cache if it exists.
# It then (3) runs `bun install --frozen-lockfile` which ensures the local cache is fully populated.
# It then (3) runs `yarn install --frozen-lockfile --prefer-offline` which ensures the local cache is fully populated.
# IMPORTANT: Caching node_modules after patches are already applied will break any PR that attempts to either:
# - upgrade a patched library
# - change a patch
@@ -92,49 +101,37 @@ jobs:
with:
apply-patches: "false"
- name: Get bun cache directory
id: bun-cache
- name: Get yarn cache directory
id: yarn-cache-dir
run: |
BUN_CACHE_DIR=$(bun pm cache)
echo "Bun cache directory: $BUN_CACHE_DIR"
echo "dir=$BUN_CACHE_DIR" >> $GITHUB_OUTPUT
YARN_CACHE_DIR=$(yarn cache dir)
echo "Yarn cache directory: $YARN_CACHE_DIR"
echo "dir=$YARN_CACHE_DIR" >> $GITHUB_OUTPUT
- name: Ensure that Cypress executable is ready
run: |
bun run cypress install
bun run cypress cache path
bun run cypress cache list
bun run cypress verify
yarn cypress install
yarn cypress cache path
yarn cypress cache list
yarn cypress verify
shell: bash
# Run Prettier to populate its cache (stored in node_modules/.cache/prettier)
# so it gets saved as part of the node_modules cache below
- name: Run Prettier to populate cache
run: bun run lint-prettier-pure
- name: Update node_modules cache
uses: ./.github/actions/update-cache
with:
path: |
${{ steps.bun-cache.outputs.dir }}
${{ steps.yarn-cache-dir.outputs.dir }}
node_modules
key: ${{ env.NODE_CACHE_KEY }}
- name: Update Cypress cache
uses: ./.github/actions/update-cache
with:
path: ~/.cache/Cypress
key: ${{ env.CYPRESS_CACHE_KEY }}
# ESLint cache (requires both backend and frontend)
- name: Compile CLJS
run: bun run build-pure:cljs
- name: Run ESLint to populate cache
run: bun run lint-eslint-pure
- name: Update eslint cache
uses: ./.github/actions/update-cache
with:
path: ${{ github.workspace}}/.eslintcache
key: ${{ env.ESLINT_CACHE_KEY }}
- name: Update Cypress cache
uses: ./.github/actions/update-cache
with:
path: ~/.cache/Cypress
key: ${{ env.CYPRESS_CACHE_KEY }}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,78 @@
name: "Claude Docs Review"
on:
pull_request:
types:
- opened
- synchronize
- reopened
paths:
- 'docs/**/*.md'
- 'docs/**/*.mdx'
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to review'
required: true
type: number
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}-claude-docs-review
cancel-in-progress: true
jobs:
review:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
# Make sure the value of GITHUB_TOKEN will not be persisted in repo's config
persist-credentials: false
- name: Delete old Claude docs review comments
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }}
shell: bash
run: |
# Delete old reviews from the bot (reviews contain inline comments)
old_review_ids=$(gh api "/repos/$REPO/pulls/$PR_NUMBER/reviews" --jq '.[] | select(.user.login=="metabase-bot[bot]") | .id')
echo "Deleting reviews: $old_review_ids"
for id in $old_review_ids; do
# Note: This deletes the review and all associated comments
gh api -X DELETE "/repos/$REPO/pulls/$PR_NUMBER/reviews/$id"
echo "Deleted review $id"
done
- name: Generate GitHub App Token for Claude
uses: actions/create-github-app-token@v1
id: claude-app-token
with:
app-id: ${{ secrets.METABASE_BOT_APP_ID }}
private-key: ${{ secrets.METABASE_BOT_APP_PRIVATE_KEY }}
permission-contents: read # Required for reading repository files and PR diffs
permission-pull-requests: write # Required for creating inline comments and PR comments
- uses: anthropics/claude-code-action@v1
timeout-minutes: 10
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ steps.claude-app-token.outputs.token }}
prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }}
Review the documentation changes in this pull request.
Use the docs-review skill to review all changed documentation files for compliance with the Metabase writing style guide.
DO NOT post any comments confirming that something is correct - only post comments for style violations or concerns.
claude_args: |
--allowedTools "mcp__github__create_pending_pull_request_review,mcp__github__get_pull_request_diff,mcp__github__add_pull_request_review_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,Bash(gh pr view:*)"

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

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

View File

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

View File

@@ -1,118 +1,19 @@
name: Codespell
on:
schedule:
# Run nightly at 4:00 AM UTC
- cron: "0 4 * * *"
workflow_dispatch:
push:
branches:
- 'master'
- 'release-**'
pull_request:
jobs:
codespell:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Install codespell
run: pip install codespell
- name: Set up git configuration
run: |
git config --global user.name "Claude Code[bot]"
git config --global user.email "noreply@anthropic.com"
- name: Get current date
id: date
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Run codespell check
id: codespell_check
continue-on-error: true
run: |
set -o pipefail
./bin/mage codespell 2>&1 | tee codespell_output.txt
- name: Generate GitHub App Token for Claude
if: steps.codespell_check.outcome == 'failure'
uses: actions/create-github-app-token@v1
id: claude-app-token
- uses: actions/checkout@v3
- uses: codespell-project/actions-codespell@v1
with:
app-id: ${{ secrets.METABASE_BOT_APP_ID }}
private-key: ${{ secrets.METABASE_BOT_APP_PRIVATE_KEY }}
- name: Build Claude prompt
if: steps.codespell_check.outcome == 'failure'
run: |
cat > /tmp/claude_prompt.txt << 'PROMPT_HEADER'
You are tasked with fixing spelling errors detected by codespell in this repository.
## Codespell Output
The following spelling errors were detected:
```
PROMPT_HEADER
cat codespell_output.txt >> /tmp/claude_prompt.txt
cat >> /tmp/claude_prompt.txt << 'PROMPT_FOOTER'
```
## Instructions
1. **Review the codespell output and identify reported spelling errors** - The usual format codespell uses is `typo ==> correction`
2. **Fix every spelling error** - Use the Edit tool to fix each misspelling with the suggested correction
- If codespell suggests multiple corrections, choose the most appropriate one based on context
Human reviewers will verify the changes before merging, so fix all errors reported by codespell.
PROMPT_FOOTER
- name: Fix spelling errors with Claude Code
if: steps.codespell_check.outcome == 'failure'
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ steps.claude-app-token.outputs.token }}
prompt_file: /tmp/claude_prompt.txt
claude_args: |
--allowedTools "Write,Read,Glob,Grep,Edit,MultiEdit,Bash(cat*),Bash(ls*)"
--disallowedTools "Bash(git commit*),Bash(git push*),Bash(gh*)"
- name: Clean up temporary files
if: steps.codespell_check.outcome == 'failure'
run: rm -f codespell_output.txt
- name: Create Pull Request
if: steps.codespell_check.outcome == 'failure'
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: claude-fix/codespell-${{ steps.date.outputs.date }}
base: master
delete-branch: true
commit-message: |
Fix spelling errors detected by codespell (${{ steps.date.outputs.date }})
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
committer: "Claude Code[bot] <noreply@anthropic.com>"
author: "Claude Code[bot] <noreply@anthropic.com>"
title: "Fix spelling errors detected by codespell (${{ steps.date.outputs.date }})"
body: |
This PR automatically fixes spelling errors detected by the scheduled codespell check.
## Automated Fix
🤖 This fix was generated automatically by Claude Code based on codespell output.
**Please review carefully:**
- [ ] The spelling fixes are correct
- [ ] No code functionality was changed
- [ ] Technical terms were preserved correctly
---
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
labels: |
automated-fix
backport
# keep this list in sync with the list in `mage/src/mage/codespell.yml`
path: >-
docs

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,52 +12,12 @@ concurrency:
cancel-in-progress: true
jobs:
# This job determines which driver tests should run based on PR context.
# All decision logic is consolidated in mage driver-decisions.
# This job will determine if the tier 3 driver tests should be skipped.
# It will always run first.
determine-driver-skips:
runs-on: ubuntu-latest
outputs:
# 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 }}
skip: ${{ steps.check-branch-and-files-changed.outputs.skip }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -66,31 +26,42 @@ 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 }}
- name: Determine which drivers should run
id: driver-decisions
# Only run this if it's a pull request and the base ref exists
- name: Get cache keys
uses: ./.github/actions/get-cache-keys
id: get-cache-keys
- name: Restore babashka cache
uses: actions/cache/restore@v4
with:
path: |
~/.deps.clj
${{ github.workspace}}/bin/bb
key: ${{ steps.get-cache-keys.outputs.m2-cache-key }}
- name: Check if driver tests should run
id: check-branch-and-files-changed
run: |
# All decision logic is in mage - it analyzes module dependencies,
# branch context, labels, and file changes to decide what runs.
MAGE_ARGS=(
--git-ref="${{ github.event.pull_request.base.ref || github.ref_name }}"
--is-master-or-release="${{ github.ref_name == 'master' || startsWith(github.ref_name, 'release-') }}"
--pr-labels="${{ join(github.event.pull_request.labels.*.name, ',') }}"
--skip="${{ inputs.skip }}"
)
echo "Running:"
echo "./bin/mage -driver-decisions ${MAGE_ARGS[@]}"
echo ""
# First call: show human-readable output for debugging
./bin/mage -driver-decisions "${MAGE_ARGS[@]}"
# Second call: output only key=value lines for GITHUB_OUTPUT
./bin/mage -driver-decisions "${MAGE_ARGS[@]}" --github-output-only >> $GITHUB_OUTPUT
IS_MASTER_OR_RELEASE="${{ github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/release-') }}"
echo "Is master or release: ${IS_MASTER_OR_RELEASE}"
if [[ "${IS_MASTER_OR_RELEASE}" == "true" ]]; then
echo "skip=false" >> $GITHUB_OUTPUT
echo "Merging into master or a release branch. All driver tests will run."
else
echo "Checking for changes in relevant modules..."
if ./bin/mage can-skip-driver-tests "${{ github.event.pull_request.base.ref }}"; then
echo 'skip=true' >> $GITHUB_OUTPUT
else
echo 'skip=false' >> $GITHUB_OUTPUT
fi
fi
shell: bash
be-tests-athena-ee:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.athena-should-run == 'true' || needs.determine-driver-skips.outputs.athena-quarantine-conflict == 'true' }}
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 40
timeout-minutes: 60
env:
CI: 'true'
DRIVERS: athena
@@ -105,19 +76,8 @@ 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
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-athena-ee'
@@ -135,11 +95,10 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
be-tests-bigquery-cloud-sdk-ee:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.bigquery-should-run == 'true' || needs.determine-driver-skips.outputs.bigquery-quarantine-conflict == 'true' }}
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
@@ -150,11 +109,11 @@ jobs:
test-args: >-
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/transforms-python-test]
:fail-fast? true
- name: Transforms Python Tests
test-args: >-
:only-tags [:mb/transforms-python-test]
needs-python-runner: true
# DS 2025-09-26 temporarily disabled while we diagnose flakes
# - name: Transforms Python Tests
# test-args: >-
# :only-tags [:mb/transforms-python-test]
# needs-python-runner: true
env:
CI: 'true'
DRIVERS: bigquery-cloud-sdk
@@ -167,16 +126,6 @@ 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 }}
@@ -189,7 +138,6 @@ jobs:
with:
junit-name: 'be-tests-bigquery-cloud-sdk-ee'
test-args: ${{ matrix.job.test-args }}
logs-artifact-suffix: ${{ matrix.job.name }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -201,7 +149,6 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
- name: Cleanup python-runner
if: ${{ always() && matrix.job.needs-python-runner == true }}
uses: ./.github/actions/cleanup-python-runner
@@ -211,9 +158,9 @@ jobs:
be-tests-clickhouse-ee:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.clickhouse-should-run == 'true' || needs.determine-driver-skips.outputs.clickhouse-quarantine-conflict == 'true' }}
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 40
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
@@ -235,16 +182,6 @@ 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 }}
@@ -272,7 +209,6 @@ jobs:
with:
junit-name: 'be-tests-clickhouse-ee'
test-args: ${{ matrix.job.test-args }}
logs-artifact-suffix: ${{ matrix.job.name }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -284,7 +220,6 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
- name: Cleanup python-runner
if: ${{ always() && matrix.job.needs-python-runner == true }}
uses: ./.github/actions/cleanup-python-runner
@@ -294,9 +229,9 @@ jobs:
be-tests-druid-ee:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.druid-should-run == 'true' || needs.determine-driver-skips.outputs.druid-quarantine-conflict == 'true' }}
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 40
timeout-minutes: 60
env:
CI: 'true'
DRIVERS: druid
@@ -309,19 +244,8 @@ 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
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-druid-ee'
@@ -339,13 +263,12 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
be-tests-druid-jdbc-ee:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.druid-jdbc-should-run == 'true' || needs.determine-driver-skips.outputs.druid-jdbc-quarantine-conflict == 'true' }}
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 40
timeout-minutes: 60
env:
CI: 'true'
DRIVERS: druid-jdbc
@@ -358,19 +281,8 @@ 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
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-druid-jdbc-ee'
@@ -388,13 +300,12 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
be-tests-databricks-ee:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.databricks-should-run == 'true' || needs.determine-driver-skips.outputs.databricks-quarantine-conflict == 'true' }}
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 40
timeout-minutes: 60
env:
CI: 'true'
DRIVERS: databricks
@@ -407,19 +318,8 @@ 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
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-databricks-ee'
@@ -437,13 +337,12 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
be-tests-databricks-multi-catalog-ee:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.databricks-should-run == 'true' || needs.determine-driver-skips.outputs.databricks-quarantine-conflict == 'true' }}
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 40
timeout-minutes: 60
env:
CI: 'true'
DRIVERS: databricks
@@ -457,19 +356,8 @@ 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
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-databricks-multi-catalog-ee'
@@ -487,13 +375,11 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
be-tests-h2:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.h2-should-run == 'true' }}
if: ${{ !inputs.skip }}
runs-on: ubuntu-latest
timeout-minutes: 40
timeout-minutes: 60
env:
CI: 'true'
name: H2
@@ -518,52 +404,19 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
be-tests-h2-oss:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.h2-should-run == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 40
env:
CI: 'true'
name: H2 (OSS)
steps:
- uses: actions/checkout@v4
- name: Test H2 driver (OSS)
id: test-driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-h2-oss'
edition: 'oss'
test-args: >-
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/transforms-python-test]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
be-tests-mysql-mariadb:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.mysql-mariadb-should-run == 'true' }}
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 40
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
version:
- name: MariaDB 10.6
junit-name: be-tests-mariadb-10-6-ee
image: circleci/mariadb:10.6
- name: MariaDB 10.2
junit-name: be-tests-mariadb-10-2-ee
image: circleci/mariadb:10.2.23
env:
enable-ssl-tests: 'false'
enable-aws-iam-tests: 'false'
@@ -573,9 +426,9 @@ jobs:
env:
enable-ssl-tests: 'false'
enable-aws-iam-tests: 'false'
- name: MySQL 8.4
junit-name: be-tests-mysql-8-4-ee
image: mysql:8.4
- name: MySQL 8.0
junit-name: be-tests-mysql-8-0-ee
image: mysql:8.0
env:
enable-ssl-tests: 'false'
enable-aws-iam-tests: 'false'
@@ -612,8 +465,8 @@ jobs:
(
github.event_name == 'push' &&
(
github.ref_name == 'master' ||
startsWith(github.ref_name, 'release-')
github.ref == 'master' ||
startsWith(github.ref, 'release-')
) &&
''
) ||
@@ -666,7 +519,6 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: ${{ matrix.version.junit-name }}
logs-artifact-suffix: ${{ matrix.version.name }}
test-args: >-
${{ matrix.job.test-args }}
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
@@ -681,7 +533,6 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
- name: Cleanup python-runner
if: always()
uses: ./.github/actions/cleanup-python-runner
@@ -691,9 +542,9 @@ jobs:
be-tests-mongo:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.mongo-should-run == 'true' || needs.determine-driver-skips.outputs.mongo-quarantine-conflict == 'true' }}
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 40
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
@@ -728,16 +579,6 @@ 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 }}
@@ -750,7 +591,6 @@ jobs:
with:
junit-name: ${{ matrix.version.junit-name }}
test-args: ${{ matrix.job.test-args }}
logs-artifact-suffix: ${{ matrix.job.name }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -762,7 +602,6 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
- name: Cleanup python-runner
if: ${{ always() && matrix.job.needs-python-runner == true }}
uses: ./.github/actions/cleanup-python-runner
@@ -772,9 +611,9 @@ jobs:
be-tests-mongo-ssl:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.mongo-ssl-should-run == 'true' || needs.determine-driver-skips.outputs.mongo-ssl-quarantine-conflict == 'true' }}
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 40
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
@@ -790,16 +629,6 @@ 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: >-
@@ -827,7 +656,6 @@ jobs:
curl https://raw.githubusercontent.com/metabase/metabase-qa/master/dbs/mongo/certificates/metaca.crt \
-o ./test_resources/ssl/mongo/metaca.crt
- name: Test ${{ matrix.version.name }}
id: test-driver
uses: ./.github/actions/test-driver
with:
junit-name: ${{ matrix.version.junit-name }}
@@ -845,13 +673,12 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
be-tests-mongo-sharded-cluster-ee:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.mongo-sharded-cluster-should-run == 'true' || needs.determine-driver-skips.outputs.mongo-sharded-cluster-quarantine-conflict == 'true' }}
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 40
timeout-minutes: 60
env:
CI: 'true'
DRIVERS: mongo
@@ -862,19 +689,8 @@ 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
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mongo-sharded-cluster-ee'
@@ -891,13 +707,12 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
be-tests-oracle:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.oracle-should-run == 'true' || needs.determine-driver-skips.outputs.oracle-quarantine-conflict == 'true' }}
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 40
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
@@ -946,23 +761,11 @@ 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
uses: ./.github/actions/test-driver
with:
junit-name: ${{ matrix.version.junit-name }}
logs-artifact-suffix: ${{ matrix.version.name }}
test-args: >-
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/transforms-python-test]
@@ -977,20 +780,19 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
be-tests-postgres:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.postgres-should-run == 'true' }}
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 40
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
version:
- name: Postgres 14.x
- name: Postgres 12.x
junit-name: postgres-ee
docker-image: postgres:14-alpine
docker-image: postgres:12-alpine
env:
enable-ssl-tests: 'false'
enable-aws-iam-tests: 'false'
@@ -1019,8 +821,8 @@ jobs:
(
github.event_name == 'push' &&
(
github.ref_name == 'master' ||
startsWith(github.ref_name, 'release-')
github.ref == 'master' ||
startsWith(github.ref, 'release-')
) &&
''
) ||
@@ -1067,7 +869,6 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-${{ matrix.version.junit-name }}'
logs-artifact-suffix: ${{ matrix.version.name }}-${{ matrix.job.name }}
test-args: >-
${{ matrix.job.test-args }}
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
@@ -1082,7 +883,6 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
- name: Cleanup python-runner
if: always()
uses: ./.github/actions/cleanup-python-runner
@@ -1092,9 +892,9 @@ jobs:
be-tests-presto-jdbc-ee:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.presto-jdbc-should-run == 'true' || needs.determine-driver-skips.outputs.presto-jdbc-quarantine-conflict == 'true' }}
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 40
timeout-minutes: 60
env:
CI: 'true'
DRIVERS: presto-jdbc
@@ -1115,16 +915,6 @@ 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:
@@ -1152,7 +942,6 @@ jobs:
-file /tmp/presto-ssl-ca.der
-trustcacerts
- name: Test Presto JDBC driver
id: test-driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-presto-jdbc-ee'
@@ -1170,13 +959,12 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
be-tests-redshift-ee:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.redshift-should-run == 'true' || needs.determine-driver-skips.outputs.redshift-quarantine-conflict == 'true' }}
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 40
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
@@ -1186,22 +974,12 @@ jobs:
test-args: >-
:only-tags [:mb/upload-tests]
:fail-fast? true
- name: Redshift (Excluding Upload Tests) Part 1
- name: Redshift (Excluding Upload Tests)
skip: false
test-args: >-
:only-tags [:mb/driver-tests]
:exclude-tags '[:mb/upload-tests :mb/transforms-python-test]'
:fail-fast? true
:partition/total 2
:partition/index 0
- name: Redshift (Excluding Upload Tests) Part 2
skip: false
test-args: >-
:only-tags [:mb/driver-tests]
:exclude-tags '[:mb/upload-tests :mb/transforms-python-test]'
:fail-fast? true
:partition/total 2
:partition/index 1
- name: Redshift Transforms Python Tests
skip: false
test-args: >-
@@ -1219,16 +997,6 @@ 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 }}
@@ -1243,7 +1011,6 @@ jobs:
junit-name: 'be-tests-redshift-ee'
test-args: >-
${{ matrix.job.test-args }}
logs-artifact-suffix: ${{ matrix.job.name }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: ${{ !matrix.job.skip && !cancelled() }}
@@ -1255,7 +1022,6 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
- name: Cleanup python-runner
if: ${{ always() && matrix.job.needs-python-runner == true }}
uses: ./.github/actions/cleanup-python-runner
@@ -1264,8 +1030,7 @@ jobs:
logs-artifact-suffix: ${{ matrix.job.name }} ${{ matrix.version.name }}
be-tests-snowflake-ee:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.snowflake-should-run == 'true' || needs.determine-driver-skips.outputs.snowflake-quarantine-conflict == 'true' }}
if: ${{ !inputs.skip }}
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
@@ -1276,11 +1041,12 @@ jobs:
test-args: >-
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/transforms-python-test]
:fail-fast? true
- name: Transforms Python Tests
test-args: >-
:only-tags [:mb/transforms-python-test]
needs-python-runner: true
# DS 2025-09-26 temporarily disabled while we diagnose flakes
# - name: Transforms Python Tests
# test-args: >-
# :only-tags [:mb/transforms-python-test]
# needs-python-runner: true
env:
CI: 'true'
DRIVERS: snowflake
@@ -1302,16 +1068,6 @@ 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 }}
@@ -1324,7 +1080,6 @@ jobs:
with:
junit-name: 'be-tests-snowflake-ee'
test-args: ${{ matrix.job.test-args }}
logs-artifact-suffix: ${{ matrix.job.name }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -1336,7 +1091,6 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
- name: Cleanup python-runner
if: ${{ always() && matrix.job.needs-python-runner == true }}
uses: ./.github/actions/cleanup-python-runner
@@ -1346,9 +1100,9 @@ jobs:
be-tests-sparksql-ee:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.sparksql-should-run == 'true' || needs.determine-driver-skips.outputs.sparksql-quarantine-conflict == 'true' }}
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 40
timeout-minutes: 60
env:
CI: 'true'
DRIVERS: sparksql
@@ -1359,19 +1113,8 @@ 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
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-sparksql-ee'
@@ -1389,31 +1132,19 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
be-tests-sqlite-ee:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.sqlite-should-run == 'true' || needs.determine-driver-skips.outputs.sqlite-quarantine-conflict == 'true' }}
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 40
timeout-minutes: 60
env:
CI: 'true'
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
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-sqlite-ee'
@@ -1431,11 +1162,10 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
be-tests-sqlserver:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.sqlserver-should-run == 'true' || needs.determine-driver-skips.outputs.sqlserver-quarantine-conflict == 'true' }}
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
@@ -1474,16 +1204,6 @@ 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 }}
@@ -1496,7 +1216,6 @@ jobs:
with:
junit-name: ${{ matrix.version.junit-name }}
test-args: ${{ matrix.job.test-args }}
logs-artifact-suffix: ${{ matrix.version.name }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -1508,7 +1227,6 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
- name: Cleanup python-runner
if: ${{ always() && matrix.job.needs-python-runner == true }}
uses: ./.github/actions/cleanup-python-runner
@@ -1521,7 +1239,7 @@ jobs:
# needs: determine-driver-skips
# if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
# runs-on: ubuntu-latest
# timeout-minutes: 40
# timeout-minutes: 60
# env:
# CI: 'true'
# DRIVERS: starburst
@@ -1557,7 +1275,7 @@ jobs:
login-to-ecr:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.vertica-should-run == 'true' || needs.determine-driver-skips.outputs.vertica-quarantine-conflict == 'true' }}
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
@@ -1587,9 +1305,9 @@ jobs:
be-tests-vertica-ee:
needs: [determine-driver-skips, login-to-ecr]
if: ${{ needs.determine-driver-skips.outputs.vertica-should-run == 'true' || needs.determine-driver-skips.outputs.vertica-quarantine-conflict == 'true' }}
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 40
timeout-minutes: 60
env:
CI: 'true'
DRIVERS: vertica
@@ -1603,21 +1321,10 @@ 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
- name: Test Vertica driver
id: test-driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-vertica-ee'
@@ -1635,7 +1342,6 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
drivers-tests-result:
needs:

View File

@@ -0,0 +1,188 @@
# This workflow does the backward compatibility tests
# between the Embedding SDK package of version N and Embedding SDK Bundle of version N - 1
# N - 1 is a target of a PR
name: Embedding SDK backward-compatibility E2E Component Tests
on:
workflow_call:
inputs:
cached-embedding-sdk-dist-artifact-name:
required: true
type: string
jobs:
files-changed:
name: "SDK backward-compatibility E2E > Check which files changed"
runs-on: ubuntu-22.04
timeout-minutes: 3
outputs:
e2e_embedding_sdk: ${{ steps.changes.outputs.e2e_embedding_sdk }}
steps:
- uses: actions/checkout@v4
- name: Test which files changed
uses: dorny/paths-filter@v3.0.0
id: changes
with:
token: ${{ github.token }}
filters: .github/file-paths.yaml
decide-target-ref:
runs-on: ubuntu-latest
outputs:
target-ref: ${{ steps.get-target-ref.outputs.value }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Decide ref to checkout
id: get-target-ref
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "value=${{ github.base_ref }}" >> "$GITHUB_OUTPUT"
else
echo "value=$(git rev-parse HEAD^1)" >> "$GITHUB_OUTPUT"
fi
# if this is a test on a release branch, we need to check the build requirements
get-build-requirements:
name: "SDK backward-compatibility E2E > [Backport only] Get build requirements"
needs: [decide-target-ref]
if: |
!cancelled() &&
contains(github.base_ref || github.ref, 'release-x')
runs-on: ubuntu-22.04
timeout-minutes: 10
outputs:
java_version: ${{ fromJson(steps.dependencies.outputs.result).java_version }}
node_version: ${{ fromJson(steps.dependencies.outputs.result).node_version }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.decide-target-ref.outputs.target-ref }}
sparse-checkout: release
- name: Prepare build scripts
run: cd ${{ github.workspace }}/release && yarn && yarn build
- name: Get build dependencies
uses: actions/github-script@v7
id: dependencies
with:
script: | # js
const {
getBuildRequirements,
getVersionFromReleaseBranch,
} = require('${{ github.workspace }}/release/dist/index.cjs');
const targetBranchName = '${{ github.base_ref || github.ref }}';
const version = getVersionFromReleaseBranch(targetBranchName);
const requirements = getBuildRequirements(version);
return {
java_version: requirements.java,
node_version: requirements.node,
};
e2e-tests:
name: "SDK backward-compatibility E2E > Run e2e tests"
needs: [files-changed, decide-target-ref, get-build-requirements]
if: |
!cancelled() &&
needs.files-changed.outputs.e2e_embedding_sdk == 'true'
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()`
# 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
with:
ref: ${{ needs.decide-target-ref.outputs.target-ref }}
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
with:
java-version: "${{ needs.get-build-requirements.outputs.java_version || 21 }}"
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
with:
node-version: "${{ needs.get-build-requirements.outputs.node_version }}"
- name: Build uberjar with ./bin/build.sh
run: ./bin/build.sh
- name: Retrieve Embedding SDK dist artifact
uses: actions/download-artifact@v4
with:
name: ${{ inputs.cached-embedding-sdk-dist-artifact-name }}
path: ${{ github.workspace }}/resources/embedding-sdk
- name: Prepare Cypress environment
id: cypress-prep
uses: ./.github/actions/prepare-cypress
- name: Run Metabase
run: node e2e/runner/run_cypress_ci.js start
- name: Make app db snapshot
env:
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
run: node e2e/runner/run_cypress_ci.js snapshot
- name: Run component tests for Embedding SDK
env:
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
run: |
node e2e/runner/run_cypress_ci.js component \
--env grepTags="-@skip-backward-compatibility"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit
output-name: e2e-embedding-sdk
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
- name: Upload Cypress Artifacts upon failure
uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-recording-embedding-sdk-cross-version
path: |
./cypress
./logs/test.log
if-no-files-found: ignore
- name: Publish Summary
if: failure()
uses: actions/github-script@v7
with:
script: | #js
const {
generateReport,
parseReport,
formatSummary
} = require("./.github/scripts/handle-mochawesome-report.js");
const report = await generateReport();
const results = parseReport(report);
const summary = formatSummary(results);
await core.summary.addRaw(summary).write();

View File

@@ -0,0 +1,207 @@
name: E2E Component Tests for Embedding SDK
on:
workflow_call:
inputs:
cached-embedding-sdk-dist-artifact-name:
required: true
type: string
jobs:
files-changed:
name: "SDK e2e > Check which files changed"
runs-on: ubuntu-22.04
timeout-minutes: 3
outputs:
e2e_embedding_sdk: ${{ steps.changes.outputs.e2e_embedding_sdk }}
steps:
- uses: actions/checkout@v4
- name: Test which files changed
uses: dorny/paths-filter@v3.0.0
id: changes
with:
token: ${{ github.token }}
filters: .github/file-paths.yaml
# if this is a test on a release branch, we need to check the build requirements
get-build-requirements:
name: "SDK e2e > [Backport only] Get build requirements"
if: |
!cancelled() &&
contains(github.base_ref || github.ref, 'release-x')
runs-on: ubuntu-22.04
timeout-minutes: 10
outputs:
java_version: ${{ fromJson(steps.dependencies.outputs.result).java_version }}
node_version: ${{ fromJson(steps.dependencies.outputs.result).node_version }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: release
- name: Prepare build scripts
run: cd ${{ github.workspace }}/release && yarn && yarn build
- name: Get build dependencies
uses: actions/github-script@v7
id: dependencies
with:
script: | # js
const {
getBuildRequirements,
getVersionFromReleaseBranch,
} = require('${{ github.workspace }}/release/dist/index.cjs');
const targetBranchName = '${{ github.base_ref || github.ref }}';
const version = getVersionFromReleaseBranch(targetBranchName);
const requirements = getBuildRequirements(version);
return {
java_version: requirements.java,
node_version: requirements.node,
};
build-uberjar:
name: "SDK e2e > Build uberjar"
needs: [files-changed, get-build-requirements]
if: |
!cancelled() &&
needs.files-changed.outputs.e2e_embedding_sdk == 'true'
runs-on: ubuntu-22.04
timeout-minutes: 25
env:
MB_EDITION: ee
INTERACTIVE: false
# make sure that builds on release branches get licenses, because we use them for releases
SKIP_LICENSES: ${{ github.event_name == 'pull_request' }}
steps:
- uses: actions/checkout@v4
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
with:
node-version: "${{ needs.get-build-requirements.outputs.node_version }}"
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
with:
java-version: "${{ needs.get-build-requirements.outputs.java_version || 21 }}"
- name: Build uberjar with ./bin/build.sh
run: ./bin/build.sh
- name: Prepare uberjar artifact
uses: ./.github/actions/prepare-uberjar-artifact
with:
name: metabase-ee-${{ github.event.pull_request.head.sha || github.sha }}-uberjar-sdk-tests
e2e-tests:
name: "SDK e2e > Run e2e tests (React ${{ matrix.react-version }})"
needs: [get-build-requirements, build-uberjar]
if: |
!cancelled() &&
needs.build-uberjar.result == 'success'
runs-on: ubuntu-22.04
timeout-minutes: 45
strategy:
matrix:
react-version: [18, 19]
fail-fast: false
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()`
# Example: you can get `CYPRESS_FOO` with `Cypress.env("FOO")`
CYPRESS_MB_ALL_FEATURES_TOKEN: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
CYPRESS_MB_STARTER_CLOUD_TOKEN: ${{ secrets.STAGING_MB_STARTER_CLOUD_TOKEN }}
CYPRESS_MB_PRO_CLOUD_TOKEN: ${{ secrets.STAGING_MB_PRO_CLOUD_TOKEN }}
CYPRESS_MB_PRO_SELF_HOSTED_TOKEN: ${{ secrets.STAGING_MB_PRO_SELF_HOSTED_TOKEN }}
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title || github.event.head_commit.message || github.event.head.sha }}
TZ: US/Pacific # to make node match the instance tz
CYPRESS_CI: true
steps:
- uses: actions/checkout@v4
- name: Retrieve uberjar artifact for ee
uses: actions/download-artifact@v4
with:
name: metabase-ee-${{ github.event.pull_request.head.sha || github.sha }}-uberjar-sdk-tests
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare JDK ${{ needs.get-build-requirements.outputs.java_version || 21 }}
uses: actions/setup-java@v4
with:
java-version: ${{ needs.get-build-requirements.outputs.java_version || 21 }}
distribution: "temurin"
- name: Prepare Cypress environment
id: cypress-prep
uses: ./.github/actions/prepare-cypress
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Retrieve Embedding SDK dist artifact
uses: actions/download-artifact@v4
with:
name: ${{ inputs.cached-embedding-sdk-dist-artifact-name }}
path: ${{ github.workspace }}/resources/embedding-sdk
- name: Run Metabase
run: node e2e/runner/run_cypress_ci.js start
- name: Make app db snapshot
env:
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
run: node e2e/runner/run_cypress_ci.js snapshot
# Has to be run after the app db snapshot is made, as it is not compatible with React 19
- name: Change React version to ${{ matrix.react-version }}
run: ./bin/embedding-sdk/change-react-version.bash ${{ matrix.react-version }}
- name: Run component tests for Embedding SDK
env:
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
run: node e2e/runner/run_cypress_ci.js component
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit
output-name: e2e-embedding-sdk-react-${{ matrix.react-version }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
- name: "SDK e2e > Upload Cypress Artifacts upon failure"
uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-recording-embedding-sdk-react-${{ matrix.react-version }}
path: |
./cypress
./logs/test.log
if-no-files-found: ignore
- name: "SDK e2e > Publish Summary"
if: failure()
uses: actions/github-script@v7
with:
script: | #js
const {
generateReport,
parseReport,
formatSummary
} = require("./.github/scripts/handle-mochawesome-report.js");
const report = await generateReport();
const results = parseReport(report);
const summary = formatSummary(results);
await core.summary.addRaw(summary).write();

View File

@@ -0,0 +1,413 @@
name: "[NOT REQUIRED] SDK Compatibility tests"
on:
push:
branches:
- "master"
- "release-**"
pull_request:
types: [opened, synchronize, reopened]
branches:
- "master"
- "release-**"
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
cancel-in-progress: true
jobs:
files-changed:
name: Check which files changed
runs-on: ubuntu-22.04
timeout-minutes: 3
outputs:
e2e_embedding_sdk_compatibility: ${{ steps.changes.outputs.e2e_embedding_sdk_compatibility }}
steps:
- uses: actions/checkout@v4
- name: Test which files changed
uses: dorny/paths-filter@v3.0.0
id: changes
with:
token: ${{ github.token }}
filters: .github/file-paths.yaml
# if this is a test on a release branch, we need to check the build requirements
get-build-requirements:
if: |
!cancelled() &&
contains(github.base_ref || github.ref, 'release-x')
runs-on: ubuntu-22.04
timeout-minutes: 10
outputs:
java_version: ${{ fromJson(steps.dependencies.outputs.result).java_version }}
node_version: ${{ fromJson(steps.dependencies.outputs.result).node_version }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: release
- name: Prepare build scripts
run: cd ${{ github.workspace }}/release && yarn && yarn build
- name: Get build dependencies
uses: actions/github-script@v7
id: dependencies
with:
script: | # js
const {
getBuildRequirements,
getVersionFromReleaseBranch,
} = require('${{ github.workspace }}/release/dist/index.cjs');
const targetBranchName = '${{ github.base_ref || github.ref }}';
const version = getVersionFromReleaseBranch(targetBranchName);
const requirements = getBuildRequirements(version);
return {
java_version: requirements.java,
node_version: requirements.node,
};
get-sample-apps-data:
if: |
!cancelled()
runs-on: ubuntu-22.04
timeout-minutes: 10
env:
# we must support last two stable releases
SUPPORTED_RELEASES_COUNT: 2
TEST_SUITES: |
[
"metabase-nodejs-react-sdk-embedding-sample-e2e",
"metabase-nextjs-sdk-embedding-sample-e2e",
"shoppy-e2e"
]
TEST_SUITES_TO_RUN_IN_MASTER_ONLY: |
[
"shoppy-e2e"
]
SAMPLE_APP_MAIN_BRANCH_NAME: "main"
SAMPLE_APP_STABLE_RELEASE_BRANCH_NAME_SUFFIX: "stable"
outputs:
branch_name: ${{ fromJson(steps.build-sample-apps-data.outputs.result).branch_name }}
matrix: ${{ toJSON(fromJson(steps.build-sample-apps-data.outputs.result).matrix) }}
steps:
- uses: actions/checkout@v4
- name: Prepare build scripts
run: cd ${{ github.workspace }}/release && yarn && yarn build
- name: Build sample app compatibility data
uses: actions/github-script@v7
id: build-sample-apps-data
with:
script: | # js
const {
isReleaseBranch,
getMajorVersionNumberFromReleaseBranch,
} = require('${{ github.workspace }}/release/dist/index.cjs');
const testSuites = JSON.parse(process.env.TEST_SUITES);
const testSuitesToRunInMasterOnly = JSON.parse(process.env.TEST_SUITES_TO_RUN_IN_MASTER_ONLY);
const ref = '${{ github.base_ref || github.ref }}';
const branchName = ref.replace(/^refs\/heads\//, '');
const isMainBranchRef = branchName === 'master';
const isReleaseBranchRef = isReleaseBranch(branchName);
if (isMainBranchRef) {
return {
branch_name: '${{ env.SAMPLE_APP_MAIN_BRANCH_NAME }}',
matrix: testSuites,
};
}
if (isReleaseBranchRef) {
const supportedReleasesCount = Number(${{ env.SUPPORTED_RELEASES_COUNT }});
const currentReleaseVersion = Number(${{ vars.CURRENT_VERSION }});
const currentBranchVersion = Number(getMajorVersionNumberFromReleaseBranch(branchName));
const sampleAppBranchName = `${currentBranchVersion}-${{ env.SAMPLE_APP_STABLE_RELEASE_BRANCH_NAME_SUFFIX }}`
const shouldRunTestsForVersion = (currentReleaseVersion - currentBranchVersion) <= (supportedReleasesCount - 1);
const releaseBranchTestSuites = testSuites.filter(testSuite => !testSuitesToRunInMasterOnly.includes(testSuite));
return {
branch_name: sampleAppBranchName,
matrix: shouldRunTestsForVersion
? releaseBranchTestSuites
: []
};
}
return {
branch_name: '',
matrix: []
};
build:
needs: [files-changed, get-build-requirements, get-sample-apps-data]
if: |
!cancelled() &&
needs.files-changed.outputs.e2e_embedding_sdk_compatibility == 'true' &&
fromJSON(needs.get-sample-apps-data.outputs.matrix)[0] != null
runs-on: ubuntu-22.04
timeout-minutes: 25
env:
MB_EDITION: ee
INTERACTIVE: false
# make sure that builds on release branches get licenses, because we use them for releases
SKIP_LICENSES: ${{ github.event_name == 'pull_request' }}
steps:
- uses: actions/checkout@v4
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
with:
node-version: "${{ needs.get-build-requirements.outputs.node_version }}"
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
with:
java-version: "${{ needs.get-build-requirements.outputs.java_version || 21 }}"
- name: Build uberjar with ./bin/build.sh
run: ./bin/build.sh
- name: Prepare uberjar artifact
uses: ./.github/actions/prepare-uberjar-artifact
with:
name: metabase-ee-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
- name: Build Embedding SDK package
run: |
# Remove the matchers to not report false positives on every PR
# unrelated to embedding (EMB-765)
echo "::remove-matcher owner=tsc::"
yarn build-embedding-sdk-package
- name: Prepare Embedding SDK artifact
uses: actions/upload-artifact@v4
with:
name: embedding-sdk-${{ github.event.pull_request.head.sha || github.sha }}
path: resources/embedding-sdk
# In order to check compatibility between Sample Apps and Embedding SDK, the job for each sample app:
# - pulls a Sample App
# - starts it
# - runs its Cypress tests against the local metabase.jar and local @metabase/embedding-sdk-react package.
# The job is not required, though if it's failing better to be sure that it's not caused by a breaking change.
# For more details see `Sample Apps compatibility with Embedding SDK tests` in `enterprise/frontend/src/embedding-sdk-package/dev.md`
e2e-sample-apps-tests:
needs: [get-build-requirements, get-sample-apps-data, build]
strategy:
matrix:
test_suite: ${{ fromJSON(needs.get-sample-apps-data.outputs.matrix) }}
environment: ["production", "development"]
if: |
!cancelled() && needs.build.result == 'success'
runs-on: ubuntu-22.04
timeout-minutes: 45
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:
id-token: write
steps:
- uses: actions/checkout@v4
# We have to use the latest version that does not have this issue https://github.com/docker/compose/pull/12752
- name: Set up Docker Compose
uses: docker/setup-compose-action@v1
with:
version: latest
- name: Get Sample App name
id: get-sample-app-name
shell: bash
run: |
test_suite="${{ matrix.test_suite }}"
echo "sample_app_name=${test_suite%-e2e}" >> "$GITHUB_OUTPUT"
- name: Retrieve uberjar artifact for ee
uses: actions/download-artifact@v4
with:
name: metabase-ee-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
- name: Retrieve Embedding SDK dist artifact
uses: actions/download-artifact@v4
with:
name: embedding-sdk-${{ github.event.pull_request.head.sha || github.sha }}
path: ${{ github.workspace }}/resources/embedding-sdk
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare Cypress environment
id: cypress-prep
uses: ./.github/actions/prepare-cypress
- name: Check if sample app branch exists
id: check-sample-app-branch
run: |
TEST_SUITE="${{ matrix.test_suite }}"
REPO_NAME="${TEST_SUITE%-e2e}"
REPO_URL="https://github.com/metabase/${REPO_NAME}.git"
echo "Checking if branch '${SAMPLE_APP_BRANCH_NAME}' exists in ${REPO_URL}..."
if git ls-remote --exit-code --heads "$REPO_URL" "${SAMPLE_APP_BRANCH_NAME}"; then
echo "sample_app_branch_exists=true" >> $GITHUB_OUTPUT
else
echo "sample_app_branch_exists=false" >> $GITHUB_OUTPUT
fi
- name: Configure AWS credentials
if: ${{ matrix.test_suite == 'shoppy-e2e' }}
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.SHOPPY_IAM_ROLE }}
role-session-name: GitHub_to_AWS_via_FederatedOIDC
aws-region: us-east-1
- name: Download Shoppy's App DB PostgreSQL dump from S3
if: ${{ matrix.test_suite == 'shoppy-e2e' }}
run: |
aws s3 cp s3://${{ secrets.SHOPPY_METABASE_APP_DB_S3_BUCKET }}/${{ secrets.SHOPPY_METABASE_APP_DB_S3_KEY }} ./e2e/tmp/db_dumps/shoppy_metabase_app_db_dump.sql
- name: Prepare and launch Sample Apps for shoppy-e2e test suite
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' && matrix.test_suite == 'shoppy-e2e' }}
env:
METABASE_APP_DB_USER: ${{ secrets.SHOPPY_METABASE_APP_DB_USER }}
METABASE_APP_DB: ${{ secrets.SHOPPY_METABASE_APP_DB_DATABASE }}
run: npx tsx ./e2e/runner/embedding-sdk/sample-apps/start-ci.ts shoppy-e2e
- name: Prepare and launch Sample Apps for other test suites
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' && matrix.test_suite != 'shoppy-e2e' }}
run: npx tsx ./e2e/runner/embedding-sdk/sample-apps/start-ci.ts ${{ matrix.test_suite }}
- name: Run e2e tests for Sample App
id: run-e2e-tests
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' }}
continue-on-error: true
env:
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
run: node e2e/runner/run_cypress_ci.js ${{ matrix.test_suite }}
- name: Upload Cypress Artifacts upon failure
uses: actions/upload-artifact@v4
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' && steps.run-e2e-tests.outcome != 'success' }}
with:
name: cypress-recording-sample-app-${{ matrix.test_suite }}-latest
path: |
./e2e/tmp/${{ steps.get-sample-app-name.outputs.sample_app_name }}/cypress
if-no-files-found: ignore
- name: Fail job if tests failed
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' && steps.run-e2e-tests.outcome != 'success' }}
run: |
exit 1
# The job checks integration of the Embedding SDK with consumer's apps that use different frameworks/bundlers,
# or when we want to test some tricky integration cases like conflicting types.
# The job
# - starts an app
# - runs its Cypress tests against the e2e test metabase instance and local @metabase/embedding-sdk-react package.
# The job is not required, though if it's failing better to be sure that it's not caused by a breaking change.
# For more details see `Embedding SDK integration tests with Host Apps` in `enterprise/frontend/src/embedding-sdk-package/dev.md`
e2e-host-apps-tests:
needs: [get-build-requirements, build]
strategy:
matrix:
test_suite:
[
"vite-6-host-app-e2e",
"next-15-app-router-host-app-e2e",
"next-15-pages-router-host-app-e2e",
"angular-20-host-app-e2e",
]
environment: ["production", "development"]
if: |
!cancelled() && needs.build.result == 'success'
runs-on: ubuntu-22.04
timeout-minutes: 45
name: e2e-host-app-${{ matrix.test_suite }}-tests-${{ matrix.environment }}-bundle
env:
MB_EDITION: ee
DISPLAY: ""
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number || '' }}
HASH: ${{ github.event.pull_request.head.sha || github.sha }}-${{ github.run_attempt }}
MB_RUN_MODE: e2e
METASTORE_DEV_SERVER_URL: ${{ secrets.METASTORE_DEV_SERVER_URL }}
CYPRESS_MB_ALL_FEATURES_TOKEN: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
TZ: US/Pacific # to make node match the instance tz
CYPRESS_CI: true
HOST_APP_ENVIRONMENT: ${{ matrix.environment }}
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- name: Retrieve uberjar artifact for ee
uses: actions/download-artifact@v4
with:
name: metabase-ee-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
- name: Retrieve Embedding SDK dist artifact
uses: actions/download-artifact@v4
with:
name: embedding-sdk-${{ github.event.pull_request.head.sha || github.sha }}
path: ${{ github.workspace }}/resources/embedding-sdk
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare Cypress environment
id: cypress-prep
uses: ./.github/actions/prepare-cypress
- name: Prepare JDK 21
uses: actions/setup-java@v4
with:
java-version: 21
distribution: "temurin"
- name: Run Metabase
run: node e2e/runner/run_cypress_ci.js start
- name: Generate database snapshots
run: node e2e/runner/run_cypress_ci.js snapshot
- name: Prepare and launch Host App
run: npx tsx ./e2e/runner/embedding-sdk/host-apps/start-ci.ts ${{ matrix.test_suite }}
- name: Run e2e tests for Host App
id: run-e2e-tests
continue-on-error: true
env:
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
run: node e2e/runner/run_cypress_ci.js ${{ matrix.test_suite }}
- name: Upload Cypress Artifacts upon failure
uses: actions/upload-artifact@v4
if: ${{ steps.run-e2e-tests.outcome != 'success' }}
with:
name: cypress-recording-host-app-${{ matrix.test_suite }}-${{ matrix.environment }}-latest
path: |
./cypress
if-no-files-found: ignore
- name: Fail job if tests failed
if: ${{ steps.run-e2e-tests.outcome != 'success' }}
run: |
exit 1

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