Compare commits
4 Commits
hackathon-
...
python_run
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a43b2d35f | ||
|
|
832f8ac44d | ||
|
|
88bdc6c06c | ||
|
|
bdfc24938c |
@@ -1 +0,0 @@
|
||||
Run mutation testing for $ARGUMENTS
|
||||
@@ -12,7 +12,14 @@
|
||||
|
||||
## Testing
|
||||
|
||||
Use `./bin/test-agent` to run backend tests. See the top-level CLAUDE.md for usage.
|
||||
- **Run a test:** `./bin/mage run-tests namespace/test-name`
|
||||
- **Run all tests in a namespace:** `./bin/mage run-tests namespace`
|
||||
- **Run all tests for a module:** `./bin/mage run-tests test/metabase/notification` Because the module lives in that directory.
|
||||
|
||||
Note: the `./bin/mage run-tests` command accepts multiple args, so you can pass
|
||||
`./bin/mage run-tests namespace/test-name namespace/other-test namespace/third-test`
|
||||
to run 3 tests, or
|
||||
`./bin/mage run-tests test/metabase/module1 test/metabase/module2` to run 2 modules.
|
||||
|
||||
## Code Readability
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,357 +0,0 @@
|
||||
---
|
||||
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)
|
||||
```
|
||||
@@ -1,173 +0,0 @@
|
||||
---
|
||||
name: clojure-eval
|
||||
description: Evaluate Clojure code via nREPL using clj-nrepl-eval. Use this when you need to test code, check if edited files compile, verify function behavior, or interact with a running REPL session.
|
||||
---
|
||||
|
||||
# Clojure REPL Evaluation
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when you need to:
|
||||
- **Verify that edited Clojure files compile and load correctly**
|
||||
- Test function behavior interactively
|
||||
- Check the current state of the REPL
|
||||
- Debug code by evaluating expressions
|
||||
- Require or load namespaces for testing
|
||||
- Validate that code changes work before committing
|
||||
|
||||
## How It Works
|
||||
|
||||
The `clj-nrepl-eval` command evaluates Clojure code against an nREPL server. **Session state persists between evaluations**, so you can require a namespace in one evaluation and use it in subsequent calls. Each host:port combination maintains its own session file.
|
||||
|
||||
## Instructions
|
||||
|
||||
### 0. Discover and select nREPL server
|
||||
|
||||
First, discover what nREPL servers are running in the current directory:
|
||||
|
||||
```bash
|
||||
clj-nrepl-eval --discover-ports
|
||||
```
|
||||
|
||||
This will show all nREPL servers (Clojure, Babashka, shadow-cljs, etc.) running in the current project directory.
|
||||
|
||||
**Then use the AskUserQuestion tool:**
|
||||
|
||||
- **If ports are discovered:** Prompt user to select which nREPL port to use:
|
||||
- **question:** "Which nREPL port would you like to use?"
|
||||
- **header:** "nREPL Port"
|
||||
- **options:** Present each discovered port as an option with:
|
||||
- **label:** The port number
|
||||
- **description:** The server type and status (e.g., "Clojure nREPL server in current directory")
|
||||
- Include up to 4 discovered ports as options
|
||||
- The user can select "Other" to enter a custom port number
|
||||
|
||||
- **If no ports are discovered:** Prompt user how to start an nREPL server:
|
||||
- **question:** "No nREPL servers found. How would you like to start one?"
|
||||
- **header:** "Start nREPL"
|
||||
- **options:**
|
||||
- **label:** "deps.edn alias", **description:** "Find and use an nREPL alias in deps.edn"
|
||||
- **label:** "Leiningen", **description:** "Start nREPL using 'lein repl'"
|
||||
- The user can select "Other" for alternative methods or if they already have a server running on a specific port
|
||||
|
||||
IMPORTANT: IF you start a REPL do not supply a port let the nREPL start and return the port that it was started on.
|
||||
|
||||
### 1. Evaluate Clojure Code
|
||||
|
||||
> Evaluation automatically connects to the given port
|
||||
|
||||
Use the `-p` flag to specify the port and pass your Clojure code.
|
||||
|
||||
**Recommended: Pass code as a command-line argument:**
|
||||
```bash
|
||||
clj-nrepl-eval -p <PORT> "(+ 1 2 3)"
|
||||
```
|
||||
|
||||
**For multiple expressions (single line):**
|
||||
```bash
|
||||
clj-nrepl-eval -p <PORT> "(def x 10) (+ x 20)"
|
||||
```
|
||||
|
||||
**Alternative: Using heredoc (may require permission approval for multiline commands):**
|
||||
```bash
|
||||
clj-nrepl-eval -p <PORT> <<'EOF'
|
||||
(def x 10)
|
||||
(+ x 20)
|
||||
EOF
|
||||
```
|
||||
|
||||
**Alternative: Via stdin pipe:**
|
||||
```bash
|
||||
echo "(+ 1 2 3)" | clj-nrepl-eval -p <PORT>
|
||||
```
|
||||
|
||||
### 2. Display nREPL Sessions
|
||||
|
||||
**Discover all nREPL servers in current directory:**
|
||||
```bash
|
||||
clj-nrepl-eval --discover-ports
|
||||
```
|
||||
Shows all running nREPL servers in the current project directory, including their type (clj/bb/basilisp) and whether they match the current working directory.
|
||||
|
||||
**Check previously connected sessions:**
|
||||
```bash
|
||||
clj-nrepl-eval --connected-ports
|
||||
```
|
||||
Shows only connections you have made before (appears after first evaluation on a port).
|
||||
|
||||
### 3. Common Patterns
|
||||
|
||||
**Require a namespace (always use :reload to pick up changes):**
|
||||
```bash
|
||||
clj-nrepl-eval -p <PORT> "(require '[my.namespace :as ns] :reload)"
|
||||
```
|
||||
|
||||
**Test a function after requiring:**
|
||||
```bash
|
||||
clj-nrepl-eval -p <PORT> "(ns/my-function arg1 arg2)"
|
||||
```
|
||||
|
||||
**Check if a file compiles:**
|
||||
```bash
|
||||
clj-nrepl-eval -p <PORT> "(require 'my.namespace :reload)"
|
||||
```
|
||||
|
||||
**Multiple expressions:**
|
||||
```bash
|
||||
clj-nrepl-eval -p <PORT> "(def x 10) (* x 2) (+ x 5)"
|
||||
```
|
||||
|
||||
**Complex multiline code (using heredoc):**
|
||||
```bash
|
||||
clj-nrepl-eval -p <PORT> <<'EOF'
|
||||
(def x 10)
|
||||
(* x 2)
|
||||
(+ x 5)
|
||||
EOF
|
||||
```
|
||||
*Note: Heredoc syntax may require permission approval.*
|
||||
|
||||
**With custom timeout (in milliseconds):**
|
||||
```bash
|
||||
clj-nrepl-eval -p <PORT> --timeout 5000 "(long-running-fn)"
|
||||
```
|
||||
|
||||
**Reset the session (clears all state):**
|
||||
```bash
|
||||
clj-nrepl-eval -p <PORT> --reset-session
|
||||
clj-nrepl-eval -p <PORT> --reset-session "(def x 1)"
|
||||
```
|
||||
|
||||
## Available Options
|
||||
|
||||
- `-p, --port PORT` - nREPL port (required)
|
||||
- `-H, --host HOST` - nREPL host (default: 127.0.0.1)
|
||||
- `-t, --timeout MILLISECONDS` - Timeout (default: 120000 = 2 minutes)
|
||||
- `-r, --reset-session` - Reset the persistent nREPL session
|
||||
- `-c, --connected-ports` - List previously connected nREPL sessions
|
||||
- `-d, --discover-ports` - Discover nREPL servers in current directory
|
||||
- `-h, --help` - Show help message
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Prefer command-line arguments:** Pass code as quoted strings: `clj-nrepl-eval -p <PORT> "(+ 1 2 3)"` - works with existing permissions
|
||||
- **Heredoc for complex code:** Use heredoc (`<<'EOF' ... EOF`) for truly multiline code, but note it may require permission approval
|
||||
- **Sessions persist:** State (vars, namespaces, loaded libraries) persists across invocations until the nREPL server restarts or `--reset-session` is used
|
||||
- **Automatic delimiter repair:** The tool automatically repairs missing or mismatched parentheses
|
||||
- **Always use :reload:** When requiring namespaces, use `:reload` to pick up recent changes
|
||||
- **Default timeout:** 2 minutes (120000ms) - increase for long-running operations
|
||||
- **Input precedence:** Command-line arguments take precedence over stdin
|
||||
|
||||
## Typical Workflow
|
||||
|
||||
1. Discover nREPL servers: `clj-nrepl-eval --discover-ports`
|
||||
2. Use **AskUserQuestion** tool to prompt user to select a port
|
||||
3. Require namespace:
|
||||
```bash
|
||||
clj-nrepl-eval -p <PORT> "(require '[my.ns :as ns] :reload)"
|
||||
```
|
||||
4. Test function:
|
||||
```bash
|
||||
clj-nrepl-eval -p <PORT> "(ns/my-fn ...)"
|
||||
```
|
||||
5. Iterate: Make changes, re-require with `:reload`, test again
|
||||
@@ -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
|
||||
|
||||
@@ -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;`.
|
||||
@@ -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
|
||||
@@ -1,834 +0,0 @@
|
||||
---
|
||||
name: emotion-migrate
|
||||
description: Migrate Emotion styled-components to Mantine components with style props and CSS modules. Use when converting .styled.tsx files or removing @emotion imports from components.
|
||||
---
|
||||
|
||||
# Emotion → Mantine + CSS Modules Migration Skill
|
||||
|
||||
Migrate Emotion styled-components (`@emotion/styled`, `@emotion/react`) to Mantine layout components with style props and CSS modules. The goal is zero Emotion imports, zero inline styles, and maximum use of design system tokens.
|
||||
|
||||
## Priority Order (Strict)
|
||||
|
||||
1. **Mantine components + style props** — `Box`, `Flex`, `Stack`, `Group`, `Text`, `Title`, `Card`. This is the DEFAULT. Every CSS property must be checked against style props FIRST.
|
||||
2. **CSS modules** (`.module.css`) — ONLY for properties that Mantine style props genuinely cannot express: pseudo-selectors (`:hover`, `:focus`, `::before`), `box-shadow`, `border` shorthand, `animation`/`@keyframes`, complex selectors, `cursor`, `pointer-events`, `overflow`, `text-overflow`, `white-space`, `transition`.
|
||||
3. **Inline styles ONLY for dynamic values** — `style={{ }}` is allowed only for truly dynamic runtime values (e.g., computed widths, positions, data-driven colors). All static styles must use Mantine props or CSS modules.
|
||||
|
||||
### Mantine-First Decision Gate (CRITICAL)
|
||||
|
||||
For EACH styled component, go through every CSS property and ask: "Can this be a Mantine style prop?" If yes → style prop. If no → CSS module. Do NOT dump an entire component into a CSS module just because one property needs it — split them.
|
||||
|
||||
**Properties that ARE style props** (use these, not CSS modules):
|
||||
|
||||
- `display` → `display` prop
|
||||
- `color` → `c` prop (`c="brand"`, `c="text-primary"`)
|
||||
- `background-color` → `bg` prop (`bg="background-primary"`)
|
||||
- `font-size` → `fz` prop (`fz="md"`)
|
||||
- `font-weight` → `fw` prop (`fw="bold"`)
|
||||
- `line-height` → `lh` prop (`lh="md"`)
|
||||
- `text-align` → `ta` prop (`ta="center"`)
|
||||
- `padding` (all variants) → `p`, `px`, `py`, `pt`, `pb`, `pl`, `pr`
|
||||
- `margin` (all variants) → `m`, `mx`, `my`, `mt`, `mb`, `ml`, `mr`
|
||||
- `width` → `w`, `min-width` → `miw`, `max-width` → `maw`
|
||||
- `height` → `h`, `min-height` → `mih`, `max-height` → `mah`
|
||||
- `flex` → `flex` prop (`flex="0 0 auto"`, `flex={1}`)
|
||||
- `gap` → `gap` prop (on Flex/Stack/Group)
|
||||
- `align-items` → `align` prop (on Flex/Stack/Group)
|
||||
- `justify-content` → `justify` prop (on Flex/Stack/Group)
|
||||
- `flex-direction` → `direction` prop (on Flex)
|
||||
- `flex-wrap` → `wrap` prop (on Flex)
|
||||
- `position` → `pos` prop
|
||||
- `top/right/bottom/left` → `top`, `right`, `bottom`, `left` props
|
||||
- `opacity` → `opacity` prop
|
||||
|
||||
**Properties that NEED CSS modules** (no style prop equivalent):
|
||||
|
||||
- `:hover`, `:focus`, `:active`, `::before`, `::after` (pseudo-selectors)
|
||||
- `box-shadow`, `border` (shorthand with color), `outline`
|
||||
- `cursor`, `pointer-events`
|
||||
- `overflow`, `text-overflow`, `white-space`
|
||||
- `animation`, `transition`, `transform`
|
||||
- `@media` queries (UNLESS it's simple responsive spacing/sizing — then use responsive syntax: `p={{ base: "md", lg: "xl" }}`)
|
||||
|
||||
**Hybrid approach** — when a component needs BOTH, put style props on the Mantine component AND add a CSS module class for the rest:
|
||||
|
||||
```tsx
|
||||
<Flex
|
||||
className={S.root} /* for :hover, box-shadow, border */
|
||||
align="center" /* style prop */
|
||||
gap="sm" /* style prop */
|
||||
p="md" /* style prop */
|
||||
bg="background-primary" /* style prop */
|
||||
>
|
||||
```
|
||||
|
||||
## CSS Module Conventions (Strict)
|
||||
|
||||
### Class Naming: camelCase
|
||||
|
||||
All CSS module class names MUST use **camelCase**. This is the dominant convention across the codebase (~830 camelCase vs ~620 PascalCase classes), used consistently in Mantine UI components, and matches standard CSS module conventions.
|
||||
|
||||
```css
|
||||
/* CORRECT */
|
||||
.root {
|
||||
}
|
||||
.settingsSection {
|
||||
}
|
||||
.dragHandle {
|
||||
}
|
||||
.closeIcon {
|
||||
}
|
||||
|
||||
/* WRONG — do not use PascalCase or kebab-case */
|
||||
.ItemRoot {
|
||||
}
|
||||
.settings-section {
|
||||
}
|
||||
```
|
||||
|
||||
Modifier/state classes also use camelCase:
|
||||
|
||||
```css
|
||||
.selected {
|
||||
}
|
||||
.disabled {
|
||||
}
|
||||
.interactive {
|
||||
}
|
||||
.draggable {
|
||||
}
|
||||
```
|
||||
|
||||
### No Cascading — Direct Class Assignment
|
||||
|
||||
Cascading selectors are **discouraged**. Instead of styling through parent-child relationships, assign a class directly to the element that needs styling.
|
||||
|
||||
```css
|
||||
/* WRONG — cascading/descendant selectors */
|
||||
.root > input {
|
||||
}
|
||||
.root .label {
|
||||
}
|
||||
.container > div > span {
|
||||
}
|
||||
|
||||
/* CORRECT — direct class on the target element */
|
||||
.input {
|
||||
}
|
||||
.label {
|
||||
}
|
||||
.title {
|
||||
}
|
||||
```
|
||||
|
||||
The only acceptable nesting patterns are:
|
||||
|
||||
- **Pseudo-selectors on the same element**: `.item { &:hover { } }`
|
||||
- **Modifier composition**: `.item { &.selected { } }`
|
||||
- **Hover-reveal patterns** where a parent hover affects a child: `.root:hover .showOnHover { opacity: 1; }` — but only when structurally necessary (the child has no way to know about the parent's hover state)
|
||||
|
||||
### Import Alias
|
||||
|
||||
Always import the CSS module as `S`:
|
||||
|
||||
```tsx
|
||||
import S from "./ComponentName.module.css";
|
||||
```
|
||||
|
||||
## Step-by-Step Migration Process
|
||||
|
||||
### Step 1: Read and Understand
|
||||
|
||||
Read the `.styled.tsx` file AND every component that imports from it. Understand:
|
||||
|
||||
- Which styled components are used and where
|
||||
- Which props drive dynamic styles
|
||||
- Which styles are static vs conditional
|
||||
- Which styles can map directly to Mantine style props
|
||||
|
||||
### Step 2: Classify Each Styled Component
|
||||
|
||||
For each styled component, apply the Mantine-First Decision Gate above. Then determine the migration target:
|
||||
|
||||
| Emotion Pattern | Migration Target |
|
||||
| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `styled.div` with only layout/spacing/color | `Box`, `Flex`, `Stack`, or `Group` with style props. **NO CSS module needed.** |
|
||||
| `styled.div` with flexbox column | `Stack` component with style props |
|
||||
| `styled.div` with flexbox row | `Flex` or `Group` component with style props |
|
||||
| `styled.span` / `styled.p` with color/weight/size | **`Text component="span"` with style props** (`c`, `fw`, `fz`). NO CSS module. |
|
||||
| `styled.div` with hover/focus/pseudo-selectors | **Hybrid**: Mantine component with style props for expressible properties + CSS module class for pseudo-selectors only |
|
||||
| `styled.div` with media queries (simple spacing/sizing) | **Responsive style props**: `p={{ base: "md", lg: "xl" }}`. NO CSS module. |
|
||||
| `styled.div` with media queries (complex/non-spacing) | CSS module for the media query parts, style props for the rest |
|
||||
| `styled.div` with animations/keyframes | CSS module for animation, style props for layout |
|
||||
| `styled(SomeComponent)` with only color/spacing/flex | **Wrap in Mantine component** with style props: `<Box c="brand" flex="0 0 auto"><Icon /></Box>`, or pass style props if the component accepts them |
|
||||
| `styled(SomeComponent)` with pseudo-selectors/complex styles | CSS module `className` on the component |
|
||||
| Dynamic props `styled.div<{ isActive: boolean }>` | Mantine style props for simple toggles (`c={active ? "brand" : "text-primary"}`), `cx()` with CSS module classes for complex state combinations involving pseudo-selectors |
|
||||
|
||||
### Step 3: Create CSS Module (if needed)
|
||||
|
||||
Create `ComponentName.module.css` alongside the component file:
|
||||
|
||||
```css
|
||||
/* Use design system tokens — NEVER raw color/spacing values */
|
||||
.root {
|
||||
border: 1px solid var(--mb-color-border);
|
||||
border-radius: var(--mantine-radius-md);
|
||||
background-color: var(--mb-color-background-primary);
|
||||
}
|
||||
|
||||
/* Conditional states as separate classes, combined with cx() */
|
||||
.active {
|
||||
background-color: var(--mb-color-brand);
|
||||
color: var(--mb-color-text-primary-inverse);
|
||||
}
|
||||
|
||||
.disabled {
|
||||
color: var(--mb-color-text-tertiary);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Hover/focus/pseudo-selectors — nest with & */
|
||||
.interactive {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--mb-color-brand);
|
||||
background-color: var(--mb-color-background-hover);
|
||||
}
|
||||
}
|
||||
|
||||
/* Hover-reveal: acceptable parent→child nesting */
|
||||
.root:hover .showOnHover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Responsive styles */
|
||||
@media (--breakpoint-min-md) {
|
||||
.root {
|
||||
padding: var(--mantine-spacing-lg);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Update the Component TSX
|
||||
|
||||
```tsx
|
||||
import cx from "classnames";
|
||||
import CS from "metabase/css/core/index.css";
|
||||
import { Box, Flex, Group, Stack, Text } from "metabase/ui";
|
||||
import S from "./ComponentName.module.css";
|
||||
|
||||
// Dynamic props → cx() with conditional classes
|
||||
<Flex
|
||||
className={cx(S.root, {
|
||||
[S.active]: isActive,
|
||||
[S.disabled]: disabled,
|
||||
})}
|
||||
align="center"
|
||||
gap="sm"
|
||||
p="md"
|
||||
w="100%"
|
||||
>
|
||||
```
|
||||
|
||||
### Step 5: Delete the `.styled.tsx` File
|
||||
|
||||
Remove the old styled file entirely. Remove all imports of it from other files.
|
||||
|
||||
### Step 6: Verify
|
||||
|
||||
- Confirm zero `@emotion/styled` or `@emotion/react` imports remain in the migrated files
|
||||
- Confirm no static inline styles (dynamic runtime values are fine)
|
||||
- Confirm all colors use tokens, not raw hex/rgb values
|
||||
|
||||
## Design System Token Reference
|
||||
|
||||
### Mantine Style Props (use directly on components)
|
||||
|
||||
**Spacing** (`p`, `px`, `py`, `pt`, `pb`, `pl`, `pr`, `m`, `mx`, `my`, `mt`, `mb`, `ml`, `mr`):
|
||||
|
||||
- `"xs"` = 4px, `"sm"` = 8px, `"md"` = 16px, `"lg"` = 24px, `"xl"` = 32px
|
||||
- Custom: `rem(48)` for non-standard values (import `rem` from `metabase/ui`)
|
||||
|
||||
**Dimensions** (`w`, `h`, `maw`, `mah`, `miw`, `mih`):
|
||||
|
||||
- `"100%"`, `"100vh"`, `rem(400)`, etc.
|
||||
|
||||
**Colors** (`c`, `bg`):
|
||||
|
||||
- Text: `"text-primary"`, `"text-secondary"`, `"text-tertiary"`
|
||||
- Background: `"background-primary"`, `"background-secondary"`, `"background-hover"`
|
||||
- Brand: `"brand"`, `"error"`, `"success"`, `"warning"`
|
||||
|
||||
**Typography** (`fz`, `fw`, `lh`, `ta`, `ff`):
|
||||
|
||||
- Font size: `"xs"` = 11px, `"sm"` = 12px, `"md"` = 14px, `"lg"` = 17px, `"xl"` = 21px
|
||||
- Font weight: `"bold"`, `"normal"`, or numeric `700`
|
||||
- Line height: `"xs"` = 100%, `"sm"` = 115%, `"md"` = 122%, `"lg"` = 138%, `"xl"` = 150%
|
||||
- Text align: `"center"`, `"left"`, `"right"`
|
||||
|
||||
**Flexbox** (`align`, `justify`, `gap`, `direction`, `wrap`, `flex`):
|
||||
|
||||
- `align="center"`, `justify="space-between"`, `gap="sm"`, `direction="column"`, `wrap="nowrap"`
|
||||
- `flex={1}`, `flex="0 0 auto"`
|
||||
|
||||
**Other**: `pos` (position), `display`
|
||||
|
||||
**Responsive syntax**: `px={{ base: "md", md: "lg", lg: rem(48) }}`
|
||||
|
||||
### CSS Variable Tokens (use in `.module.css` files)
|
||||
|
||||
**Colors** — `var(--mb-color-<name>)`:
|
||||
|
||||
- `--mb-color-text-primary`, `--mb-color-text-secondary`, `--mb-color-text-tertiary`
|
||||
- `--mb-color-background-primary`, `--mb-color-background-secondary`, `--mb-color-background-hover`
|
||||
- `--mb-color-border`, `--mb-color-border-strong`, `--mb-color-border-subtle`
|
||||
- `--mb-color-brand`, `--mb-color-brand-hover`
|
||||
- `--mb-color-error`, `--mb-color-success`, `--mb-color-warning`
|
||||
- `--mb-color-shadow`, `--mb-color-focus`
|
||||
- `--mb-color-accent0` through `--mb-color-accent7`
|
||||
|
||||
**Mantine spacing** — `var(--mantine-spacing-<size>)`:
|
||||
|
||||
- `--mantine-spacing-xs` (4px), `--mantine-spacing-sm` (8px), `--mantine-spacing-md` (16px), `--mantine-spacing-lg` (24px), `--mantine-spacing-xl` (32px)
|
||||
|
||||
**Mantine radius** — `var(--mantine-radius-<size>)`:
|
||||
|
||||
- `--mantine-radius-xs` (4px), `--mantine-radius-sm` (6px), `--mantine-radius-md` (8px), `--mantine-radius-xl` (40px)
|
||||
|
||||
**Mantine font sizes** — `var(--mantine-font-size-<size>)`:
|
||||
|
||||
- `--mantine-font-size-xs` (11px), `--mantine-font-size-sm` (12px), `--mantine-font-size-md` (14px), `--mantine-font-size-lg` (17px), `--mantine-font-size-xl` (21px)
|
||||
|
||||
**Breakpoints** (in `.module.css` files):
|
||||
|
||||
- `@media (--breakpoint-min-sm)` (40em / 640px)
|
||||
- `@media (--breakpoint-min-md)` (60em / 960px)
|
||||
- `@media (--breakpoint-min-lg)` (80em / 1280px)
|
||||
- `@media (--breakpoint-min-xl)` (120em / 1920px)
|
||||
- Also available: `--breakpoint-max-*` variants
|
||||
|
||||
### Snap Hardcoded Literals to Nearest Design Token (CRITICAL)
|
||||
|
||||
When migrating, **never carry over hardcoded `rem`/`px` values** from the original Emotion code. Instead, snap them to the nearest design system token. The original values were often arbitrary — the migration is an opportunity to align with the design system.
|
||||
|
||||
**Spacing** — snap to the nearest Mantine spacing token:
|
||||
|
||||
| Hardcoded value | Nearest token | Style prop | CSS variable |
|
||||
| --------------------------------- | ------------- | ------------ | ------------------------------ |
|
||||
| `0.125rem` (2px) | `2px` | `2` (number) | `2px` (keep literal, no token) |
|
||||
| `0.2rem` (3.2px), `0.25rem` (4px) | **xs** (4px) | `"xs"` | `var(--mantine-spacing-xs)` |
|
||||
| `0.5rem` (8px) | **sm** (8px) | `"sm"` | `var(--mantine-spacing-sm)` |
|
||||
| `0.75rem` (12px) | between sm/md | `rem(12)` | `0.75rem` (no exact token) |
|
||||
| `1rem` (16px) | **md** (16px) | `"md"` | `var(--mantine-spacing-md)` |
|
||||
| `1.5rem` (24px) | **lg** (24px) | `"lg"` | `var(--mantine-spacing-lg)` |
|
||||
| `2rem` (32px) | **xl** (32px) | `"xl"` | `var(--mantine-spacing-xl)` |
|
||||
|
||||
**Border radius** — snap to the nearest Mantine radius token:
|
||||
|
||||
| Hardcoded value | Nearest token | CSS variable |
|
||||
| ---------------- | ----------------------------- | -------------------------- |
|
||||
| `0.25rem` (4px) | **xs** (4px) | `var(--mantine-radius-xs)` |
|
||||
| `0.375rem` (6px) | **sm** (6px) | `var(--mantine-radius-sm)` |
|
||||
| `0.5rem` (8px) | **md** (8px) | `var(--mantine-radius-md)` |
|
||||
| `1rem`+ (16px+) | **xl** (40px) or keep literal | `var(--mantine-radius-xl)` |
|
||||
|
||||
**Font sizes** — snap to the nearest Mantine font size token:
|
||||
|
||||
| Hardcoded value | Nearest token | Style prop | CSS variable |
|
||||
| -------------------------------- | ------------- | ---------- | ----------------------------- |
|
||||
| `0.6875rem` (11px) | **xs** (11px) | `fz="xs"` | `var(--mantine-font-size-xs)` |
|
||||
| `0.75rem` (12px) | **sm** (12px) | `fz="sm"` | `var(--mantine-font-size-sm)` |
|
||||
| `0.875rem` (14px), `1rem` (16px) | **md** (14px) | `fz="md"` | `var(--mantine-font-size-md)` |
|
||||
| `1.063rem` (17px) | **lg** (17px) | `fz="lg"` | `var(--mantine-font-size-lg)` |
|
||||
| `1.3rem` (21px) | **xl** (21px) | `fz="xl"` | `var(--mantine-font-size-xl)` |
|
||||
|
||||
**Rules:**
|
||||
|
||||
- If the hardcoded value is within ~2px of a token, use the token
|
||||
- If it falls exactly between two tokens, prefer the smaller one (tighter is safer)
|
||||
- If no token is close (e.g., `48px` spacing), use `rem(48)` for style props or the literal value in CSS modules
|
||||
- This applies everywhere: style props, CSS module values, Icon `size` props, etc.
|
||||
|
||||
## Common Migration Patterns
|
||||
|
||||
### Pattern 1: Static Layout Container → Mantine Component
|
||||
|
||||
**Before:**
|
||||
|
||||
```tsx
|
||||
// Component.styled.tsx
|
||||
export const Container = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
`;
|
||||
```
|
||||
|
||||
**After:**
|
||||
|
||||
```tsx
|
||||
<Flex align="center" gap="sm" p="md" w="100%">
|
||||
```
|
||||
|
||||
### Pattern 2: Vertical Stack → Stack Component
|
||||
|
||||
**Before:**
|
||||
|
||||
```tsx
|
||||
export const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
padding: 2rem;
|
||||
`;
|
||||
```
|
||||
|
||||
**After:**
|
||||
|
||||
```tsx
|
||||
<Stack gap="lg" p="xl">
|
||||
```
|
||||
|
||||
### Pattern 3: Dynamic Props → cx() with CSS Module Classes
|
||||
|
||||
**Before:**
|
||||
|
||||
```tsx
|
||||
export const Item = styled.div<{ isSelected: boolean; disabled: boolean }>`
|
||||
color: ${(props) =>
|
||||
props.disabled ? color("text-tertiary") : color("text-primary")};
|
||||
background-color: ${(props) =>
|
||||
props.isSelected ? color("brand") : "transparent"};
|
||||
cursor: ${(props) => (props.disabled ? "default" : "pointer")};
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => !props.disabled && color("brand")};
|
||||
}
|
||||
`;
|
||||
```
|
||||
|
||||
**After (CSS module):**
|
||||
|
||||
```css
|
||||
/* Item.module.css */
|
||||
.itemRoot {
|
||||
color: var(--mb-color-text-primary);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--mb-color-brand);
|
||||
}
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: var(--mb-color-brand);
|
||||
}
|
||||
|
||||
.disabled {
|
||||
color: var(--mb-color-text-tertiary);
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
color: var(--mb-color-text-tertiary);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**After (TSX):**
|
||||
|
||||
```tsx
|
||||
<Box
|
||||
className={cx(S.itemRoot, {
|
||||
[S.selected]: isSelected,
|
||||
[S.disabled]: disabled,
|
||||
})}
|
||||
>
|
||||
```
|
||||
|
||||
### Pattern 4: Extending a Component → Style Props First, CSS Module Only If Needed
|
||||
|
||||
When a styled component wraps another component, first check if the styles can be expressed as style props on a Mantine wrapper or on the component itself. Only use a CSS module when properties genuinely need it.
|
||||
|
||||
**Before:**
|
||||
|
||||
```tsx
|
||||
export const CardIcon = styled(Icon)`
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
color: var(--mb-color-brand);
|
||||
`;
|
||||
|
||||
export const CardTitle = styled(Ellipsified)`
|
||||
color: var(--mb-color-text-primary);
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
margin-left: 1rem;
|
||||
`;
|
||||
```
|
||||
|
||||
**After — style props on Mantine wrapper (preferred when ALL properties are expressible):**
|
||||
|
||||
```tsx
|
||||
<Box display="block" flex="0 0 auto" c="brand">
|
||||
<Icon {...icon} />
|
||||
</Box>
|
||||
<Box c="text-primary" fz="md" fw="bold" ml="md">
|
||||
<Ellipsified>{title}</Ellipsified>
|
||||
</Box>
|
||||
```
|
||||
|
||||
**After — CSS module (use only when component has pseudo-selectors or non-expressible styles):**
|
||||
|
||||
```css
|
||||
.icon {
|
||||
color: var(--mb-color-brand);
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--mb-color-brand-hover);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
<Icon className={S.icon} name="warning" />
|
||||
```
|
||||
|
||||
### Pattern 4b: `styled.span` / `styled.p` with Text Styles → `Text` Component
|
||||
|
||||
**Before:**
|
||||
|
||||
```tsx
|
||||
export const Label = styled.span`
|
||||
color: var(--mb-color-text-secondary);
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
export const Title = styled.span`
|
||||
color: var(--mb-color-text-primary);
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
margin-left: 0.5rem;
|
||||
`;
|
||||
```
|
||||
|
||||
**After — `Text` component with style props (NO CSS module needed):**
|
||||
|
||||
```tsx
|
||||
<Text component="span" c="text-secondary" fw="bold">
|
||||
{label}
|
||||
</Text>
|
||||
|
||||
<Text component="span" c="text-primary" fz="md" fw="bold" ml="sm">
|
||||
{title}
|
||||
</Text>
|
||||
```
|
||||
|
||||
`Text` renders as `<p>` by default. Use `component="span"` to render inline. All color, typography, and spacing props work directly — no CSS module needed for pure text styling.
|
||||
|
||||
### Pattern 5: Keyframes Animation → CSS Module
|
||||
|
||||
**Before:**
|
||||
|
||||
```tsx
|
||||
const spin = keyframes`
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
`;
|
||||
|
||||
export const Spinner = styled.div`
|
||||
animation: ${spin} 1s infinite linear;
|
||||
`;
|
||||
```
|
||||
|
||||
**After (CSS module):**
|
||||
|
||||
```css
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.spinner {
|
||||
animation: spin 1s infinite linear;
|
||||
}
|
||||
```
|
||||
|
||||
**After (TSX):**
|
||||
|
||||
```tsx
|
||||
<Box className={S.spinner} />
|
||||
```
|
||||
|
||||
### Pattern 6: color() Function → CSS Variables
|
||||
|
||||
| Emotion (JS) | CSS Module | Mantine Style Prop |
|
||||
| ----------------------- | ------------------------------------------------------------ | --------------------------- |
|
||||
| `color("brand")` | `var(--mb-color-brand)` | `c="brand"` or `bg="brand"` |
|
||||
| `color("text-primary")` | `var(--mb-color-text-primary)` | `c="text-primary"` |
|
||||
| `color("border")` | `var(--mb-color-border)` | N/A (use CSS module) |
|
||||
| `alpha("brand", 0.2)` | `color-mix(in srgb, var(--mb-color-brand), transparent 80%)` | N/A (use CSS module) |
|
||||
|
||||
### Pattern 7: Responsive Spacing/Sizing → Responsive Style Props (NOT CSS Media Queries)
|
||||
|
||||
When a styled component only changes spacing, sizing, or other style-prop-expressible values at breakpoints, use Mantine's responsive syntax instead of CSS module media queries.
|
||||
|
||||
**Before:**
|
||||
|
||||
```tsx
|
||||
export const CaptionRoot = styled.div`
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
${breakpointMinExtraLarge} {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
`;
|
||||
```
|
||||
|
||||
**After — responsive style prop (NO CSS module needed):**
|
||||
|
||||
```tsx
|
||||
<Flex mb={{ base: "lg", xl: "xl" }}>
|
||||
```
|
||||
|
||||
Breakpoint keys: `base` (default), `xs` (40em), `sm` (48em), `md` (60em), `lg` (80em), `xl` (120em).
|
||||
|
||||
Only use CSS module `@media` queries when the responsive change involves non-style-prop properties (e.g., `box-shadow`, `border`, `cursor` changes at breakpoints).
|
||||
|
||||
### Pattern 8: Dynamic Computed Styles → Inline Styles
|
||||
|
||||
Inline styles are allowed **only** for truly dynamic values computed at runtime (e.g., widths, positions, colors from data). Everything else must use Mantine style props or CSS modules.
|
||||
|
||||
**Before:**
|
||||
|
||||
```tsx
|
||||
export const Bar = styled.div<{ width: number }>`
|
||||
width: ${(props) => props.width}%;
|
||||
`;
|
||||
```
|
||||
|
||||
**After:**
|
||||
|
||||
```tsx
|
||||
<Box style={{ width: `${width}%` }} />
|
||||
```
|
||||
|
||||
### Pattern 9: Core CSS Utility Classes (Discouraged)
|
||||
|
||||
**Do NOT introduce new `CS` utility class usage.** Core CSS utilities (`CS` from `metabase/css/core/index.css`) are legacy and discouraged for new code. Prefer Mantine style props or CSS modules instead.
|
||||
|
||||
If existing code already uses `CS` classes and you're not migrating that specific code, leave them alone. But when migrating Emotion → Mantine, replace with the proper alternative:
|
||||
|
||||
| Instead of `CS.*` | Use |
|
||||
| ------------------- | ------------------------------------------------------- |
|
||||
| `CS.overflowHidden` | CSS module: `overflow: hidden` |
|
||||
| `CS.cursorPointer` | CSS module: `cursor: pointer` |
|
||||
| `CS.flex1` | Style prop: `flex={1}` |
|
||||
| `CS.flexNoShrink` | Style prop: `flex="0 0 auto"` |
|
||||
| `CS.textBrandHover` | CSS module: `&:hover { color: var(--mb-color-brand); }` |
|
||||
|
||||
### Pattern 10: Shared Emotion Styles Across Files → Shared CSS Module + cx()
|
||||
|
||||
When Emotion exports a shared `css` block (like `animationStyles`) imported by many files, create ONE CSS module with the shared keyframe and a reusable class, then compose via `cx()`.
|
||||
|
||||
**Before (Emotion — shared animation used by 13+ skeleton files):**
|
||||
|
||||
```tsx
|
||||
// ChartSkeleton.styled.tsx — defines shared animation
|
||||
const fadingKeyframes = keyframes`
|
||||
0% { opacity: 0.0625; }
|
||||
50% { opacity: 0.125; }
|
||||
100% { opacity: 0.0625; }
|
||||
`;
|
||||
export const animationStyles = css`
|
||||
opacity: 0.1;
|
||||
animation: ${fadingKeyframes} 1.5s infinite;
|
||||
`;
|
||||
|
||||
// AreaSkeleton.styled.tsx — consumes it
|
||||
import { animationStyles } from ".../ChartSkeleton.styled";
|
||||
export const SkeletonImage = styled.svg`
|
||||
${animationStyles};
|
||||
flex: 1 1 0;
|
||||
margin-top: 1rem;
|
||||
border-bottom: 1px solid currentColor;
|
||||
`;
|
||||
|
||||
// BarSkeleton.styled.tsx — also consumes it
|
||||
import { animationStyles } from ".../ChartSkeleton.styled";
|
||||
export const SkeletonImage = styled.svg`
|
||||
${animationStyles};
|
||||
flex: 1 1 0;
|
||||
margin-top: 1rem;
|
||||
`;
|
||||
```
|
||||
|
||||
**After — shared CSS module:**
|
||||
|
||||
```css
|
||||
/* ChartSkeleton.module.css */
|
||||
@keyframes fading {
|
||||
0% {
|
||||
opacity: 0.0625;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.125;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0.0625;
|
||||
}
|
||||
}
|
||||
|
||||
.animated {
|
||||
opacity: 0.1;
|
||||
animation: fading 1.5s infinite;
|
||||
}
|
||||
```
|
||||
|
||||
**After — per-component CSS modules with own styles only:**
|
||||
|
||||
```css
|
||||
/* AreaSkeleton.module.css */
|
||||
.skeletonImage {
|
||||
flex: 1 1 0;
|
||||
margin-top: 1rem;
|
||||
border-bottom: 1px solid currentColor;
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
/* BarSkeleton.module.css */
|
||||
.skeletonImage {
|
||||
flex: 1 1 0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
```
|
||||
|
||||
**After — component TSX uses cx() to compose:**
|
||||
|
||||
```tsx
|
||||
// AreaSkeleton.tsx
|
||||
import cx from "classnames";
|
||||
import ChartSkeletonS from "../ChartSkeleton/ChartSkeleton.module.css";
|
||||
import S from "./AreaSkeleton.module.css";
|
||||
|
||||
const AreaSkeleton = (): JSX.Element => {
|
||||
return (
|
||||
<svg
|
||||
className={cx(ChartSkeletonS.animated, S.skeletonImage)}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 371 113"
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
<path d="..." fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
The shared module owns the `@keyframes` and the `.animated` class. Each consumer composes it with its own module class via `cx()`. No duplication of the keyframe definition.
|
||||
|
||||
## Import Template
|
||||
|
||||
```tsx
|
||||
import cx from "classnames"; // only if needed for conditional classes
|
||||
|
||||
import { Box, Flex, Group, Stack, Text, rem } from "metabase/ui";
|
||||
|
||||
import S from "./ComponentName.module.css"; // only if CSS module is needed
|
||||
```
|
||||
|
||||
## Checklist Before Finishing
|
||||
|
||||
- [ ] **MANTINE-FIRST CHECK**: Every CSS property that has a style prop equivalent (`c`, `bg`, `p`, `m`, `fw`, `fz`, `flex`, `gap`, `align`, `w`, `h`, etc.) is expressed as a style prop, NOT in a CSS module
|
||||
- [ ] `styled.span`/`styled.p` with only color/weight/size are replaced with `Text component="span"` + style props, NOT CSS module classes
|
||||
- [ ] `styled.div` with only layout props are replaced with `Flex`/`Stack`/`Group`/`Box` + style props, NOT CSS module classes
|
||||
- [ ] Responsive spacing/sizing uses responsive style props (`mb={{ base: "lg", xl: "xl" }}`), NOT CSS module `@media` queries
|
||||
- [ ] CSS modules are used ONLY for: pseudo-selectors, box-shadow, border, cursor, overflow, animation, transition, transform, or complex selectors
|
||||
- [ ] All `@emotion/styled` and `@emotion/react` imports removed from migrated files
|
||||
- [ ] The `.styled.tsx` file is deleted
|
||||
- [ ] All imports of the deleted styled file are updated
|
||||
- [ ] No static inline styles — `style={{ }}` used only for truly dynamic runtime values
|
||||
- [ ] All colors use design tokens (`c="brand"`, `var(--mb-color-brand)`), not raw hex/rgb
|
||||
- [ ] All spacing uses design tokens (`p="md"`, `var(--mantine-spacing-md)`), not arbitrary px values — hardcoded `rem`/`px` literals snapped to the nearest token (see mapping table above)
|
||||
- [ ] All border-radius values use radius tokens (`var(--mantine-radius-md)`), not hardcoded `0.5rem`
|
||||
- [ ] All font sizes use font-size tokens (`fz="md"`, `var(--mantine-font-size-md)`), not hardcoded `1rem`
|
||||
- [ ] Layout uses Mantine components (`Flex`, `Stack`, `Group`, `Box`, `Text`) not raw divs/spans with CSS
|
||||
- [ ] All CSS module class names use camelCase
|
||||
- [ ] No cascading/descendant selectors targeting raw elements — use direct class assignment instead
|
||||
- [ ] Component renders identically to the original (visually verify if possible)
|
||||
|
||||
## Visual Verification with Screenshots (Optional — Only When Explicitly Asked)
|
||||
|
||||
When the user explicitly asks to create before/after screenshots, use the Playwright MCP tools to capture them. This is NOT done by default — only when requested.
|
||||
|
||||
Screenshots must be taken in **both light and dark mode** to verify token usage is correct in both themes.
|
||||
|
||||
### Switching Color Scheme via API
|
||||
|
||||
Toggle between light and dark mode using the Metabase settings API. Use Playwright's `browser_evaluate` or `browser_navigate` with fetch:
|
||||
|
||||
```js
|
||||
// Switch to dark mode
|
||||
await fetch("/api/setting/color-scheme", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ value: "dark" }),
|
||||
});
|
||||
|
||||
// Switch to light mode
|
||||
await fetch("/api/setting/color-scheme", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ value: "light" }),
|
||||
});
|
||||
```
|
||||
|
||||
After switching, reload the page for the theme to take effect.
|
||||
|
||||
### Process
|
||||
|
||||
For each state (**after** = migrated code, **before** = stashed original), capture screenshots in both themes at both viewport widths. This produces **8 screenshots total**.
|
||||
|
||||
1. **Take "after" screenshots first** (migrated code is already in place):
|
||||
|
||||
- Navigate to the page that renders the migrated components (`browser_navigate`)
|
||||
- Wait for content to load (`browser_wait_for` with key text, or `textGone: "Loading..."`)
|
||||
- Close the sidebar if it overlaps content on narrow viewports (`browser_click` the toggle button)
|
||||
- **Light mode** (set `color-scheme` to `"light"` via API, reload):
|
||||
- Desktop (1280x900): `after-light-desktop.png`
|
||||
- Narrow (640x900), close sidebar: `after-light-narrow.png`
|
||||
- **Dark mode** (set `color-scheme` to `"dark"` via API, reload):
|
||||
- Desktop (1280x900): `after-dark-desktop.png`
|
||||
- Narrow (640x900), close sidebar: `after-dark-narrow.png`
|
||||
|
||||
2. **Stash changes to capture "before" screenshots**:
|
||||
|
||||
- Run `git stash --include-untracked` to temporarily revert to the Emotion version
|
||||
- Repeat the same 4 screenshots with `before-` prefix:
|
||||
- `before-light-desktop.png`, `before-light-narrow.png`
|
||||
- `before-dark-desktop.png`, `before-dark-narrow.png`
|
||||
- Run `git stash pop` to restore the migrated code
|
||||
|
||||
3. **Restore the user's original color scheme** (set back to `"auto"` or whatever it was before).
|
||||
|
||||
4. **Display all 8 screenshots** using the `Read` tool so the user can visually compare before/after in both themes at both viewport sizes.
|
||||
|
||||
### Key Details
|
||||
|
||||
- Save screenshots in the repo root (they will be untracked — user can delete after review)
|
||||
- The dev server must be running (typically `http://localhost:3000`)
|
||||
- Use `browser_snapshot` to find element refs when you need to click buttons (e.g., sidebar toggle)
|
||||
- The greeting text on the home page changes randomly on each load — this is expected, not a regression
|
||||
- If the page being tested is not the home page, navigate to the correct URL that exercises the migrated components
|
||||
- Dark mode screenshots are critical for verifying that CSS variable tokens (`var(--mb-color-*)`) are used correctly — hardcoded colors will look wrong in dark mode
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -1,7 +1,7 @@
|
||||
;; -*- comment-column: 100; -*-
|
||||
{:config-paths ["macros" "src" "config"]
|
||||
:linters
|
||||
{;;
|
||||
{ ;;
|
||||
;; enabled optional linters (these are disabled by default)
|
||||
;;
|
||||
|
||||
@@ -34,10 +34,6 @@
|
||||
:use {:level :warning}
|
||||
:warn-on-reflection {:level :warning}
|
||||
|
||||
;; release flag linters
|
||||
:metabase/unknown-release-flag {:level :warning}
|
||||
:metabase/non-literal-release-flag {:level :warning}
|
||||
|
||||
;;
|
||||
;; other disabled-by-default linters it would be nice to enable soon
|
||||
;;
|
||||
@@ -54,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}
|
||||
@@ -73,6 +67,7 @@
|
||||
:metabase/namespace-name {:level :warning}
|
||||
:metabase/no-jsqlparser-imports {:level :warning}
|
||||
:metabase/require-shape-checker {:level :warning}
|
||||
:metabase/test-helpers-use-non-thread-safe-functions {:level :warning} ; more config in `.clj-kondo/config/test-helpers/config.edn`
|
||||
:metabase/tests-must-live-in-known-modules {:level :warning}
|
||||
:metabase/validate-defendpoint-route-uses-kebab-case {:level :warning}
|
||||
:metabase/validate-defendpoint-query-params-use-kebab-case {:level :warning}
|
||||
@@ -180,17 +175,10 @@
|
||||
clojure.core.async/to-chan!
|
||||
clojure.core.async/to-chan!!
|
||||
metabase.channel.core/send!
|
||||
metabase.warehouses.models.database/assert-not-h2!
|
||||
metabase.driver.sql-jdbc.execute/execute-prepared-statement!
|
||||
metabase.notification.models/create-notification!
|
||||
metabase.oauth-server.core/reset-provider!
|
||||
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!
|
||||
@@ -204,7 +192,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
|
||||
@@ -232,10 +220,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]}}
|
||||
|
||||
@@ -254,17 +239,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 [<>])
|
||||
@@ -316,13 +297,11 @@
|
||||
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"}
|
||||
macaw.core/parsed-query {:message "Use metabase.driver.util/parsed-query instead of macaw.core/parsed-query"}}
|
||||
|
||||
:discouraged-namespace
|
||||
{camel-snake-kebab.core {:message "CSK is not Turkish-safe, use the versions in metabase.util instead."}
|
||||
@@ -427,8 +406,6 @@
|
||||
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
|
||||
@@ -443,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
|
||||
@@ -534,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
|
||||
@@ -557,7 +526,6 @@
|
||||
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.metabot.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
|
||||
@@ -691,11 +659,6 @@
|
||||
metabase.util.malli.registry/def hooks.metabase.util.malli.registry/def
|
||||
metabase.util/case-enum hooks.metabase.util/case-enum
|
||||
metabase.util/format-color hooks.metabase.util/format-color
|
||||
metabase.release-flags.core/has-release-flag? hooks.metabase.release-flags.models/has-release-flag?
|
||||
metabase.release-flags.core/guard-namespace! hooks.metabase.release-flags.models/guard-namespace!
|
||||
metabase.release-flags.guard/bypass-guard-fixture hooks.metabase.release-flags.models/bypass-guard-fixture
|
||||
metabase.release-flags.guard/guard-namespace! hooks.metabase.release-flags.models/guard-namespace!
|
||||
metabase.release-flags.models/has-release-flag? hooks.metabase.release-flags.models/has-release-flag?
|
||||
metabase.xrays.api.automagic-dashboards-test/with-indexed-model! hooks.metabase.xrays.api.automagic-dashboards-test/with-indexed-model!}
|
||||
|
||||
:macroexpand
|
||||
@@ -799,17 +762,22 @@
|
||||
: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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
||||
;; Source of truth for release flags.
|
||||
;; Auto-loaded by clj-kondo via :config-paths, and read by metabase.release-flags.init at startup.
|
||||
;; See docs/developers-guide/release-flags.md for usage.
|
||||
{:metabase/release-flags
|
||||
{"joke-of-the-day" {:description "Whether to show the joke of the day on the homepage."
|
||||
:start-date "2026-02-24"}}}
|
||||
@@ -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!
|
||||
@@ -110,7 +110,6 @@
|
||||
metabase.settings.models.setting-test/test-user-local-allowed-setting!
|
||||
metabase.settings.models.setting-test/test-user-local-only-setting!
|
||||
metabase.settings.models.setting.cache/restore-cache!
|
||||
metabase.settings.models.setting/log-deprecated-env-var-usage!
|
||||
metabase.settings.models.setting/set!
|
||||
metabase.settings.models.setting/validate-settings-formatting!
|
||||
metabase.setup.core/create-token!
|
||||
|
||||
@@ -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}))))
|
||||
@@ -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}))))
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -75,15 +75,6 @@
|
||||
(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"))
|
||||
|
||||
(defn usage-error
|
||||
"Find usage errors when a `required-namespace` is required in the `current-module`. Returns a string describing the
|
||||
error type if there is one, otherwise `nil` if there are no errors."
|
||||
@@ -102,15 +93,4 @@
|
||||
(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)))))
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
(ns hooks.metabase.release-flags.models
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as api]
|
||||
[clojure.string :as str]))
|
||||
|
||||
(defn- keyword->flag-name
|
||||
"Converts a keyword to its full string representation, including namespace if present.
|
||||
:foo => \"foo\", :ns/foo => \"ns/foo\""
|
||||
[kw]
|
||||
(if (namespace kw)
|
||||
(str (namespace kw) "/" (name kw))
|
||||
(name kw)))
|
||||
|
||||
(defn- check-flag-name
|
||||
"Checks if a flag name is in the valid set and registers a finding if not."
|
||||
[valid-flags flag-name flag-node]
|
||||
(when (and (seq valid-flags) (not (contains? valid-flags flag-name)))
|
||||
(api/reg-finding! (assoc (meta flag-node)
|
||||
:message (str "Unknown release flag: " flag-name
|
||||
". Valid flags: " (str/join ", " (sort valid-flags)))
|
||||
:type :metabase/unknown-release-flag))))
|
||||
|
||||
(defn- check-flag-arg
|
||||
"Shared lint check for a flag argument node. Warns if not a literal keyword/string
|
||||
or if the flag is not in release-flags.json."
|
||||
[valid-flags flag-node]
|
||||
(cond
|
||||
(api/keyword-node? flag-node)
|
||||
(check-flag-name valid-flags (keyword->flag-name (api/sexpr flag-node)) flag-node)
|
||||
|
||||
(api/string-node? flag-node)
|
||||
(check-flag-name valid-flags (api/sexpr flag-node) flag-node)
|
||||
|
||||
:else
|
||||
(api/reg-finding! (assoc (meta flag-node)
|
||||
:message "Release flag should be a literal keyword or string, not a variable."
|
||||
:type :metabase/non-literal-release-flag))))
|
||||
|
||||
(defn- get-valid-flags
|
||||
"Reads the valid release flag names from the kondo config.
|
||||
Loaded from .clj-kondo/config/release-flags/config.edn via :config-paths."
|
||||
[config]
|
||||
(set (keys (get config :metabase/release-flags {}))))
|
||||
|
||||
(defn has-release-flag?
|
||||
"Lint hook for metabase.release-flags.models/has-release-flag?."
|
||||
[{:keys [node config]}]
|
||||
(let [[_ flag-node] (:children node)]
|
||||
(check-flag-arg (get-valid-flags config) flag-node)))
|
||||
|
||||
(defn guard-namespace!
|
||||
"Lint hook for metabase.release-flags.guard/guard-namespace!."
|
||||
[{:keys [node config]}]
|
||||
(let [[_ flag-node] (:children node)]
|
||||
(check-flag-arg (get-valid-flags config) flag-node)))
|
||||
|
||||
(defn bypass-guard-fixture
|
||||
"Lint hook for metabase.release-flags.guard/bypass-guard-fixture."
|
||||
[{:keys [node config]}]
|
||||
(let [valid-flags (get-valid-flags config)]
|
||||
(doseq [flag-node (rest (:children node))]
|
||||
(check-flag-arg valid-flags flag-node))))
|
||||
@@ -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
|
||||
|
||||
@@ -90,7 +90,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 +119,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 +133,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 +154,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]]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -100,7 +100,7 @@ METASTORE_DEV_SERVER_URL="https://token-check.staging.metabase.com"
|
||||
#
|
||||
# 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).
|
||||
# backend on your own, or using `yarn test-cypress` (local test runner).
|
||||
#
|
||||
# You don't _have to_ export any of these vars (except for the tokens below).
|
||||
# Things will still work.
|
||||
@@ -118,9 +118,6 @@ MB_EDITION="ee"
|
||||
# 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
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
6
.eslintignore
Normal file
6
.eslintignore
Normal file
@@ -0,0 +1,6 @@
|
||||
frontend/src/cljs
|
||||
frontend/src/cljs_release
|
||||
e2e/support/cypress_sample_database.js
|
||||
e2e/support/cypress_sample_instance_data.js
|
||||
frontend/src/metabase-types/openapi/types.gen.ts
|
||||
e2e/embedding-sdk-host-apps
|
||||
356
.eslintrc.js
Normal file
356
.eslintrc.js
Normal file
@@ -0,0 +1,356 @@
|
||||
/* eslint-disable import/no-commonjs */
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
const path = require("path");
|
||||
|
||||
// `postcss-modules` lints css modules class names, but it currently crashes
|
||||
// eslint on vscode. If you use webstorm or want to run the lint for the cli, you
|
||||
// can use this flag to enable it. This is set to true in CI
|
||||
const shouldLintCssModules =
|
||||
process.env.LINT_CSS_MODULES === "true" || process.env.CI;
|
||||
const plugins = ["react", "no-only-tests", "ttag", "i18next"];
|
||||
if (shouldLintCssModules) {
|
||||
plugins.push("postcss-modules");
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ignorePatterns: ["!.storybook"],
|
||||
rules: {
|
||||
strict: [2, "never"],
|
||||
"no-undef": 2,
|
||||
"no-var": 1,
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
vars: "all",
|
||||
args: "none",
|
||||
varsIgnorePattern: "^_.+$",
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
"no-empty": [1, { allowEmptyCatch: true }],
|
||||
// Note: adding this rule to a eslint config file in a subfolder will remove
|
||||
// *not* carry over the restricted imports from parent folders, you will
|
||||
// need to copy them over
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: "moment",
|
||||
message: "Moment is deprecated, please use dayjs",
|
||||
},
|
||||
{
|
||||
name: "moment-timezone",
|
||||
message: "Moment is deprecated, please use dayjs",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
curly: [1, "all"],
|
||||
eqeqeq: [1, "smart"],
|
||||
"import/no-duplicates": ["warn", { considerQueryString: true }],
|
||||
"import/no-default-export": 2,
|
||||
"import/no-named-as-default": 0,
|
||||
"import/no-commonjs": 1,
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"newlines-between": "always",
|
||||
alphabetize: {
|
||||
order: "asc",
|
||||
orderImportKind: "asc",
|
||||
caseInsensitive: false,
|
||||
},
|
||||
groups: [
|
||||
"builtin",
|
||||
"external",
|
||||
"internal",
|
||||
"parent",
|
||||
"sibling",
|
||||
"index",
|
||||
],
|
||||
warnOnUnassignedImports: false,
|
||||
},
|
||||
],
|
||||
"sort-imports": [
|
||||
"error",
|
||||
{
|
||||
// allows this rule to work with import/order
|
||||
ignoreDeclarationSort: true,
|
||||
},
|
||||
],
|
||||
"no-console": [2, { allow: ["warn", "error", "errorBuffer"] }],
|
||||
"react/no-is-mounted": 2,
|
||||
"react/prefer-es6-class": 2,
|
||||
"react/display-name": 1,
|
||||
"react/prop-types": 2,
|
||||
"react/no-did-mount-set-state": 0,
|
||||
"react/no-did-update-set-state": 0,
|
||||
"react/no-find-dom-node": 0,
|
||||
"react/no-children-prop": 2,
|
||||
"react/no-string-refs": 2,
|
||||
"react/no-unescaped-entities": 2,
|
||||
"react/jsx-no-target-blank": 2,
|
||||
"react/jsx-key": 2,
|
||||
"react/forbid-component-props": [2, { forbid: ["sx"] }],
|
||||
"react-hooks/exhaustive-deps": ["warn"],
|
||||
"prefer-const": [1, { destructuring: "all" }],
|
||||
"no-restricted-globals": ["error", "close"],
|
||||
"no-useless-escape": 0,
|
||||
"no-only-tests/no-only-tests": [
|
||||
"error",
|
||||
{
|
||||
block: [
|
||||
"describe",
|
||||
"it",
|
||||
"context",
|
||||
"test",
|
||||
"tape",
|
||||
"fixture",
|
||||
"serial",
|
||||
"Feature",
|
||||
"Scenario",
|
||||
"Given",
|
||||
"And",
|
||||
"When",
|
||||
"Then",
|
||||
],
|
||||
},
|
||||
],
|
||||
complexity: ["error", { max: 55 }],
|
||||
...(shouldLintCssModules
|
||||
? {
|
||||
"postcss-modules/no-undef-class": "error",
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
globals: {
|
||||
before: true,
|
||||
cy: true,
|
||||
Cypress: true,
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2020: true,
|
||||
commonjs: true,
|
||||
jest: true,
|
||||
"jest/globals": true,
|
||||
},
|
||||
parser: "babel-eslint",
|
||||
plugins,
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react/jsx-runtime",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings",
|
||||
"plugin:import/typescript",
|
||||
"plugin:depend/recommended",
|
||||
"plugin:storybook/recommended",
|
||||
"plugin:i18next/recommended",
|
||||
],
|
||||
settings: {
|
||||
"import/internal-regex": "^metabase/|^metabase-lib/",
|
||||
"import/resolver": {
|
||||
webpack: {
|
||||
config: path.resolve(__dirname, "./rspack.main.config.js"),
|
||||
typescript: true,
|
||||
},
|
||||
},
|
||||
"import/ignore": ["\\.css$"],
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
"postcss-modules": {
|
||||
baseDir: "./frontend/src",
|
||||
},
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
legacyDecorators: true,
|
||||
},
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.js", "*.jsx", "*.ts", "*.tsx"],
|
||||
rules: {
|
||||
"jtag-missing-key": "error",
|
||||
"no-unconditional-metabase-links-render": "error",
|
||||
"no-color-literals": "error",
|
||||
"no-literal-metabase-strings": "error",
|
||||
"no-oss-reinitialize-import": "error",
|
||||
"depend/ban-dependencies": [
|
||||
"error",
|
||||
{
|
||||
allowed: [
|
||||
"underscore",
|
||||
"moment",
|
||||
"lodash.orderby",
|
||||
"lodash.debounce",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
"*.unit.spec.*",
|
||||
"frontend/src/metabase/admin/**/*",
|
||||
"frontend/src/metabase/setup/**/*",
|
||||
"enterprise/frontend/src/metabase-enterprise/whitelabel/**/*",
|
||||
"enterprise/frontend/src/metabase-enterprise/embedding/**/*",
|
||||
"frontend/lint/**/*",
|
||||
"*.stories.*",
|
||||
"**/.storybook/*",
|
||||
"stories-data.*",
|
||||
"e2e/**/*",
|
||||
"**/tests/*",
|
||||
"release/**/*",
|
||||
"rspack.config.js",
|
||||
"rspack.main.config.js",
|
||||
"rspack.embedding-sdk-package.config.js",
|
||||
"rspack.embedding-sdk-bundle.config.js",
|
||||
],
|
||||
rules: {
|
||||
"no-color-literals": "off",
|
||||
"no-unconditional-metabase-links-render": "off",
|
||||
"no-literal-metabase-strings": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
"*.unit.spec.*",
|
||||
"frontend/lint/**/*",
|
||||
"*.stories.*",
|
||||
"stories-data.*",
|
||||
"e2e/**/*",
|
||||
"**/tests/*",
|
||||
"release/**/*",
|
||||
"rspack.main.config.js",
|
||||
],
|
||||
rules: {
|
||||
"i18next/no-literal-string": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
extends: ["plugin:@typescript-eslint/recommended"],
|
||||
files: ["*.ts", "*.tsx"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint"],
|
||||
rules: {
|
||||
"prefer-rest-params": "off",
|
||||
"react/prop-types": "off", // TypeScript handles prop validation
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-inferrable-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-this-alias": "off",
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"error",
|
||||
{
|
||||
fixStyle: "inline-type-imports",
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-import-type-side-effects": "error",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
argsIgnorePattern: "^_.+$",
|
||||
varsIgnorePattern: "^_.+$",
|
||||
ignoreRestSiblings: true,
|
||||
destructuredArrayIgnorePattern: "^_.+$",
|
||||
},
|
||||
],
|
||||
// This was introduced in 6.0.0
|
||||
"@typescript-eslint/no-unsafe-declaration-merging": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
extends: [
|
||||
"plugin:jest/recommended",
|
||||
"plugin:jest-dom/recommended",
|
||||
"plugin:testing-library/react",
|
||||
"plugin:jest-formatting/recommended",
|
||||
],
|
||||
plugins: ["jest", "jest-dom", "testing-library", "jest-formatting"],
|
||||
files: [
|
||||
"*.unit.spec.ts",
|
||||
"*.unit.spec.tsx",
|
||||
"*.unit.spec.js",
|
||||
"*.unit.spec.jsx",
|
||||
],
|
||||
rules: {
|
||||
"jest/valid-title": ["error", { ignoreTypeOfDescribeName: true }],
|
||||
"jest/expect-expect": [
|
||||
"error",
|
||||
{
|
||||
assertFunctionNames: ["expect*", "assert*"],
|
||||
additionalTestBlockFunctions: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Enable jest formatting for cypress tests too, the plugin logic just works
|
||||
extends: ["plugin:jest-formatting/recommended"],
|
||||
files: ["*.cy.spec.ts", "*.cy.spec.js"],
|
||||
},
|
||||
{
|
||||
files: ["frontend/src/**/*"],
|
||||
rules: {
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
selector: "Literal[value=/mb-base-color-/]",
|
||||
message:
|
||||
"You may not use base colors in the application, use semantic colors instead. (see colors.module.css)",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["frontend/src/metabase/**/*"],
|
||||
rules: {
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
selector: "Literal[value=/mb-base-color-/]",
|
||||
message:
|
||||
"You may not use base colors in the application, use semantic colors instead. (see colors.module.css)",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["frontend/src/metabase/query_builder/**/*"],
|
||||
rules: {
|
||||
"import/no-cycle": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["docs/**/snippets/**/*.{ts,tsx,js,jsx}"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"import/no-commonjs": "off",
|
||||
"import/no-default-export": "off",
|
||||
"import/order": "off",
|
||||
"import/no-unresolved": "off",
|
||||
"no-color-literals": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["frontend/build/**/*.js"],
|
||||
rules: {
|
||||
"import/no-commonjs": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["**/*.stories.tsx", "**/preview.tsx"],
|
||||
rules: {
|
||||
"import/no-default-export": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
8
.gitattributes
vendored
8
.gitattributes
vendored
@@ -1,9 +1,7 @@
|
||||
# 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.
|
||||
# Custom merge driver for YAML migration files
|
||||
# Treats each changeset as an atomic unit to avoid conflicts within changesets
|
||||
# Execute this in your shell to enable custom merge driver:
|
||||
# echo '\n[include]\npath=../.gitconfig' >> .git/config
|
||||
#
|
||||
# TODO (Bryan 12/12/25) -- reenable this when we can completely verify correctness
|
||||
# resources/migrations/*_update_migrations.yaml merge=yaml-migrations
|
||||
# resources/migrations/*.yaml merge=yaml-migrations
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 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
|
||||
#
|
||||
# TODO (Bryan 12/12/25) -- reenable this when we can completely verify correctness
|
||||
# [merge "yaml-migrations"]
|
||||
# name = YAML migration changeset-aware merge
|
||||
# driver = mage -merge-yaml-migrations %O %A %B %L %P
|
||||
|
||||
268
.github/CODEOWNERS
vendored
268
.github/CODEOWNERS
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
46
.github/actions/fetch-artifact/action.yml
vendored
46
.github/actions/fetch-artifact/action.yml
vendored
@@ -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));
|
||||
|
||||
|
||||
8
.github/actions/get-cache-keys/action.yml
vendored
8
.github/actions/get-cache-keys/action.yml
vendored
@@ -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
|
||||
|
||||
30
.github/actions/prepare-backend/action.yml
vendored
30
.github/actions/prepare-backend/action.yml
vendored
@@ -1,13 +1,14 @@
|
||||
name: Prepare back-end environment
|
||||
description: |
|
||||
Prepare the backend. Installs jdk, Clojure, and fetches the cache for m2.
|
||||
The CI config fetches `ci-test-config.json` from master and lets us ignore tests at the var level if they are flaking.
|
||||
inputs:
|
||||
java-version:
|
||||
required: true
|
||||
default: "21"
|
||||
default: '21'
|
||||
clojure-version:
|
||||
required: true
|
||||
default: "1.12.0.1488"
|
||||
default: '1.12.0.1488'
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
@@ -16,33 +17,18 @@ runs:
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ inputs.java-version }}
|
||||
distribution: "temurin"
|
||||
distribution: 'temurin'
|
||||
- name: Install Clojure CLI
|
||||
shell: bash
|
||||
continue-on-error: true
|
||||
run: |
|
||||
for attempt in 1 2 3; do
|
||||
curl --retry 3 --retry-delay 5 --fail -O https://download.clojure.org/install/linux-install-${{ inputs.clojure-version }}.sh \
|
||||
&& sudo bash ./linux-install-${{ inputs.clojure-version }}.sh \
|
||||
&& break
|
||||
echo "::warning::Clojure install attempt $attempt failed, retrying in 10s..."
|
||||
sleep 10
|
||||
done
|
||||
if ! clojure --version; then
|
||||
echo "::error::Clojure CLI installation failed after 3 attempts"
|
||||
exit 1
|
||||
fi
|
||||
curl -O https://download.clojure.org/install/linux-install-${{ inputs.clojure-version }}.sh &&
|
||||
sudo bash ./linux-install-${{ inputs.clojure-version }}.sh
|
||||
|
||||
- name: Get M2 cache key
|
||||
uses: ./.github/actions/get-cache-keys
|
||||
id: get-cache-keys
|
||||
|
||||
- name: Install uv (for Python package installation)
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
enable-cache: false
|
||||
version: "0.10.2" # keep in sync with mise.toml
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Restore M2 cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
@@ -50,5 +36,3 @@ runs:
|
||||
~/.m2
|
||||
~/.gitlibs
|
||||
key: ${{ steps.get-cache-keys.outputs.m2-cache-key }}
|
||||
restore-keys: |
|
||||
${{ steps.get-cache-keys.outputs.m2-restore-key }}
|
||||
|
||||
17
.github/actions/prepare-cypress/action.yml
vendored
17
.github/actions/prepare-cypress/action.yml
vendored
@@ -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 }}
|
||||
|
||||
27
.github/actions/prepare-frontend/action.yml
vendored
27
.github/actions/prepare-frontend/action.yml
vendored
@@ -11,10 +11,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 +28,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 +42,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
|
||||
|
||||
20
.github/actions/prepare-node-bun/action.yml
vendored
20
.github/actions/prepare-node-bun/action.yml
vendored
@@ -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 }}
|
||||
@@ -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
|
||||
132
.github/actions/prepare-shoppy-e2e/action.yml
vendored
132
.github/actions/prepare-shoppy-e2e/action.yml
vendored
@@ -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 /
|
||||
@@ -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
|
||||
|
||||
110
.github/actions/sdk-bundle-size-check/action.yml
vendored
110
.github/actions/sdk-bundle-size-check/action.yml
vendored
@@ -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
|
||||
15
.github/actions/test-driver/action.yml
vendored
15
.github/actions/test-driver/action.yml
vendored
@@ -12,15 +12,6 @@ inputs:
|
||||
logs-artifact-suffix:
|
||||
required: false
|
||||
description: Suffix to append to the test logs artifact name to avoid conflicts in matrix jobs
|
||||
edition:
|
||||
required: false
|
||||
default: 'ee'
|
||||
description: Edition to test (oss or ee)
|
||||
|
||||
outputs:
|
||||
test-outcome:
|
||||
description: "The outcome of the test run (success, failure, cancelled, skipped)"
|
||||
value: ${{ steps.run-test.outcome }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
@@ -32,12 +23,12 @@ runs:
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
- name: Build static viz frontend
|
||||
if: ${{ inputs.build-static-viz == 'true' }}
|
||||
run: bun run build-static-viz
|
||||
run: yarn build-static-viz
|
||||
shell: bash
|
||||
env:
|
||||
MB_EDITION: ee
|
||||
- name: Test database driver
|
||||
run: clojure -X:dev:ci:${{ inputs.edition }}:${{ inputs.edition }}-dev:drivers:drivers-dev:test ${{ inputs.test-args }}
|
||||
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!
|
||||
@@ -61,7 +52,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: ${{ format('test-logs-{0}{1}', github.job, (inputs.logs-artifact-suffix == '' || inputs.logs-artifact-suffix == null) && '' || format('-{0}', inputs.logs-artifact-suffix)) }}
|
||||
path: logs/test-log.json
|
||||
retention-days: 1
|
||||
# Failing to upload the artifact should never affect the outcome of the whole test run.
|
||||
|
||||
@@ -21,8 +21,6 @@ inputs:
|
||||
variant:
|
||||
required: false
|
||||
default: ""
|
||||
previous-step-outcome:
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
@@ -59,4 +57,3 @@ runs:
|
||||
dry-run: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == true }} # quarantine, but do not upload results
|
||||
token: ${{ inputs.trunk-api-token }}
|
||||
use-cache: true
|
||||
previous-step-outcome: ${{ inputs.previous-step-outcome }}
|
||||
|
||||
75
.github/copilot-instructions.md
vendored
75
.github/copilot-instructions.md
vendored
@@ -1,59 +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
|
||||
|
||||
## Release flags
|
||||
Enterprise functionality MUST use the plugin system. It is very important to not expose enterprise code in the OSS version.
|
||||
|
||||
All code adding or using release flags should follow the best practices outlined in `docs/developers-guide/release-flags.md`.
|
||||
#### Testing Requirements
|
||||
|
||||
Language-specific rules are in `.github/instructions/*.instructions.md`.
|
||||
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.
|
||||
|
||||
18
.github/file-paths.yaml
vendored
18
.github/file-paths.yaml
vendored
@@ -26,11 +26,11 @@ frontend_ci: &frontend_ci
|
||||
- ".github/workflows/frontend.yml"
|
||||
|
||||
frontend_configs: &frontend_configs
|
||||
- "bun.lock"
|
||||
- "yarn.lock"
|
||||
- "**/tsconfig*.json"
|
||||
- "package.json"
|
||||
- "babel.config.json"
|
||||
- "eslint.config.mjs"
|
||||
- ".eslintrc.js"
|
||||
- "postcss.config.js"
|
||||
- "rspack.config.js"
|
||||
- "rspack.main.config.js"
|
||||
@@ -85,9 +85,7 @@ backend_ci: &backend_ci
|
||||
|
||||
backend_presto_kerberos:
|
||||
- "**/presto_jdbc/**"
|
||||
- "**/presto-jdbc/**" # the driver module has a hyphen. underscores are classpaths
|
||||
- "**/presto_jdbc.clj"
|
||||
- "**/presto_jdbc_test.clj"
|
||||
- ".github/workflows/presto-kerberos-integration-test.yml"
|
||||
|
||||
backend_sources: &backend_sources
|
||||
@@ -199,15 +197,3 @@ cljs:
|
||||
- *shared_specs
|
||||
- ".github/workflows/cljs.yml"
|
||||
- "shadow-cljs.edn"
|
||||
|
||||
openapi:
|
||||
- ".github/workflows/openapi.yml"
|
||||
- "src/metabase/api/**"
|
||||
- "src/metabase/api_routes/**"
|
||||
- "src/metabase/*_rest/api.clj"
|
||||
- "src/metabase/*/api.clj"
|
||||
- "src/metabase/*/api/*.clj"
|
||||
- "enterprise/backend/src/metabase_enterprise/**/api.clj"
|
||||
- "enterprise/backend/src/metabase_enterprise/**/api/*.clj"
|
||||
- "resources/openapi/**"
|
||||
- "package.json"
|
||||
|
||||
9
.github/instructions/clojure.instructions.md
vendored
9
.github/instructions/clojure.instructions.md
vendored
@@ -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`
|
||||
9
.github/instructions/docs.instructions.md
vendored
9
.github/instructions/docs.instructions.md
vendored
@@ -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`
|
||||
11
.github/instructions/typescript.instructions.md
vendored
11
.github/instructions/typescript.instructions.md
vendored
@@ -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`
|
||||
52
.github/scripts/build-cross-version-matrix.js
vendored
52
.github/scripts/build-cross-version-matrix.js
vendored
@@ -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 };
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ const { buildMatrix } = require("./build-e2e-matrix");
|
||||
describe("buildMatrix", () => {
|
||||
const options = {
|
||||
java: "11",
|
||||
defaultRunner: "${{ vars.DEFAULT_RUNNER_KEY }}",
|
||||
defaultRunner: "ubuntu-22.04",
|
||||
};
|
||||
const testBuildMatrix = (entry, chunks) =>
|
||||
buildMatrix(options, entry, chunks);
|
||||
|
||||
41
.github/scripts/csv-to-mb.js
vendored
41
.github/scripts/csv-to-mb.js
vendored
@@ -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 };
|
||||
317
.github/scripts/generate-llms-txt.js
vendored
Normal file
317
.github/scripts/generate-llms-txt.js
vendored
Normal file
@@ -0,0 +1,317 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const REPO = "metabase/metabase";
|
||||
const OUTPUT_FILE = "llms.txt";
|
||||
|
||||
// Sections to generate llms-<name>-full.txt for.
|
||||
// Each section can define a preamble (introductory content before the main docs).
|
||||
const LLMS_FULL_TO_GENERATE = [
|
||||
{
|
||||
section: "embedding",
|
||||
getPreamble: (branch) =>
|
||||
aboveVersion(branch, 57) ? getModularEmbeddingGotchaNotes() : "",
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* List directory recursively to get markdown files
|
||||
*
|
||||
* @param {string} docsPath - full path to docs directory
|
||||
* @returns {Array} list of markdown file objects with fullPath and relativePath
|
||||
*/
|
||||
function getMarkdownFilesList(docsPath) {
|
||||
if (!fs.existsSync(docsPath)) {
|
||||
console.error(`Error: Docs directory '${docsPath}' does not exist`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(docsPath, {
|
||||
recursive: true,
|
||||
withFileTypes: true,
|
||||
});
|
||||
|
||||
return entries
|
||||
.filter((entry) => {
|
||||
return (
|
||||
entry.isFile() &&
|
||||
// Only markdown files
|
||||
entry.name.endsWith(".md") &&
|
||||
// Ignore README.md
|
||||
entry.name !== "README.md" &&
|
||||
// Ignore node_modules and hidden directories
|
||||
!entry.parentPath
|
||||
.split(path.sep)
|
||||
.some((dir) => dir === "node_modules" || dir.startsWith("."))
|
||||
);
|
||||
})
|
||||
.map((entry) => {
|
||||
const fullPath = path
|
||||
.join(entry.parentPath, entry.name)
|
||||
.replace(/\\/g, "/");
|
||||
|
||||
const relativePath = path
|
||||
.relative(docsPath, fullPath)
|
||||
.replace(/\\/g, "/");
|
||||
|
||||
return { fullPath, relativePath };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate llms.txt file with links to all markdown files in docs/
|
||||
*
|
||||
* @param {string} branch - branch name (e.g. 'master' or 'release-x.58.x')
|
||||
* @param {Array} files - list of all markdown file objects
|
||||
* @param {Array<Object>} sectionsConfig - list of section config objects to generate complete references for
|
||||
*/
|
||||
function generateLlmsTxt(branch, files, sectionsConfig) {
|
||||
const baseUrl = `https://raw.githubusercontent.com/${REPO}/refs/heads/${branch}`;
|
||||
|
||||
const filesToExtract = files
|
||||
// Files in embedding/sdk/api/snippets does not exist in the repo.
|
||||
.filter(
|
||||
(file) => !file.relativePath.startsWith("embedding/sdk/api/snippets"),
|
||||
);
|
||||
|
||||
const entries = filesToExtract.map((file) => {
|
||||
const content = fs.readFileSync(file.fullPath, "utf8");
|
||||
const title = extractTitle(content, file.relativePath);
|
||||
const url = `${baseUrl}/docs/${file.relativePath}`;
|
||||
|
||||
return { title, url, path: file.relativePath };
|
||||
});
|
||||
|
||||
// Generate table of contents entries
|
||||
const tableOfContents = entries.map(
|
||||
(entry) => `- [${entry.title}](${entry.url})`,
|
||||
);
|
||||
|
||||
// Generate complete references entries
|
||||
const completeReferences = sectionsConfig
|
||||
.filter((config) =>
|
||||
files.some((file) => file.relativePath.startsWith(`${config.section}/`)),
|
||||
)
|
||||
.map((config) => {
|
||||
const capitalized =
|
||||
config.section.charAt(0).toUpperCase() + config.section.slice(1);
|
||||
return `- [${capitalized} - Complete Reference](${baseUrl}/llms-${config.section}-full.txt)`;
|
||||
});
|
||||
|
||||
// Generate llms.txt content using template literal
|
||||
const content = `# Metabase Documentation
|
||||
Metabase - The simplest, fastest way to get business intelligence and analytics to everyone in your company.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
${tableOfContents.join("\n")}
|
||||
|
||||
## Complete References
|
||||
|
||||
These files are very large and are around 90,000 tokens. Do not use by default unless the context window is huge or RAG is supported in your editor.
|
||||
|
||||
${completeReferences.join("\n")}
|
||||
`;
|
||||
|
||||
fs.writeFileSync(OUTPUT_FILE, content, "utf8");
|
||||
|
||||
console.log(
|
||||
`✅ Generated ${OUTPUT_FILE} with ${entries.length} markdown files`,
|
||||
);
|
||||
console.log(` Branch: ${branch}`);
|
||||
console.log(` Base URL: ${baseUrl}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate llms-full.txt files for specified sections
|
||||
*
|
||||
* @param {string} branch - branch name (e.g., 'master', 'release-x.58.x')
|
||||
* @param {Array} files - list of all markdown file objects
|
||||
* @param {Array<Object>} sectionsConfig - list of section config objects to generate full files for
|
||||
*/
|
||||
function generateLlmsFullFiles(branch, files, sectionsConfig) {
|
||||
const baseUrl = `https://raw.githubusercontent.com/${REPO}/refs/heads/${branch}`;
|
||||
|
||||
sectionsConfig.forEach((config) => {
|
||||
generateLlmsFullTxt(branch, config, files, baseUrl);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* LLMs are likely to pay attention to the very first lines.
|
||||
*
|
||||
* We add the most important context for LLMs to avoid
|
||||
* confusion and pitfalls like out-of-date APIs in trained data.
|
||||
*
|
||||
* @returns {string} Markdown formatted gotcha notes
|
||||
*/
|
||||
function getModularEmbeddingGotchaNotes() {
|
||||
return `> **Important Version Notes**
|
||||
>
|
||||
> Watch out for these deprecated props and gotchas for Metabase 57 onwards, for modular embedding:
|
||||
>
|
||||
> 1. \`config\` prop on MetabaseProvider no longer exist - it is replaced by \`authConfig\`.
|
||||
> 2. \`authProviderUri\` field no longer exist.
|
||||
> 3. \`jwtProviderUri\` optional field only exists in v58+. This is used to make JWT auth faster by skipping the \`GET /auth/sso\` discovery request. Not needed for initial implementation.
|
||||
> 4. \`fetchRequestToken\` is not needed by default. This is only used to customize how the SDK fetches the request token.
|
||||
> 5. Numeric IDs must be integers not strings, e.g. \`dashboardId={1}\`. When the ID is retrieved from the URL and it is numeric, convert it to an integer via \`parseInt\` before passing it to the SDK. IDs can also be strings for entity ids, so you should not parse all IDs as numbers if entity ids are also to be expected.
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate llms-full.txt file for a specific section
|
||||
* @param {string} branch - The branch name
|
||||
* @param {Object} config - Section config object with section name and optional getPreamble function
|
||||
* @param {Array} files - Array of all markdown file objects
|
||||
* @param {string} baseUrl - Base URL for raw GitHub links
|
||||
*/
|
||||
function generateLlmsFullTxt(branch, config, files, baseUrl) {
|
||||
const { section, getPreamble } = config;
|
||||
|
||||
// Filter docs for this section
|
||||
const sectionFiles = files.filter((file) =>
|
||||
file.relativePath.startsWith(`${section}/`),
|
||||
);
|
||||
|
||||
if (sectionFiles.length === 0) {
|
||||
console.log(` Skipping llms-full.txt for ${section} (no files found)`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build content
|
||||
const sectionCapitalized = section.charAt(0).toUpperCase() + section.slice(1);
|
||||
const header = `# Metabase ${sectionCapitalized} - Complete Reference for AI agents
|
||||
|
||||
> Table of contents: ${baseUrl}/${OUTPUT_FILE}
|
||||
|
||||
`;
|
||||
|
||||
// Add preamble if provided (e.g., version-specific notes, warnings)
|
||||
const preamble = getPreamble ? getPreamble(branch) : "";
|
||||
|
||||
// Concatenate all documents
|
||||
const docs = sectionFiles
|
||||
.map((file) => readAndCleanMarkdown(file.fullPath))
|
||||
.join("\n");
|
||||
|
||||
// Write llms-full.txt file at repo root
|
||||
const content = header + preamble + "\n" + docs;
|
||||
const llmsFullPath = `llms-${section}-full.txt`;
|
||||
fs.writeFileSync(llmsFullPath, content, "utf8");
|
||||
|
||||
console.log(
|
||||
`✅ Generated ${llmsFullPath} with ${sectionFiles.length} documents`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract title from markdown file frontmatter or filename.
|
||||
*
|
||||
* We try to extract the title in this order: 1) YAML frontmatter
|
||||
* 2) first H1 heading, and 3) the Markdown file name.
|
||||
*
|
||||
* Example Markdown and frontmatter format:
|
||||
* ```markdown
|
||||
* ---
|
||||
* title: "Getting Started"
|
||||
* description: "A guide"
|
||||
* ---
|
||||
* # Getting Started
|
||||
* ```
|
||||
*
|
||||
* @param {string} content - file content
|
||||
* @param {string} filePath - relative file path
|
||||
* @returns {string} title of content
|
||||
*/
|
||||
function extractTitle(content, filePath) {
|
||||
// Try to extract title from YAML frontmatter
|
||||
// First, match against the YAML frontmatter delimiter `---`
|
||||
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
|
||||
|
||||
if (frontmatterMatch) {
|
||||
// Then, match against the title property
|
||||
// Example: `title: "My Page"`
|
||||
const frontmatter = frontmatterMatch[1];
|
||||
const titleMatch = frontmatter.match(/^title:\s*(.+)$/m);
|
||||
|
||||
// Remove quotes if present
|
||||
if (titleMatch) {
|
||||
return titleMatch[1].trim().replace(/^["']|["']$/g, "");
|
||||
}
|
||||
}
|
||||
|
||||
// Try to extract title from first H1 heading
|
||||
// Example: `# My Page`
|
||||
const h1Match = content.match(/^#\s+(.+)$/m);
|
||||
|
||||
if (h1Match) {
|
||||
return h1Match[1].trim();
|
||||
}
|
||||
|
||||
// Fallback to filename, e.g. `my-page.md`
|
||||
const filename = path.basename(filePath, ".md");
|
||||
|
||||
// Convert kebab-case or snake_case to Title Case
|
||||
// `my-page.md` would become "My Page"
|
||||
return filename
|
||||
.split(/[-_]/)
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and clean markdown file content
|
||||
*
|
||||
* @param {string} filePath - full path to the markdown file
|
||||
* @returns {string} cleaned markdown content with separator
|
||||
*/
|
||||
function readAndCleanMarkdown(filePath) {
|
||||
let content = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
// Strip YAML frontmatter
|
||||
content = content.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, "");
|
||||
|
||||
// Strip Jekyll/Liquid template syntax
|
||||
content = content.replace(/\{%.*?%\}/g, "");
|
||||
content = content.replace(/\{\{.*?\}\}/g, "");
|
||||
|
||||
return `${content.trim()}\n\n---\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if branch version is above target version
|
||||
* @param {string} branch - Branch name (e.g., 'master', 'release-x.58.x')
|
||||
* @param {number} targetVersion - Target version number
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function aboveVersion(branch, targetVersion) {
|
||||
if (branch === "master") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse version from release branch like "release-x.58.x" -> 58
|
||||
const match = branch.match(/release-x\.(\d+)\.x/);
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const version = parseInt(match[1], 10);
|
||||
|
||||
return version >= targetVersion;
|
||||
}
|
||||
|
||||
// Main execution
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
const docsPath = args[0] || "docs";
|
||||
const branch = args[1] || "master";
|
||||
|
||||
const resolvedDocsPath = path.resolve(docsPath);
|
||||
const markdownFiles = getMarkdownFilesList(resolvedDocsPath);
|
||||
|
||||
// Sort all files upfront for consistent ordering across all generated files
|
||||
markdownFiles.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
||||
|
||||
generateLlmsTxt(branch, markdownFiles, LLMS_FULL_TO_GENERATE);
|
||||
generateLlmsFullFiles(branch, markdownFiles, LLMS_FULL_TO_GENERATE);
|
||||
}
|
||||
3
.github/scripts/setup-python-runner.sh
vendored
3
.github/scripts/setup-python-runner.sh
vendored
@@ -19,11 +19,12 @@ while [[ $# -gt 0 ]]; do
|
||||
done
|
||||
|
||||
echo "Cloning python-runner repository..."
|
||||
git clone --depth 1 https://${METABASE_AUTOMATION_USER_TOKEN}@github.com/metabase/python-runner-container.git python-runner
|
||||
git clone https://${METABASE_AUTOMATION_USER_TOKEN}@github.com/metabase/python-runner-container.git python-runner
|
||||
|
||||
if [ "$SKIP_START" = false ]; then
|
||||
echo "Starting python-runner..."
|
||||
cd python-runner
|
||||
git checkout origin/memory_improvements_2
|
||||
make run-ci 1> ../python-runner.out.log 2> ../python-runner.err.log
|
||||
make logs 1>> ../python-runner.out.log 2>> ../python-runner.err.log &
|
||||
cd ..
|
||||
|
||||
52
.github/team.json
vendored
52
.github/team.json
vendored
@@ -1,26 +1,24 @@
|
||||
{
|
||||
"teams": [
|
||||
{
|
||||
"name": "Querying Platform",
|
||||
"label": ".Team/QueryingPlatform",
|
||||
"members": [
|
||||
"bshepherdson",
|
||||
"metamben",
|
||||
"rileythomp",
|
||||
"camsaul"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Graphy",
|
||||
"label": ".Team/Graphy",
|
||||
"name": "Querying",
|
||||
"label": ".Team/Querying",
|
||||
"members": [
|
||||
"ranquild",
|
||||
"romeovs",
|
||||
"bshepherdson",
|
||||
"metamben",
|
||||
"wzimrin",
|
||||
"ericnormand",
|
||||
"galdre",
|
||||
"dpsutton",
|
||||
"escherize",
|
||||
"bgrabow"
|
||||
"galdre"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Drivers",
|
||||
"label": ".Team/Drivers",
|
||||
"members": [
|
||||
"snoe",
|
||||
"rileythomp"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -45,6 +43,9 @@
|
||||
"name": "DevEx",
|
||||
"label": ".Team/DevEx",
|
||||
"members": [
|
||||
"camsaul",
|
||||
"dpsutton",
|
||||
"escherize",
|
||||
"fraserdrops",
|
||||
"nemanjaglumac"
|
||||
]
|
||||
@@ -61,8 +62,7 @@
|
||||
"tsplude",
|
||||
"lbrdnk",
|
||||
"piranha",
|
||||
"crisptrutski",
|
||||
"undrfined"
|
||||
"crisptrutski"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -86,12 +86,16 @@
|
||||
"eliotmetabase",
|
||||
"npfitz",
|
||||
"johnswanson",
|
||||
"iethree",
|
||||
"nvoxland",
|
||||
"edpaget",
|
||||
"deniskaber",
|
||||
"alxnddr",
|
||||
"bpander",
|
||||
"andsalves",
|
||||
"kevinkelleher12"
|
||||
"gantoreno",
|
||||
"kevinkelleher12",
|
||||
"undrfined"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -108,16 +112,6 @@
|
||||
"uladzimirdev",
|
||||
"egorgrushin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Growth Eng",
|
||||
"label": ".Team/GrowthEng",
|
||||
"members": [
|
||||
"iethree",
|
||||
"bpander",
|
||||
"gantoreno",
|
||||
"losrebellos"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
52
.github/workflows/app-db.yml
vendored
52
.github/workflows/app-db.yml
vendored
@@ -14,20 +14,20 @@ concurrency:
|
||||
jobs:
|
||||
be-tests-mariadb:
|
||||
if: ${{ !inputs.skip }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 40
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version:
|
||||
- name: MariaDB 10.6
|
||||
junit-name: be-tests-mariadb-10-6-ee
|
||||
image: metabase/mariadb:10.6 # https://github.com/metabase/metabase-mariadb-docker-image
|
||||
- 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
|
||||
junit-name: be-tests-mariadb-latest-ee
|
||||
image: mariadb:latest
|
||||
image: circleci/mariadb:latest
|
||||
env:
|
||||
enable-ssl-tests: 'false'
|
||||
job:
|
||||
@@ -57,8 +57,8 @@ jobs:
|
||||
ports:
|
||||
- "3306:3306"
|
||||
env:
|
||||
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: true
|
||||
MARIADB_DATABASE: mb_test
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: true
|
||||
MYSQL_DATABASE: circle_test
|
||||
env:
|
||||
# Only run old migrations tests on pushes to master or release branches. All other branches should skip tests
|
||||
# with the tag `mb/old-migrations-test`. `__ADDITIONAL_EXCLUDED_TAG__` is not used anywhere outside of splicing
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
MB_DB_TYPE: mysql
|
||||
MB_DB_HOST: localhost
|
||||
MB_DB_PORT: 3306
|
||||
MB_DB_DBNAME: mb_test
|
||||
MB_DB_DBNAME: circle_test
|
||||
MB_DB_USER: root
|
||||
MB_MYSQL_TEST_USER: root
|
||||
#
|
||||
@@ -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: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 40
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version:
|
||||
- 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: cimg/mysql:8.0
|
||||
env:
|
||||
enable-ssl-tests: 'false'
|
||||
enable-aws-iam-tests: 'false'
|
||||
@@ -174,7 +172,7 @@ jobs:
|
||||
- "3306:3306"
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: true
|
||||
MYSQL_DATABASE: mb_test
|
||||
MYSQL_DATABASE: circle_test
|
||||
env:
|
||||
# Only run old migrations tests on pushes to master or release branches. All other branches should skip tests
|
||||
# with the tag `mb/old-migrations-test`. `__ADDITIONAL_EXCLUDED_TAG__` is not used anywhere outside of splicing
|
||||
@@ -197,7 +195,7 @@ jobs:
|
||||
MB_DB_TYPE: mysql
|
||||
MB_DB_HOST: localhost
|
||||
MB_DB_PORT: 3306
|
||||
MB_DB_DBNAME: mb_test
|
||||
MB_DB_DBNAME: circle_test
|
||||
MB_DB_USER: root
|
||||
MB_MYSQL_TEST_USER: root
|
||||
#
|
||||
@@ -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: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 40
|
||||
runs-on: ubuntu-22.04
|
||||
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'
|
||||
@@ -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,14 +354,13 @@ 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:
|
||||
- be-tests-mariadb
|
||||
- be-tests-mysql
|
||||
- be-tests-postgres
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
name: app-db-tests-result
|
||||
if: always() && !cancelled()
|
||||
|
||||
2
.github/workflows/athena.yml
vendored
2
.github/workflows/athena.yml
vendored
@@ -11,7 +11,7 @@ on:
|
||||
jobs:
|
||||
deploy:
|
||||
name: Publish Athena JDBC Driver
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Check out the code
|
||||
|
||||
2
.github/workflows/auto-assign.yml
vendored
2
.github/workflows/auto-assign.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
update_pr:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: tibdex/github-app-token@v2.1.0
|
||||
|
||||
2
.github/workflows/auto-backport.yml
vendored
2
.github/workflows/auto-backport.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
github.event.label.name == 'triple-backport'
|
||||
)
|
||||
)
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
185
.github/workflows/backend-cloverage.yml
vendored
185
.github/workflows/backend-cloverage.yml
vendored
@@ -1,42 +1,109 @@
|
||||
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: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
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: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
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,27 +130,61 @@ jobs:
|
||||
files: ./target/coverage/codecov.json
|
||||
flags: back-end
|
||||
|
||||
setup-ee:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
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: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
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
|
||||
|
||||
30
.github/workflows/backend.yml
vendored
30
.github/workflows/backend.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
jobs:
|
||||
be-linter-clj-kondo:
|
||||
if: ${{ !inputs.skip }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
name: Clj-Kondo
|
||||
steps:
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
be-linter-eastwood:
|
||||
if: ${{ !inputs.skip }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 20
|
||||
name: Eastwood
|
||||
steps:
|
||||
@@ -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 && github.ref_name == 'master' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 30
|
||||
name: Eastwood (tests only)
|
||||
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 our backend tests
|
||||
|
||||
be-tests:
|
||||
if: ${{ !inputs.skip }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 40
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -116,7 +104,7 @@ jobs:
|
||||
with:
|
||||
java-version: "${{ matrix.java-version }}"
|
||||
- name: Build static-viz frontend
|
||||
run: bun run build-static-viz
|
||||
run: yarn build-static-viz
|
||||
env:
|
||||
MB_EDITION: ${{ matrix.job.edition }}
|
||||
|
||||
@@ -142,7 +130,6 @@ jobs:
|
||||
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
previous-step-outcome: ${{ steps.run-java-tests.outcome }}
|
||||
|
||||
- name: Publish Test Report (JUnit)
|
||||
uses: dorny/test-reporter@v1
|
||||
@@ -167,7 +154,7 @@ jobs:
|
||||
# as `:dev` dependencies). See #27009 for more context.
|
||||
be-check:
|
||||
if: ${{ !inputs.skip }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -183,7 +170,7 @@ jobs:
|
||||
|
||||
be-cljfmt:
|
||||
if: ${{ !inputs.skip }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
name: Cljfmt
|
||||
steps:
|
||||
@@ -196,13 +183,12 @@ jobs:
|
||||
be-tests-result:
|
||||
needs:
|
||||
- be-linter-eastwood
|
||||
- be-linter-eastwood-test
|
||||
- be-linter-clj-kondo
|
||||
- be-cljfmt
|
||||
- be-check
|
||||
- be-tests
|
||||
if: always() && !cancelled()
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
needs: ${{ toJson(needs) }}
|
||||
|
||||
2
.github/workflows/backport-reminder.yml
vendored
2
.github/workflows/backport-reminder.yml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
jobs:
|
||||
backport-reminder:
|
||||
name: Decide whether to backport or not
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: jesusvasquez333/verify-pr-label-action@v1.4.0
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
|
||||
jobs:
|
||||
check-source-branch:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Block master branch PRs
|
||||
if: github.head_ref == 'master'
|
||||
|
||||
47
.github/workflows/build-for-release.yml
vendored
47
.github/workflows/build-for-release.yml
vendored
@@ -14,16 +14,14 @@ on:
|
||||
|
||||
jobs:
|
||||
start-message:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 5
|
||||
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:
|
||||
@@ -45,7 +43,7 @@ jobs:
|
||||
}).catch(console.error);
|
||||
|
||||
trigger-milestone-check:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 5
|
||||
needs: start-message
|
||||
steps:
|
||||
@@ -66,7 +64,7 @@ jobs:
|
||||
|
||||
|
||||
check-version:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
ee: ${{ fromJson(steps.canonical_version.outputs.result).ee }}
|
||||
@@ -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
|
||||
@@ -118,18 +114,16 @@ jobs:
|
||||
return versions;
|
||||
|
||||
check-commit:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Check out the code to verify the release branch
|
||||
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,16 +135,15 @@ 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: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
java_version: ${{ fromJson(steps.dependencies.outputs.result).java_version }}
|
||||
@@ -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
|
||||
@@ -185,7 +176,7 @@ jobs:
|
||||
|
||||
build-uberjar-for-release:
|
||||
needs: [check-version, check-commit, get-build-requirements]
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 50
|
||||
strategy:
|
||||
matrix:
|
||||
|
||||
2
.github/workflows/build-scripts.yml
vendored
2
.github/workflows/build-scripts.yml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
jobs:
|
||||
|
||||
test-build-scripts:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Prepare back-end environment
|
||||
|
||||
33
.github/workflows/cache-generator.yml
vendored
33
.github/workflows/cache-generator.yml
vendored
@@ -39,8 +39,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
get-cache-keys:
|
||||
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get cache keys
|
||||
@@ -53,7 +52,7 @@ jobs:
|
||||
cypress-cache-key: ${{ steps.get-cache-keys.outputs.cypress-cache-key }}
|
||||
|
||||
generate-caches:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
needs: get-cache-keys
|
||||
env:
|
||||
@@ -83,7 +82,7 @@ jobs:
|
||||
|
||||
# Frontend preparation and caches
|
||||
# `prepare-frontend` action (1) prepares Node.js and (2) fetches the node_modules cache if it exists.
|
||||
# It then (3) runs `bun install --frozen-lockfile` which ensures the local cache is fully populated.
|
||||
# It then (3) runs `yarn install --frozen-lockfile --prefer-offline` which ensures the local cache is fully populated.
|
||||
# IMPORTANT: Caching node_modules after patches are already applied will break any PR that attempts to either:
|
||||
# - upgrade a patched library
|
||||
# - change a patch
|
||||
@@ -93,31 +92,31 @@ jobs:
|
||||
with:
|
||||
apply-patches: "false"
|
||||
|
||||
- name: Get bun cache directory
|
||||
id: bun-cache
|
||||
- name: Get yarn cache directory
|
||||
id: yarn-cache-dir
|
||||
run: |
|
||||
BUN_CACHE_DIR=$(bun pm cache)
|
||||
echo "Bun cache directory: $BUN_CACHE_DIR"
|
||||
echo "dir=$BUN_CACHE_DIR" >> $GITHUB_OUTPUT
|
||||
YARN_CACHE_DIR=$(yarn cache dir)
|
||||
echo "Yarn cache directory: $YARN_CACHE_DIR"
|
||||
echo "dir=$YARN_CACHE_DIR" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Ensure that Cypress executable is ready
|
||||
run: |
|
||||
bun run cypress install
|
||||
bun run cypress cache path
|
||||
bun run cypress cache list
|
||||
bun run cypress verify
|
||||
yarn cypress install
|
||||
yarn cypress cache path
|
||||
yarn cypress cache list
|
||||
yarn cypress verify
|
||||
shell: bash
|
||||
|
||||
# Run Prettier to populate its cache (stored in node_modules/.cache/prettier)
|
||||
# so it gets saved as part of the node_modules cache below
|
||||
- name: Run Prettier to populate cache
|
||||
run: bun run lint-prettier-pure
|
||||
run: yarn lint-prettier-pure
|
||||
|
||||
- name: Update node_modules cache
|
||||
uses: ./.github/actions/update-cache
|
||||
with:
|
||||
path: |
|
||||
${{ steps.bun-cache.outputs.dir }}
|
||||
${{ steps.yarn-cache-dir.outputs.dir }}
|
||||
node_modules
|
||||
key: ${{ env.NODE_CACHE_KEY }}
|
||||
|
||||
@@ -129,10 +128,10 @@ jobs:
|
||||
|
||||
# ESLint cache (requires both backend and frontend)
|
||||
- name: Compile CLJS
|
||||
run: bun run build-pure:cljs
|
||||
run: yarn build-pure:cljs
|
||||
|
||||
- name: Run ESLint to populate cache
|
||||
run: bun run lint-eslint-pure
|
||||
run: yarn lint-eslint-pure
|
||||
|
||||
- name: Update eslint cache
|
||||
uses: ./.github/actions/update-cache
|
||||
|
||||
2
.github/workflows/check-backport-script.yml
vendored
2
.github/workflows/check-backport-script.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
check-backport-script:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
10
.github/workflows/check-milestone.yml
vendored
10
.github/workflows/check-milestone.yml
vendored
@@ -15,7 +15,7 @@ on:
|
||||
jobs:
|
||||
set-milestone:
|
||||
name: Automatically set milestone
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
isNewReleaseBranch: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
@@ -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
|
||||
|
||||
6
.github/workflows/chromatic.yml
vendored
6
.github/workflows/chromatic.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Check which files changed
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
frontend_all: ${{ steps.changes.outputs.frontend_all }}
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
fe-chromatic:
|
||||
needs: files-changed
|
||||
if: contains(github.event.pull_request.labels.*.name, 'chromatic') || (github.ref_name == 'master' && needs.files-changed.outputs.frontend_all == 'true')
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -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:
|
||||
|
||||
354
.github/workflows/claude-code-bug-fix.yml
vendored
Normal file
354
.github/workflows/claude-code-bug-fix.yml
vendored
Normal file
@@ -0,0 +1,354 @@
|
||||
name: "Claude Code Bug Fix"
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- labeled
|
||||
pull_request_review:
|
||||
types:
|
||||
- submitted
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: "Issue number to fix"
|
||||
required: true
|
||||
type: string
|
||||
override_employee_check:
|
||||
description: "Override employee check for issue creator"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
auto-fix-bug:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
if: |
|
||||
(github.event_name == 'issues' && github.event.label.name == 'Claude Bug Fix' && github.event.issue.state == 'open') ||
|
||||
(github.event_name == 'workflow_dispatch') ||
|
||||
(github.event_name == 'pull_request_review' && github.event.review.state == 'changes_requested' && startsWith(github.event.pull_request.head.ref, 'claude-fix/'))
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get issue data and check employee status
|
||||
id: get_issue
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { getIssueData } = require('${{ github.workspace }}/.github/scripts/get-issue-data.js');
|
||||
|
||||
try {
|
||||
const result = await getIssueData({
|
||||
context,
|
||||
github,
|
||||
manualIssueNumber: '${{ github.event.inputs.issue_number }}'
|
||||
});
|
||||
|
||||
const { issueNumber, issueData, employeeComments, prReviews, triggerType, isIssueAuthorEmployee } = result;
|
||||
|
||||
// Check employee status and fail if not authorized
|
||||
if (!result.isIssueAuthorEmployee && '${{ github.event.inputs.override_employee_check }}' !== 'true') {
|
||||
core.setFailed(`User ${result.issueData.user.login} is not a Metabase employee. Skipping bug fix automation.`);
|
||||
}
|
||||
|
||||
core.setOutput('issue_number', issueNumber);
|
||||
core.setOutput('issue_data', JSON.stringify(issueData));
|
||||
core.setOutput('issue_comments', JSON.stringify(employeeComments));
|
||||
core.setOutput('pr_reviews', JSON.stringify(prReviews));
|
||||
core.setOutput('trigger_type', triggerType);
|
||||
} catch (error) {
|
||||
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]"
|
||||
git config --global user.email "noreply@anthropic.com"
|
||||
|
||||
- name: Prepare issue context and bug fix prompt
|
||||
id: prepare_prompt
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
ISSUE_DATA: ${{ steps.get_issue.outputs.issue_data }}
|
||||
ISSUE_COMMENTS: ${{ steps.get_issue.outputs.issue_comments }}
|
||||
PR_REVIEWS: ${{ steps.get_issue.outputs.pr_reviews }}
|
||||
TRIGGER_TYPE: ${{ steps.get_issue.outputs.trigger_type }}
|
||||
with:
|
||||
script: |
|
||||
const issueData = JSON.parse(process.env.ISSUE_DATA);
|
||||
const issueComments = JSON.parse(process.env.ISSUE_COMMENTS);
|
||||
const prReviews = JSON.parse(process.env.PR_REVIEWS);
|
||||
const triggerType = process.env.TRIGGER_TYPE;
|
||||
const issueNumber = '${{ steps.get_issue.outputs.issue_number }}';
|
||||
|
||||
// Convert issue title to kebab case and create branch name
|
||||
const issueTitle = issueData.title || 'untitled';
|
||||
const kebabTitle = issueTitle
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.replace(/-+/g, '-')
|
||||
.substring(0, 50);
|
||||
const branchName = `claude-fix/issue-${issueNumber}-${kebabTitle}`;
|
||||
|
||||
// Format data for bash environment
|
||||
const formattedComments = issueComments.length > 0
|
||||
? issueComments.map(comment =>
|
||||
`**Comment by @${comment.user.login} on ${comment.created_at}:**\n${comment.body}`
|
||||
).join('\n\n---\n\n')
|
||||
: '';
|
||||
const formattedReviews = prReviews.length > 0
|
||||
? prReviews.map(review =>
|
||||
`**Review by @${review.user.login} on ${review.submitted_at} (Changes Requested):**\n${review.body || 'No review comment provided'}`
|
||||
).join('\n\n---\n\n')
|
||||
: '';
|
||||
|
||||
core.exportVariable('BRANCH_NAME', branchName);
|
||||
core.exportVariable('ISSUE_NUMBER', issueNumber);
|
||||
core.exportVariable('ISSUE_TITLE', issueData.title);
|
||||
core.exportVariable('ISSUE_AUTHOR', issueData.user.login);
|
||||
core.exportVariable('ISSUE_CREATED', issueData.created_at);
|
||||
core.exportVariable('ISSUE_BODY', issueData.body || 'No description provided');
|
||||
core.exportVariable('ISSUE_COMMENTS_FORMATTED', formattedComments);
|
||||
core.exportVariable('PR_REVIEWS_FORMATTED', formattedReviews);
|
||||
core.exportVariable('TRIGGER_TYPE', triggerType);
|
||||
|
||||
- name: Generate bug fix prompt
|
||||
run: |
|
||||
cat << 'EOF' >> $GITHUB_ENV
|
||||
BUG_FIX_PROMPT<<PROMPT_END
|
||||
You are helping to fix a bug reported in GitHub issue #${{ env.ISSUE_NUMBER }}.
|
||||
|
||||
## Bug Report Details
|
||||
**Title:** ${{ env.ISSUE_TITLE }}
|
||||
**Reporter:** ${{ env.ISSUE_AUTHOR }}
|
||||
**Created:** ${{ env.ISSUE_CREATED }}
|
||||
|
||||
**Description:**
|
||||
${{ env.ISSUE_BODY }}
|
||||
|
||||
**Comments:**
|
||||
${{ env.ISSUE_COMMENTS_FORMATTED }}
|
||||
EOF
|
||||
|
||||
# Add review feedback section only if this is a review trigger
|
||||
if [ "${{ env.TRIGGER_TYPE }}" = "pull_request_review" ]; then
|
||||
cat << 'EOF' >> $GITHUB_ENV
|
||||
|
||||
## Previous Fix Attempt - Review Feedback (MUST ADDRESS)
|
||||
Your previous automated fix attempt received the following feedback that needs to be addressed:
|
||||
|
||||
${{ env.PR_REVIEWS_FORMATTED }}
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Add the rest of the prompt (always the same)
|
||||
cat << 'EOF' >> $GITHUB_ENV
|
||||
|
||||
## Instructions
|
||||
1. **Analyze the issue description and comments** - Understand what the issue is describing
|
||||
a. Pay particular attention to reproduction steps and if there's an agreed upon fix.
|
||||
2. **Search the codebase** - Use Grep and Glob tools to find relevant files
|
||||
3. **Implement a fix** - Use the appropriate write skill based on the file types:
|
||||
- Use clojure-write for Clojure/ClojureScript files
|
||||
- Use typescript-write for TypeScript/JavaScript files
|
||||
- Follow the coding standards and patterns in each skill
|
||||
4. **Test the fix** - Run appropriate tests as specified in the skills
|
||||
5. **Verify the fix** - Ensure tests pass and code follows conventions
|
||||
|
||||
## Important Guidelines
|
||||
- Use the appropriate write skill for the language you're working in
|
||||
- Write failing tests first, then fix them
|
||||
- Work in small, testable increments
|
||||
- Don't commit changes - leave that for the workflow
|
||||
- IMPORTANT: Focus only on fixing the reported bug, don't make unrelated changes
|
||||
|
||||
## Scratchpad Directory
|
||||
A `.claude-scratchpad/` directory has been created for your temporary files:
|
||||
- Use this directory for ALL temporary verification files. However, prefer using production test files rather than adhoc ones.
|
||||
- Only modify actual production code in the main repository directories
|
||||
- The scratchpad will be cleaned up automatically - nothing from it will be committed
|
||||
|
||||
Start by understanding the bug and then systematically work toward a solution.
|
||||
PROMPT_END
|
||||
EOF
|
||||
|
||||
- name: Determine branch strategy
|
||||
run: |
|
||||
BRANCH_NAME="${{ env.BRANCH_NAME }}"
|
||||
if git branch -r --list "origin/${BRANCH_NAME}" | grep -q .; then
|
||||
echo "Branch already exists: $BRANCH_NAME"
|
||||
echo "Will push to existing branch and skip PR creation"
|
||||
echo "BRANCH_EXISTS=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "Creating new branch: $BRANCH_NAME"
|
||||
echo "Will create new PR"
|
||||
echo "BRANCH_EXISTS=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
# Export the branch name
|
||||
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Create scratchpad directory for Claude
|
||||
run: |
|
||||
# Create a scratchpad directory for Claude to use for temporary files
|
||||
mkdir -p .claude-scratchpad
|
||||
echo "Scratchpad directory created for Claude's temporary verification files"
|
||||
|
||||
- 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 }}
|
||||
|
||||
- name: Generate Bug Fix with Claude Code
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ steps.claude-app-token.outputs.token }}
|
||||
prompt: ${{ env.BUG_FIX_PROMPT }}
|
||||
claude_args: |
|
||||
--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
|
||||
run: |
|
||||
# Remove the scratchpad directory so its contents don't get committed
|
||||
rm -rf .claude-scratchpad/
|
||||
# Remove output file generated by Claude Code action
|
||||
rm -f output.txt
|
||||
echo "Scratchpad directory and action output files cleaned up"
|
||||
|
||||
- name: Check for changes and analysis
|
||||
id: check_changes
|
||||
run: |
|
||||
# Now check for actual code changes (excluding analysis file)
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
echo "Changes detected:"
|
||||
git status --short
|
||||
else
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
echo "No changes made by Claude."
|
||||
fi
|
||||
|
||||
- name: Get issue team labels and prepare reviewers
|
||||
if: steps.check_changes.outputs.has_changes == 'true'
|
||||
id: prepare_reviewers
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
ISSUE_DATA: ${{ steps.get_issue.outputs.issue_data }}
|
||||
with:
|
||||
script: |
|
||||
const issueData = JSON.parse(process.env.ISSUE_DATA);
|
||||
const issueLabels = issueData.labels.map(label => label.name);
|
||||
const teamLabels = issueLabels.filter(label => label.startsWith('.Team/'));
|
||||
const issueCreator = issueData.user.login;
|
||||
core.setOutput('team_labels', teamLabels.join(','));
|
||||
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:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: ${{ env.BRANCH_NAME }}
|
||||
base: master
|
||||
commit-message: |
|
||||
Fix: ${{ env.ISSUE_TITLE }}
|
||||
|
||||
Fixes #${{ env.ISSUE_NUMBER }}
|
||||
|
||||
🤖 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: ${{ env.ISSUE_TITLE }}"
|
||||
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 }}
|
||||
**Issue Title:** ${{ env.ISSUE_TITLE }}
|
||||
|
||||
## Automated Fix
|
||||
🤖 This fix was generated automatically by Claude Code based on the issue description.
|
||||
|
||||
**Please review carefully:**
|
||||
- [ ] The fix addresses the reported issue
|
||||
- [ ] Tests pass and code follows conventions
|
||||
- [ ] No unrelated changes were introduced
|
||||
- [ ] The solution is appropriate and maintainable
|
||||
|
||||
## Next Steps
|
||||
- Review the changes and test thoroughly
|
||||
- If the fix looks good, approve and merge
|
||||
- If issues are found, either fix them or close this PR
|
||||
|
||||
---
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||
labels: |
|
||||
${{ steps.prepare_reviewers.outputs.team_labels }}
|
||||
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'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
console.log('🔄 Updated existing branch: ${{ env.BRANCH_NAME }}');
|
||||
console.log('The changes have been pushed to the existing PR for this issue.');
|
||||
|
||||
- name: Log analysis for no changes
|
||||
if: steps.check_changes.outputs.has_changes == 'false'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
console.log('🤖 No code changes were made by Claude.');
|
||||
98
.github/workflows/claude-code-review.yaml
vendored
Normal file
98
.github/workflows/claude-code-review.yaml
vendored
Normal 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)"
|
||||
103
.github/workflows/claude-docs-review.yml
vendored
Normal file
103
.github/workflows/claude-docs-review.yml
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
name: "Claude Docs Review"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
paths:
|
||||
- 'docs/**/*.md'
|
||||
- 'docs/**/*.mdx'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to review'
|
||||
required: true
|
||||
type: number
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}-claude-docs-review
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.draft == false
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
# Make sure the value of GITHUB_TOKEN will not be persisted in repo's config
|
||||
persist-credentials: false
|
||||
|
||||
- name: Generate GitHub App Token for Claude
|
||||
uses: actions/create-github-app-token@v1
|
||||
id: claude-app-token
|
||||
with:
|
||||
app-id: ${{ secrets.METABASE_BOT_APP_ID }}
|
||||
private-key: ${{ secrets.METABASE_BOT_APP_PRIVATE_KEY }}
|
||||
permission-contents: read # Required for reading repository files and PR diffs
|
||||
permission-pull-requests: write # Required for creating inline comments and PR comments
|
||||
|
||||
- name: Delete old Claude docs review comments
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.claude-app-token.outputs.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }}
|
||||
shell: bash
|
||||
run: |
|
||||
# Delete pending reviews first (these can be deleted entirely)
|
||||
pending_review_ids=$(gh api "/repos/$REPO/pulls/$PR_NUMBER/reviews" --jq '.[] | select(.user.login=="metabase-bot[bot]" and .state=="PENDING") | .id')
|
||||
if [ -n "$pending_review_ids" ]; then
|
||||
echo "Deleting pending reviews: $pending_review_ids"
|
||||
for id in $pending_review_ids; do
|
||||
gh api -X DELETE "/repos/$REPO/pulls/$PR_NUMBER/reviews/$id"
|
||||
echo "Deleted pending review $id"
|
||||
done
|
||||
fi
|
||||
|
||||
# Delete individual inline comments from submitted reviews (can't delete the review itself)
|
||||
old_line_comment_ids=$(gh api "/repos/$REPO/pulls/$PR_NUMBER/comments" --jq '.[] | select(.user.login=="metabase-bot[bot]") | .id')
|
||||
if [ -n "$old_line_comment_ids" ]; then
|
||||
echo "Deleting line comments: $old_line_comment_ids"
|
||||
for id in $old_line_comment_ids; do
|
||||
gh api -X DELETE "/repos/$REPO/pulls/comments/$id"
|
||||
echo "Deleted line comment $id"
|
||||
done
|
||||
fi
|
||||
|
||||
# Delete top-level review comments
|
||||
old_top_level_comment_ids=$(gh api "/repos/$REPO/issues/$PR_NUMBER/comments" --jq '.[] | select(.user.login=="metabase-bot[bot]") | .id')
|
||||
if [ -n "$old_top_level_comment_ids" ]; then
|
||||
echo "Deleting top-level comments: $old_top_level_comment_ids"
|
||||
for id in $old_top_level_comment_ids; do
|
||||
gh api -X DELETE "/repos/$REPO/issues/comments/$id"
|
||||
echo "Deleted top-level comment $id"
|
||||
done
|
||||
fi
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
timeout-minutes: 10
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ steps.claude-app-token.outputs.token }}
|
||||
show_full_output: true
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }}
|
||||
|
||||
Review the documentation changes in this pull request.
|
||||
|
||||
Use the docs-review skill to review all changed documentation files for compliance with the Metabase writing style guide.
|
||||
|
||||
IMPORTANT: Only review markdown files (.md and .mdx). Skip any .txt files or other non-markdown files.
|
||||
|
||||
DO NOT post any comments confirming that something is correct - only post comments for style violations or concerns.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "Skill,mcp__github__get_pull_request,mcp__github__get_pull_request_files,mcp__github__create_pending_pull_request_review,mcp__github__get_pull_request_diff,mcp__github__add_comment_to_pending_review,mcp__github__add_pull_request_review_comment_to_pending_review,mcp__github__delete_pending_pull_request_review,mcp__github__submit_pending_pull_request_review"
|
||||
6
.github/workflows/cljs.yml
vendored
6
.github/workflows/cljs.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Check which files changed
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
cljs: ${{ steps.changes.outputs.cljs }}
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
shared-tests-cljs:
|
||||
needs: files-changed
|
||||
if: needs.files-changed.outputs.cljs == 'true'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -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
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Check which files changed
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
codeql: ${{ steps.changes.outputs.codeql }}
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
analyze:
|
||||
needs: files-changed
|
||||
if: needs.files-changed.outputs.codeql == 'true'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
41
.github/workflows/codespell.yml
vendored
41
.github/workflows/codespell.yml
vendored
@@ -8,8 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
codespell:
|
||||
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -44,35 +43,27 @@ jobs:
|
||||
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
|
||||
prompt: |
|
||||
You are tasked with fixing spelling errors detected by codespell in this repository.
|
||||
|
||||
## Codespell Output
|
||||
The following spelling errors were detected:
|
||||
```
|
||||
$(cat codespell_output.txt)
|
||||
```
|
||||
|
||||
## Instructions
|
||||
1. **Review the codespell output and identify reported spelling errors** - The usual format codespell uses is `typo ==> correction`
|
||||
2. **Fix every spelling error** - Use the Edit tool to fix each misspelling with the suggested correction
|
||||
- If codespell suggests multiple corrections, choose the most appropriate one based on context
|
||||
|
||||
Human reviewers will verify the changes before merging, so fix all errors reported by codespell.
|
||||
claude_args: |
|
||||
--allowedTools "Write,Read,Glob,Grep,Edit,MultiEdit,Bash(cat*),Bash(ls*)"
|
||||
--disallowedTools "Bash(git commit*),Bash(git push*),Bash(gh*)"
|
||||
|
||||
37
.github/workflows/containerize-jar.yml
vendored
37
.github/workflows/containerize-jar.yml
vendored
@@ -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,17 +32,10 @@ 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:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
outputs:
|
||||
platforms: ${{ steps.platforms.outputs.result }}
|
||||
@@ -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
|
||||
@@ -117,7 +102,7 @@ jobs:
|
||||
bin/docker/.
|
||||
|
||||
verify-docker-pull:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: containerize
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
|
||||
17
.github/workflows/containerize-uberjar.yml
vendored
17
.github/workflows/containerize-uberjar.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
|
||||
jobs:
|
||||
get-container-info:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 5
|
||||
name: Get container info
|
||||
outputs:
|
||||
@@ -55,28 +55,28 @@ 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]
|
||||
strategy:
|
||||
matrix:
|
||||
edition: ${{ fromJson(needs.get-container-info.outputs.editions) }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
name: Run Trivy vulnerability scanner
|
||||
steps:
|
||||
- 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@57a97c7e7821a5776cebc9bb87c984fa69cba8f1
|
||||
uses: aquasecurity/trivy-action@0.29.0
|
||||
env:
|
||||
TRIVY_OFFLINE_SCAN: true
|
||||
repo: ${{ fromJson(needs.get-container-info.outputs[matrix.edition]).repo }}
|
||||
@@ -85,6 +85,7 @@ jobs:
|
||||
image-ref: docker.io/${{ env.repo }}:${{ env.tag }}
|
||||
format: sarif
|
||||
output: trivy-results.sarif
|
||||
version: "v0.57.1"
|
||||
- name: Upload Trivy scan results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
@@ -93,7 +94,7 @@ jobs:
|
||||
post-comment:
|
||||
needs: [get-container-info, containerize]
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
name: Post PR comment
|
||||
steps:
|
||||
- name: Check out the code
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Check which files changed
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
frontend_embedding_sources: ${{ steps.changes.outputs.frontend_embedding_sources }}
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
filters: .github/file-paths.yaml
|
||||
|
||||
pr-title-reminder:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
5
.github/workflows/coverage.yml
vendored
5
.github/workflows/coverage.yml
vendored
@@ -7,8 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
fe-test-coverage:
|
||||
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -18,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
|
||||
|
||||
65
.github/workflows/cross-branch-migrations.yml
vendored
65
.github/workflows/cross-branch-migrations.yml
vendored
@@ -25,7 +25,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
migrations-h2-ee:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -38,24 +38,24 @@ jobs:
|
||||
base-ref: ${{ env.BASE_REF }}
|
||||
head-ref: ${{ env.HEAD_REF }}
|
||||
|
||||
migrations-postgres-14-ee:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
migrations-postgres-12-ee:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
CI: 'true'
|
||||
MB_DB_TYPE: postgres
|
||||
MB_DB_PORT: 5432
|
||||
MB_DB_HOST: localhost
|
||||
MB_DB_DBNAME: mb_test
|
||||
MB_DB_USER: mb_test
|
||||
MB_DB_DBNAME: circle_test
|
||||
MB_DB_USER: circle_test
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14-alpine
|
||||
image: postgres:12-alpine
|
||||
ports:
|
||||
- "5432:5432"
|
||||
env:
|
||||
POSTGRES_USER: mb_test
|
||||
POSTGRES_DB: mb_test
|
||||
POSTGRES_USER: circle_test
|
||||
POSTGRES_DB: circle_test
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -65,23 +65,23 @@ jobs:
|
||||
head-ref: ${{ env.HEAD_REF }}
|
||||
|
||||
migrations-postgres-latest-ee:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
CI: 'true'
|
||||
MB_DB_TYPE: postgres
|
||||
MB_DB_PORT: 5432
|
||||
MB_DB_HOST: localhost
|
||||
MB_DB_DBNAME: mb_test
|
||||
MB_DB_USER: mb_test
|
||||
MB_DB_DBNAME: circle_test
|
||||
MB_DB_USER: circle_test
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
image: circleci/postgres:latest
|
||||
ports:
|
||||
- "5432:5432"
|
||||
env:
|
||||
POSTGRES_USER: mb_test
|
||||
POSTGRES_DB: mb_test
|
||||
POSTGRES_USER: circle_test
|
||||
POSTGRES_DB: circle_test
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -90,24 +90,21 @@ jobs:
|
||||
base-ref: ${{ env.BASE_REF }}
|
||||
head-ref: ${{ env.HEAD_REF }}
|
||||
|
||||
migrations-mysql-8-4-ee:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
migrations-mysql-8-0-ee:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
CI: 'true'
|
||||
MB_DB_TYPE: mysql
|
||||
MB_DB_HOST: localhost
|
||||
MB_DB_PORT: 3306
|
||||
MB_DB_DBNAME: mb_test
|
||||
MB_DB_DBNAME: circle_test
|
||||
MB_DB_USER: root
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.4
|
||||
image: cimg/mysql:8.0
|
||||
ports:
|
||||
- "3306:3306"
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: true
|
||||
MYSQL_DATABASE: mb_test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/cross-branch-migration-test
|
||||
@@ -116,14 +113,14 @@ jobs:
|
||||
head-ref: ${{ env.HEAD_REF }}
|
||||
|
||||
migrations-mysql-latest-ee:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
CI: 'true'
|
||||
MB_DB_TYPE: mysql
|
||||
MB_DB_HOST: localhost
|
||||
MB_DB_PORT: 3306
|
||||
MB_DB_DBNAME: mb_test
|
||||
MB_DB_DBNAME: circle_test
|
||||
MB_DB_USER: root
|
||||
MB_MYSQL_TEST_USER: root
|
||||
services:
|
||||
@@ -131,7 +128,7 @@ jobs:
|
||||
image: mysql:latest
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: true
|
||||
MYSQL_DATABASE: mb_test
|
||||
MYSQL_DATABASE: circle_test
|
||||
ports:
|
||||
- "3306:3306"
|
||||
steps:
|
||||
@@ -141,24 +138,21 @@ jobs:
|
||||
base-ref: ${{ env.BASE_REF }}
|
||||
head-ref: ${{ env.HEAD_REF }}
|
||||
|
||||
migrations-mariadb-10-6-ee:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
migrations-mariadb-10-2-ee:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
CI: 'true'
|
||||
MB_DB_TYPE: mysql
|
||||
MB_DB_HOST: localhost
|
||||
MB_DB_PORT: 3306
|
||||
MB_DB_DBNAME: mb_test
|
||||
MB_DB_DBNAME: circle_test
|
||||
MB_DB_USER: root
|
||||
services:
|
||||
mariadb:
|
||||
image: metabase/mariadb:10.6 # https://github.com/metabase/metabase-mariadb-docker-image
|
||||
image: circleci/mariadb:10.2.23
|
||||
ports:
|
||||
- "3306:3306"
|
||||
env:
|
||||
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: true
|
||||
MARIADB_DATABASE: mb_test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/cross-branch-migration-test
|
||||
@@ -167,23 +161,20 @@ jobs:
|
||||
head-ref: ${{ env.HEAD_REF }}
|
||||
|
||||
migrations-mariadb-latest-ee:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
CI: 'true'
|
||||
MB_DB_TYPE: mysql
|
||||
MB_DB_HOST: localhost
|
||||
MB_DB_PORT: 3306
|
||||
MB_DB_DBNAME: mb_test
|
||||
MB_DB_DBNAME: circle_test
|
||||
MB_DB_USER: root
|
||||
services:
|
||||
mariadb:
|
||||
image: mariadb:latest
|
||||
image: circleci/mariadb:latest
|
||||
ports:
|
||||
- "3306:3306"
|
||||
env:
|
||||
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: true
|
||||
MARIADB_DATABASE: mb_test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/cross-branch-migration-test
|
||||
|
||||
225
.github/workflows/cross-version.yml
vendored
225
.github/workflows/cross-version.yml
vendored
@@ -1,225 +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' && github.repository == 'metabase/metabase'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
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' && github.repository == 'metabase/metabase'
|
||||
needs: generate-matrix
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
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: Prepare front-end environment
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
|
||||
- name: Prepare Cypress environment
|
||||
uses: ./.github/actions/prepare-cypress
|
||||
|
||||
- name: Run cross-version test
|
||||
env:
|
||||
CURRENT_VERSION: ${{ vars.CURRENT_VERSION }}
|
||||
RESULT_FILE: ${{ runner.temp }}/cross-version-result-${{ matrix.source }}-${{ matrix.target }}.json
|
||||
run: |
|
||||
cd cross-version
|
||||
./test.sh --source "${{ matrix.source }}" --target "${{ matrix.target }}"
|
||||
|
||||
- name: Upload failure result
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: failure-result-${{ matrix.source }}-${{ matrix.target }}
|
||||
path: ${{ runner.temp }}/cross-version-result-${{ matrix.source }}-${{ matrix.target }}.json
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Upload Cypress Artifacts upon failure
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: cypress-recording-${{ matrix.source }}-${{ matrix.target }}
|
||||
path: |
|
||||
./cypress
|
||||
./logs/test.log
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Show logs on failure
|
||||
if: failure()
|
||||
run: |
|
||||
cd cross-version
|
||||
docker compose logs metabase || true
|
||||
docker compose logs postgres || true
|
||||
|
||||
# Notify Slack on nightly failures
|
||||
notify-on-failure:
|
||||
if: >-
|
||||
always()
|
||||
&& github.event_name == 'schedule'
|
||||
&& github.repository == 'metabase/metabase'
|
||||
&& needs.test-matrix.result == 'failure'
|
||||
needs: [test-matrix]
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
release
|
||||
.github
|
||||
|
||||
- name: Prepare release scripts
|
||||
uses: ./.github/actions/prepare-release-scripts
|
||||
|
||||
- name: Download failure results
|
||||
uses: actions/download-artifact@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
pattern: failure-result-*
|
||||
path: failure-results
|
||||
merge-multiple: true
|
||||
|
||||
- name: Notify Slack
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { buildFailureResults, sendCrossVersionSlackNotification } = require('${{ github.workspace }}/release/dist/index.cjs');
|
||||
|
||||
// Get all failed jobs from the GitHub API
|
||||
const { owner, repo } = context.repo;
|
||||
const jobInfo = await github.rest.actions.listJobsForWorkflowRun({
|
||||
owner,
|
||||
repo,
|
||||
run_id: context.runId,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const failedJobs = jobInfo.data.jobs
|
||||
.filter(job => job.status === 'completed' && job.conclusion === 'failure' && job.name.startsWith('test-matrix'))
|
||||
.map(job => ({ name: job.name, url: job.html_url }));
|
||||
|
||||
if (failedJobs.length === 0) {
|
||||
console.log('No failed test-matrix jobs found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Load artifact data for enrichment (phase)
|
||||
const resultsDir = 'failure-results';
|
||||
const artifactData = new Map();
|
||||
if (fs.existsSync(resultsDir)) {
|
||||
for (const file of fs.readdirSync(resultsDir)) {
|
||||
if (!file.endsWith('.json')) continue;
|
||||
try {
|
||||
const result = JSON.parse(fs.readFileSync(path.join(resultsDir, file), 'utf8'));
|
||||
artifactData.set(`${result.source}-${result.target}`, result);
|
||||
} catch (e) {
|
||||
console.log(`Skipping malformed result: ${file}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const failures = buildFailureResults(failedJobs, artifactData);
|
||||
const runUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${context.runId}`;
|
||||
const slackBotToken = process.env.SLACK_BOT_TOKEN;
|
||||
if (!slackBotToken) {
|
||||
console.log('SLACK_BOT_TOKEN is not set; skipping Slack notification.');
|
||||
return;
|
||||
}
|
||||
await sendCrossVersionSlackNotification(failures, runUrl, slackBotToken);
|
||||
console.log('Slack notification sent');
|
||||
|
||||
# Manual test for workflow_dispatch
|
||||
test-manual:
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Prepare release scripts
|
||||
uses: ./.github/actions/prepare-release-scripts
|
||||
|
||||
- name: Prepare front-end environment
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
|
||||
- name: Prepare Cypress environment
|
||||
uses: ./.github/actions/prepare-cypress
|
||||
|
||||
- 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: Upload Cypress Artifacts upon failure
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: cypress-recording-${{ inputs.source_version }}-${{ inputs.target_version }}
|
||||
path: |
|
||||
./cypress
|
||||
./logs/test.log
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Show logs on failure
|
||||
if: failure()
|
||||
run: |
|
||||
cd cross-version
|
||||
docker compose logs metabase || true
|
||||
docker compose logs postgres || true
|
||||
@@ -1,80 +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:
|
||||
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
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');
|
||||
2
.github/workflows/docs-generate-base.yml
vendored
2
.github/workflows/docs-generate-base.yml
vendored
@@ -21,7 +21,7 @@ permissions:
|
||||
jobs:
|
||||
update-docs:
|
||||
name: Update documentation for ${{ inputs.target_branch }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
2
.github/workflows/docs-links.yml
vendored
2
.github/workflows/docs-links.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
jobs:
|
||||
link-checker:
|
||||
name: Check documentation links
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
3
.github/workflows/docs-master-generate.yml
vendored
3
.github/workflows/docs-master-generate.yml
vendored
@@ -2,12 +2,11 @@ name: Auto-update documentation (master)
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 21 * * 1-5" # Every Monday through Friday at 9 PM UTC
|
||||
- cron: "0 1 * * 1-5" # Every Monday through Friday at 1 AM UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update-master-docs:
|
||||
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
|
||||
name: Call documentation update for master
|
||||
uses: ./.github/workflows/docs-generate-base.yml
|
||||
with:
|
||||
|
||||
7
.github/workflows/docs-release-generate.yml
vendored
7
.github/workflows/docs-release-generate.yml
vendored
@@ -2,21 +2,20 @@ name: Auto-update documentation (release)
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 22 * * 1-5" # Every Monday through Friday at 10 PM UTC
|
||||
- cron: "0 2 * * 1-5" # Every Monday through Friday at 2 AM UTC
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target_branch:
|
||||
description: "Target branch for documentation update"
|
||||
required: false
|
||||
default: "release-x.59.x"
|
||||
default: "release-x.58.x"
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
update-release-docs:
|
||||
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
|
||||
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.58.x' }}
|
||||
secrets:
|
||||
automation_token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
|
||||
105
.github/workflows/drivers-stress-test.yml
vendored
105
.github/workflows/drivers-stress-test.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
be-tests-athena-ee:
|
||||
if: inputs.driver == 'Athena'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
|
||||
be-tests-bigquery-cloud-sdk-ee:
|
||||
if: inputs.driver == 'BigQuery'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
|
||||
be-tests-druid-ee:
|
||||
if: inputs.driver == 'Druid'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -109,9 +109,9 @@ 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: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -119,28 +119,25 @@ jobs:
|
||||
MB_DB_TYPE: mysql
|
||||
MB_DB_HOST: localhost
|
||||
MB_DB_PORT: 3306
|
||||
MB_DB_DBNAME: mb_test
|
||||
MB_DB_DBNAME: circle_test
|
||||
MB_DB_USER: root
|
||||
MB_MYSQL_TEST_USER: root
|
||||
services:
|
||||
mariadb:
|
||||
image: metabase/mariadb:10.6 # https://github.com/metabase/metabase-mariadb-docker-image
|
||||
image: circleci/mariadb:10.2.23
|
||||
ports:
|
||||
- "3306:3306"
|
||||
env:
|
||||
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: true
|
||||
MARIADB_DATABASE: mb_test
|
||||
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:
|
||||
if: inputs.driver == 'Maria DB (latest)'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -148,17 +145,14 @@ jobs:
|
||||
MB_DB_TYPE: mysql
|
||||
MB_DB_HOST: localhost
|
||||
MB_DB_PORT: 3306
|
||||
MB_DB_DBNAME: mb_test
|
||||
MB_DB_DBNAME: circle_test
|
||||
MB_DB_USER: root
|
||||
MB_MYSQL_TEST_USER: root
|
||||
services:
|
||||
mariadb:
|
||||
image: mariadb:latest
|
||||
image: circleci/mariadb:latest
|
||||
ports:
|
||||
- "3306:3306"
|
||||
env:
|
||||
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: true
|
||||
MARIADB_DATABASE: mb_test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test MariaDB driver (latest)
|
||||
@@ -169,7 +163,7 @@ jobs:
|
||||
|
||||
be-tests-mongo-4-4-ee:
|
||||
if: inputs.driver == 'Mongo 4'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -191,7 +185,7 @@ jobs:
|
||||
|
||||
be-tests-mongo-4-4-ssl-ee:
|
||||
if: inputs.driver == 'Mongo 4 (SSL)'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -225,7 +219,7 @@ jobs:
|
||||
|
||||
be-tests-mongo-5-0-ee:
|
||||
if: inputs.driver == 'Mongo 5'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -247,7 +241,7 @@ jobs:
|
||||
|
||||
be-tests-mongo-5-0-ssl-ee:
|
||||
if: inputs.driver == 'Mongo 5 (SSL)'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -281,7 +275,7 @@ jobs:
|
||||
|
||||
be-tests-mongo-latest-ee:
|
||||
if: inputs.driver == 'Mongo (latest)'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -290,7 +284,7 @@ jobs:
|
||||
MB_MONGO_TEST_PASSWORD: metasample123
|
||||
services:
|
||||
mongodb:
|
||||
image: mongo:latest
|
||||
image: circleci/mongo:latest
|
||||
ports:
|
||||
- "27017:27017"
|
||||
env:
|
||||
@@ -304,9 +298,9 @@ 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: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -314,28 +308,25 @@ jobs:
|
||||
MB_DB_TYPE: mysql
|
||||
MB_DB_HOST: localhost
|
||||
MB_DB_PORT: 3306
|
||||
MB_DB_DBNAME: mb_test
|
||||
MB_DB_DBNAME: circle_test
|
||||
MB_DB_USER: root
|
||||
MB_MYSQL_TEST_USER: root
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.4
|
||||
image: cimg/mysql:8.0
|
||||
ports:
|
||||
- "3306:3306"
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: true
|
||||
MYSQL_DATABASE: mb_test
|
||||
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:
|
||||
if: inputs.driver == 'MySQL (latest)'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -343,7 +334,7 @@ jobs:
|
||||
MB_DB_TYPE: mysql
|
||||
MB_DB_HOST: localhost
|
||||
MB_DB_PORT: 3306
|
||||
MB_DB_DBNAME: mb_test
|
||||
MB_DB_DBNAME: circle_test
|
||||
MB_DB_USER: root
|
||||
MB_MYSQL_TEST_USER: root
|
||||
# set up env vars for something named "MYSQL_SSL" to run MySQL SSL tests verifying connectivity with PEM cert
|
||||
@@ -373,7 +364,7 @@ jobs:
|
||||
|
||||
be-tests-oracle-18-4-ee:
|
||||
if: inputs.driver == 'Oracle 18'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -399,7 +390,7 @@ jobs:
|
||||
|
||||
be-tests-oracle-21-3-ee:
|
||||
if: inputs.driver == 'Oracle 21'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -434,7 +425,7 @@ jobs:
|
||||
|
||||
be-tests-postgres-ee:
|
||||
if: inputs.driver == 'Postgres'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -447,7 +438,7 @@ jobs:
|
||||
MB_POSTGRESQL_TEST_USER: mb_test
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14-alpine
|
||||
image: postgres:12-alpine
|
||||
ports:
|
||||
- "5432:5432"
|
||||
env:
|
||||
@@ -456,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'
|
||||
@@ -464,7 +455,7 @@ jobs:
|
||||
|
||||
be-tests-postgres-latest-ee:
|
||||
if: inputs.driver == 'Postgres (latest)'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -472,20 +463,20 @@ jobs:
|
||||
MB_DB_TYPE: postgres
|
||||
MB_DB_PORT: 5432
|
||||
MB_DB_HOST: localhost
|
||||
MB_DB_DBNAME: mb_test
|
||||
MB_DB_USER: mb_test
|
||||
MB_POSTGRESQL_TEST_USER: mb_test
|
||||
MB_DB_DBNAME: circle_test
|
||||
MB_DB_USER: circle_test
|
||||
MB_POSTGRESQL_TEST_USER: circle_test
|
||||
MB_POSTGRES_SSL_TEST_SSL: true
|
||||
MB_POSTGRES_SSL_TEST_SSL_MODE: verify-full
|
||||
MB_POSTGRES_SSL_TEST_SSL_ROOT_CERT_PATH: 'test-resources/certificates/us-east-2-bundle.pem'
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
image: circleci/postgres:latest
|
||||
ports:
|
||||
- "5432:5432"
|
||||
env:
|
||||
POSTGRES_USER: mb_test
|
||||
POSTGRES_DB: mb_test
|
||||
POSTGRES_USER: circle_test
|
||||
POSTGRES_DB: circle_test
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -497,7 +488,7 @@ jobs:
|
||||
|
||||
be-tests-presto-jdbc-ee:
|
||||
if: inputs.driver == 'Presto JDBC'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -546,7 +537,7 @@ jobs:
|
||||
|
||||
be-tests-redshift-ee:
|
||||
if: inputs.driver == 'Redshift'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -565,7 +556,7 @@ jobs:
|
||||
|
||||
be-tests-snowflake-ee:
|
||||
if: inputs.driver == 'Snowflake'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -586,7 +577,7 @@ jobs:
|
||||
|
||||
be-tests-sparksql-ee:
|
||||
if: inputs.driver == 'SparkSQL'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -606,7 +597,7 @@ jobs:
|
||||
|
||||
be-tests-sqlite-ee:
|
||||
if: inputs.driver == 'SQLite'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -621,7 +612,7 @@ jobs:
|
||||
|
||||
be-tests-sqlserver-2017-ee:
|
||||
if: inputs.driver == 'SQL Server 2017'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -648,7 +639,7 @@ jobs:
|
||||
|
||||
be-tests-sqlserver-2022-ee:
|
||||
if: inputs.driver == 'SQL Server 2022'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -675,7 +666,7 @@ jobs:
|
||||
|
||||
be-tests-starburst-ee:
|
||||
if: inputs.driver == 'Starburst'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
@@ -700,7 +691,7 @@ jobs:
|
||||
|
||||
login-to-ecr:
|
||||
if: inputs.driver == 'Vertica'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -729,7 +720,7 @@ jobs:
|
||||
|
||||
be-tests-vertica-ee:
|
||||
if: inputs.driver == 'Vertica'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
needs: login-to-ecr
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
|
||||
214
.github/workflows/drivers.yml
vendored
214
.github/workflows/drivers.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
# This job determines which driver tests should run based on PR context.
|
||||
# All decision logic is consolidated in mage driver-decisions.
|
||||
determine-driver-skips:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
# All driver run decisions (from mage driver-decisions)
|
||||
h2-should-run: ${{ steps.driver-decisions.outputs.h2-should-run }}
|
||||
@@ -72,28 +72,21 @@ jobs:
|
||||
# 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 }}"
|
||||
--git-ref="${{ github.event.pull_request.base.ref || 'master' }}"
|
||||
--is-master-or-release="${{ github.ref_name == 'master' || startsWith(github.ref_name, 'release-') }}"
|
||||
--pr-labels="${{ join(github.event.pull_request.labels.*.name, ',') }}"
|
||||
--skip="${{ inputs.skip }}"
|
||||
)
|
||||
echo "Running:"
|
||||
echo "./bin/mage -driver-decisions ${MAGE_ARGS[@]}"
|
||||
echo ""
|
||||
# First call: show human-readable output for debugging
|
||||
./bin/mage -driver-decisions "${MAGE_ARGS[@]}"
|
||||
echo ""
|
||||
echo "Want to force-run a specific driver? Add a ci:run-<driver> or ci:run-all-drivers label to your PR."
|
||||
echo "Run './bin/mage -driver-decisions -h' locally for the full list of drivers and labels."
|
||||
# Second call: output only key=value lines for GITHUB_OUTPUT
|
||||
./bin/mage -driver-decisions "${MAGE_ARGS[@]}" --github-output-only >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
be-tests-athena-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ needs.determine-driver-skips.outputs.athena-should-run == 'true' || needs.determine-driver-skips.outputs.athena-quarantine-conflict == 'true' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 40
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CI: 'true'
|
||||
DRIVERS: athena
|
||||
@@ -138,12 +131,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' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -154,10 +145,11 @@ jobs:
|
||||
: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
|
||||
@@ -204,7 +196,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
|
||||
@@ -214,9 +205,8 @@ 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' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 40
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -287,7 +277,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
|
||||
@@ -297,9 +286,8 @@ 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' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 40
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CI: 'true'
|
||||
DRIVERS: druid
|
||||
@@ -342,13 +330,11 @@ jobs:
|
||||
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
|
||||
|
||||
be-tests-druid-jdbc-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ needs.determine-driver-skips.outputs.druid-jdbc-should-run == 'true' || needs.determine-driver-skips.outputs.druid-jdbc-quarantine-conflict == 'true' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 40
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CI: 'true'
|
||||
DRIVERS: druid-jdbc
|
||||
@@ -391,13 +377,11 @@ jobs:
|
||||
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
|
||||
|
||||
be-tests-databricks-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ needs.determine-driver-skips.outputs.databricks-should-run == 'true' || needs.determine-driver-skips.outputs.databricks-quarantine-conflict == 'true' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 40
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CI: 'true'
|
||||
DRIVERS: databricks
|
||||
@@ -440,13 +424,11 @@ jobs:
|
||||
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
|
||||
|
||||
be-tests-databricks-multi-catalog-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ needs.determine-driver-skips.outputs.databricks-should-run == 'true' || needs.determine-driver-skips.outputs.databricks-quarantine-conflict == 'true' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 40
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CI: 'true'
|
||||
DRIVERS: databricks
|
||||
@@ -490,13 +472,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' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 40
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CI: 'true'
|
||||
name: H2
|
||||
@@ -521,64 +501,30 @@ 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: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
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' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 40
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version:
|
||||
- name: MariaDB 10.6
|
||||
junit-name: be-tests-mariadb-10-6-ee
|
||||
image: metabase/mariadb:10.6 # https://github.com/metabase/metabase-mariadb-docker-image
|
||||
- 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'
|
||||
- name: MariaDB Latest
|
||||
junit-name: be-tests-mariadb-latest-ee
|
||||
image: mariadb:latest
|
||||
image: circleci/mariadb:latest
|
||||
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'
|
||||
@@ -605,7 +551,7 @@ jobs:
|
||||
- "3306:3306"
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: true
|
||||
MYSQL_DATABASE: mb_test
|
||||
MYSQL_DATABASE: circle_test
|
||||
env:
|
||||
# Only run old migrations tests on pushes to master or release branches. All other branches should skip tests
|
||||
# with the tag `mb/old-migrations-test`. `__ADDITIONAL_EXCLUDED_TAG__` is not used anywhere outside of splicing
|
||||
@@ -628,7 +574,7 @@ jobs:
|
||||
MB_DB_TYPE: mysql
|
||||
MB_DB_HOST: localhost
|
||||
MB_DB_PORT: 3306
|
||||
MB_DB_DBNAME: mb_test
|
||||
MB_DB_DBNAME: circle_test
|
||||
MB_DB_USER: root
|
||||
MB_MYSQL_TEST_USER: root
|
||||
#
|
||||
@@ -684,7 +630,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
|
||||
@@ -694,9 +639,8 @@ 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' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 40
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -765,7 +709,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
|
||||
@@ -775,9 +718,8 @@ 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' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 40
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -848,13 +790,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' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 40
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CI: 'true'
|
||||
DRIVERS: mongo
|
||||
@@ -894,13 +835,11 @@ jobs:
|
||||
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
|
||||
|
||||
be-tests-oracle:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ needs.determine-driver-skips.outputs.oracle-should-run == 'true' || needs.determine-driver-skips.outputs.oracle-quarantine-conflict == 'true' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 40
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -980,20 +919,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:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ needs.determine-driver-skips.outputs.postgres-should-run == 'true' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 40
|
||||
runs-on: ubuntu-latest
|
||||
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'
|
||||
@@ -1070,7 +1007,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__ }}]'
|
||||
@@ -1085,7 +1021,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
|
||||
@@ -1095,9 +1030,8 @@ 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' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 40
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CI: 'true'
|
||||
DRIVERS: presto-jdbc
|
||||
@@ -1173,13 +1107,11 @@ jobs:
|
||||
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
|
||||
|
||||
be-tests-redshift-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ needs.determine-driver-skips.outputs.redshift-should-run == 'true' || needs.determine-driver-skips.outputs.redshift-quarantine-conflict == 'true' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 40
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -1258,7 +1190,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
|
||||
@@ -1268,8 +1199,7 @@ jobs:
|
||||
|
||||
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' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -1280,10 +1210,12 @@ jobs:
|
||||
: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
|
||||
@@ -1339,7 +1271,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
|
||||
@@ -1349,9 +1280,8 @@ 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' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 40
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CI: 'true'
|
||||
DRIVERS: sparksql
|
||||
@@ -1392,13 +1322,11 @@ jobs:
|
||||
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
|
||||
|
||||
be-tests-sqlite-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ needs.determine-driver-skips.outputs.sqlite-should-run == 'true' || needs.determine-driver-skips.outputs.sqlite-quarantine-conflict == 'true' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 40
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CI: 'true'
|
||||
DRIVERS: sqlite
|
||||
@@ -1434,12 +1362,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' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -1511,7 +1437,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
|
||||
@@ -1523,8 +1448,8 @@ jobs:
|
||||
# be-tests-starburst-ee:
|
||||
# needs: determine-driver-skips
|
||||
# if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
# runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
# timeout-minutes: 40
|
||||
# runs-on: ubuntu-latest
|
||||
# timeout-minutes: 60
|
||||
# env:
|
||||
# CI: 'true'
|
||||
# DRIVERS: starburst
|
||||
@@ -1560,8 +1485,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' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -1590,9 +1514,8 @@ 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' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 40
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CI: 'true'
|
||||
DRIVERS: vertica
|
||||
@@ -1638,7 +1561,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:
|
||||
@@ -1665,7 +1587,7 @@ jobs:
|
||||
# TODO(rileythomp, 2025-08-20): Re-enable vertica tests once the docker image is updated
|
||||
# - be-tests-vertica-ee
|
||||
- be-tests-clickhouse-ee
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
name: drivers-tests-result
|
||||
if: always() && !cancelled()
|
||||
|
||||
4
.github/workflows/e2e-matrix-builder.yml
vendored
4
.github/workflows/e2e-matrix-builder.yml
vendored
@@ -21,7 +21,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build-matrix:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
matrix: ${{ steps.matrix.outputs.config }}
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
const inputSpecs = "${{ inputs.specs }}";
|
||||
const inputChunks = Number(process.env.chunks);
|
||||
const java = 21;
|
||||
const defaultRunner = "${{ vars.DEFAULT_RUNNER_KEY }}";
|
||||
const defaultRunner = "ubuntu-22.04";
|
||||
const options = {
|
||||
java,
|
||||
defaultRunner,
|
||||
|
||||
17
.github/workflows/e2e-stress-test-flake-fix.yml
vendored
17
.github/workflows/e2e-stress-test-flake-fix.yml
vendored
@@ -31,16 +31,11 @@ on:
|
||||
type: boolean
|
||||
required: false
|
||||
default: true
|
||||
fail_fast:
|
||||
description: "Stop running remaining tests after the first failure"
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
determine-test-type:
|
||||
name: Determine test type
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
type: ${{ steps.detect.outputs.type }}
|
||||
@@ -58,7 +53,7 @@ jobs:
|
||||
workflow-summary:
|
||||
name: Stress test inputs
|
||||
needs: [determine-test-type]
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Generate workflow summary
|
||||
@@ -72,13 +67,12 @@ jobs:
|
||||
echo '- `grep`: "${{ inputs.grep }}"' >> $GITHUB_STEP_SUMMARY
|
||||
echo '- `qa_db`: "${{ inputs.qa_db }}"' >> $GITHUB_STEP_SUMMARY
|
||||
echo '- `enable_network_throttling`: "${{ inputs.enable_network_throttling }}"' >> $GITHUB_STEP_SUMMARY
|
||||
echo '- `fail_fast`: "${{ inputs.fail_fast }}"' >> $GITHUB_STEP_SUMMARY
|
||||
echo '' >> $GITHUB_STEP_SUMMARY
|
||||
echo 'triggered by: @${{ github.event.sender.login }}' >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
stress-test-flake-fix:
|
||||
needs: [determine-test-type]
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 60
|
||||
name: Stress test E2E flake fix
|
||||
env:
|
||||
@@ -121,14 +115,14 @@ jobs:
|
||||
|
||||
- name: Build Embedding SDK package
|
||||
if: ${{ needs.determine-test-type.outputs.type == 'component' }}
|
||||
run: bun run build-embedding-sdk-package
|
||||
run: yarn build-embedding-sdk-package
|
||||
|
||||
- name: Prepare Cypress environment
|
||||
id: cypress-prep
|
||||
uses: ./.github/actions/prepare-cypress
|
||||
|
||||
- name: Compile CLJS
|
||||
run: bun run build-pure:cljs
|
||||
run: yarn build-pure:cljs
|
||||
|
||||
- name: Run Metabase
|
||||
run: node e2e/runner/run_cypress_ci.js start
|
||||
@@ -150,7 +144,6 @@ jobs:
|
||||
env:
|
||||
GREP: ${{ github.event.inputs.grep }}
|
||||
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
FAIL_FAST: ${{ inputs.fail_fast }}
|
||||
run: |
|
||||
node e2e/runner/run_cypress_ci.js ${{ needs.determine-test-type.outputs.type }} \
|
||||
--spec '${{ github.event.inputs.spec }}' \
|
||||
|
||||
15
.github/workflows/e2e-test.yml
vendored
15
.github/workflows/e2e-test.yml
vendored
@@ -21,7 +21,7 @@ on:
|
||||
runner:
|
||||
required: false
|
||||
type: string
|
||||
default: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
default: ubuntu-22.04
|
||||
total_chunks: # number of chunks
|
||||
required: false
|
||||
type: number
|
||||
@@ -41,13 +41,12 @@ env:
|
||||
jobs:
|
||||
e2e-tests:
|
||||
runs-on: ${{ inputs.runner }}
|
||||
timeout-minutes: 40
|
||||
timeout-minutes: 45
|
||||
name: e2e-tests-${{ inputs.name }}-${{ inputs.edition }}
|
||||
env:
|
||||
MB_EDITION: ${{ inputs.edition }}
|
||||
# Sensitive env vars (tokens, secrets) starting with `CYPRESS_` are accessed via async `cy.env()`
|
||||
# Example: `cy.env(["FOO"]).then(({ FOO }) => ...)`
|
||||
# For non-sensitive config, use `Cypress.expose()` in cypress config instead (synchronous)
|
||||
# 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 }}
|
||||
@@ -80,8 +79,7 @@ jobs:
|
||||
- name: Retrieve uberjar artifact for ${{ inputs.edition }}
|
||||
uses: ./.github/actions/fetch-artifact
|
||||
with:
|
||||
commit: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
edition: ${{ inputs.edition }}
|
||||
name: metabase-${{ inputs.edition }}-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
|
||||
extract-path: "target/uberjar"
|
||||
output-name: "metabase.jar"
|
||||
|
||||
@@ -107,7 +105,7 @@ jobs:
|
||||
run: echo "specs=$(cat failed-specs)" >> $GITHUB_ENV
|
||||
|
||||
- name: Compile CLJS
|
||||
run: bun run build-pure:cljs
|
||||
run: yarn build-pure:cljs
|
||||
|
||||
- name: Run Metabase
|
||||
run: node e2e/runner/run_cypress_ci.js start
|
||||
@@ -172,7 +170,6 @@ jobs:
|
||||
# Related to Trunk uploader
|
||||
variant: e2e-tests
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
previous-step-outcome: ${{ inputs.specs && steps.tagged-e2e.outcome || steps.split-e2e.outcome }}
|
||||
|
||||
- name: Upload Cypress Artifacts upon failure
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
6
.github/workflows/e2e-tests.yml
vendored
6
.github/workflows/e2e-tests.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
reset-e2e-test-comment:
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Remove test results comment
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
|
||||
collect-timings:
|
||||
name: Collect E2E Timings
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
needs: e2e-tests
|
||||
if: always() && !cancelled() && github.ref == 'refs/heads/master'
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
e2e-tests-result:
|
||||
needs:
|
||||
- e2e-tests
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
name: e2e-tests-result
|
||||
if: always() && !cancelled()
|
||||
|
||||
@@ -12,16 +12,15 @@ env:
|
||||
jobs:
|
||||
sdk-component-tests:
|
||||
name: "SDK component tests (React ${{ matrix.react-version }})"
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
matrix:
|
||||
react-version: [18, 19]
|
||||
fail-fast: false
|
||||
env:
|
||||
# Sensitive env vars (tokens, secrets) starting with `CYPRESS_` are accessed via async `cy.env()`
|
||||
# Example: `cy.env(["FOO"]).then(({ FOO }) => ...)`
|
||||
# For non-sensitive config, use `Cypress.expose()` in cypress config instead (synchronous)
|
||||
# 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 }}
|
||||
@@ -46,8 +45,7 @@ jobs:
|
||||
- name: Retrieve uberjar artifact for ee
|
||||
uses: ./.github/actions/fetch-artifact
|
||||
with:
|
||||
commit: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
edition: ee
|
||||
name: metabase-ee-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
|
||||
extract-path: "target/uberjar"
|
||||
output-name: "metabase.jar"
|
||||
|
||||
@@ -82,7 +80,6 @@ jobs:
|
||||
run: ./bin/embedding-sdk/change-react-version.bash ${{ matrix.react-version }}
|
||||
|
||||
- name: Run component tests for Embedding SDK
|
||||
id: run-tests
|
||||
env:
|
||||
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
run: node e2e/runner/run_cypress_ci.js component
|
||||
@@ -98,7 +95,6 @@ jobs:
|
||||
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
previous-step-outcome: ${{ steps.run-tests.outcome }}
|
||||
|
||||
- name: "SDK component tests > Upload Cypress Artifacts upon failure"
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -128,7 +124,7 @@ jobs:
|
||||
await core.summary.addRaw(summary).write();
|
||||
|
||||
decide-target-ref:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
target-ref: ${{ steps.get-target-ref.outputs.value }}
|
||||
steps:
|
||||
@@ -151,14 +147,13 @@ jobs:
|
||||
sdk-component-tests-backward-compatibility:
|
||||
name: "SDK component tests (Backward compatibility)"
|
||||
needs: [decide-target-ref]
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
MB_RUN_MODE: "e2e"
|
||||
METASTORE_DEV_SERVER_URL: ${{ secrets.METASTORE_DEV_SERVER_URL }}
|
||||
# Sensitive env vars (tokens, secrets) starting with `CYPRESS_` are accessed via async `cy.env()`
|
||||
# Example: `cy.env(["FOO"]).then(({ FOO }) => ...)`
|
||||
# For non-sensitive config, use `Cypress.expose()` in cypress config instead (synchronous)
|
||||
# Any env starting with `CYPRESS_` will be available to all Cypress tests via `Cypress.env()`
|
||||
# Example: you can get `CYPRESS_FOO` with `Cypress.env("FOO")`
|
||||
CYPRESS_MB_ALL_FEATURES_TOKEN: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
|
||||
CYPRESS_MB_STARTER_CLOUD_TOKEN: ${{ secrets.STAGING_MB_STARTER_CLOUD_TOKEN }}
|
||||
CYPRESS_MB_PRO_CLOUD_TOKEN: ${{ secrets.STAGING_MB_PRO_CLOUD_TOKEN }}
|
||||
@@ -199,8 +194,7 @@ jobs:
|
||||
id: download-uberjar
|
||||
uses: ./.github/actions/fetch-artifact
|
||||
with:
|
||||
commit: ${{ steps.get-commit-sha.outputs.sha }}
|
||||
edition: ee
|
||||
name: metabase-ee-${{ steps.get-commit-sha.outputs.sha }}-uberjar
|
||||
extract-path: "target/uberjar"
|
||||
output-name: "metabase.jar"
|
||||
continue-on-error: true # do not break the whole job if this step doesn't succeed
|
||||
@@ -234,7 +228,6 @@ jobs:
|
||||
--spec "e2e/snapshot-creators/default.cy.snap.js"
|
||||
|
||||
- name: Run component tests for Embedding SDK
|
||||
id: run-tests
|
||||
env:
|
||||
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
run: |
|
||||
@@ -252,7 +245,6 @@ jobs:
|
||||
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
previous-step-outcome: ${{ steps.run-tests.outcome }}
|
||||
|
||||
- name: Upload Cypress Artifacts upon failure
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -9,10 +9,11 @@ env:
|
||||
TZ: US/Pacific # to make Node match the instance tz
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HASH: ${{ github.event.pull_request.head.sha || github.sha }}-${{ github.run_attempt }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || '' }}
|
||||
MB_EDITION: ee
|
||||
jobs:
|
||||
get-sample-apps-data:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
# we must support the last two stable releases
|
||||
@@ -20,7 +21,12 @@ jobs:
|
||||
TEST_SUITES: |
|
||||
[
|
||||
"metabase-nodejs-react-sdk-embedding-sample-e2e",
|
||||
"metabase-nextjs-sdk-embedding-sample-e2e"
|
||||
"metabase-nextjs-sdk-embedding-sample-e2e",
|
||||
"shoppy-e2e"
|
||||
]
|
||||
TEST_SUITES_TO_RUN_IN_MASTER_ONLY: |
|
||||
[
|
||||
"shoppy-e2e"
|
||||
]
|
||||
SAMPLE_APP_MAIN_BRANCH_NAME: "main"
|
||||
SAMPLE_APP_STABLE_RELEASE_BRANCH_NAME_SUFFIX: "stable"
|
||||
@@ -30,8 +36,8 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Prepare release scripts
|
||||
uses: ./.github/actions/prepare-release-scripts
|
||||
- name: Prepare build scripts
|
||||
run: cd ${{ github.workspace }}/release && yarn && yarn build
|
||||
|
||||
- name: Build sample app compatibility data
|
||||
uses: actions/github-script@v7
|
||||
@@ -44,6 +50,7 @@ jobs:
|
||||
} = require('${{ github.workspace }}/release/dist/index.cjs');
|
||||
|
||||
const testSuites = JSON.parse(process.env.TEST_SUITES);
|
||||
const testSuitesToRunInMasterOnly = JSON.parse(process.env.TEST_SUITES_TO_RUN_IN_MASTER_ONLY);
|
||||
|
||||
const ref = '${{ github.base_ref || github.ref }}';
|
||||
const branchName = ref.replace(/^refs\/heads\//, '');
|
||||
@@ -67,16 +74,19 @@ jobs:
|
||||
const sampleAppBranchName = `${currentBranchVersion}-${{ env.SAMPLE_APP_STABLE_RELEASE_BRANCH_NAME_SUFFIX }}`
|
||||
|
||||
const shouldRunTestsForVersion = (currentReleaseVersion - currentBranchVersion) <= (supportedReleasesCount - 1);
|
||||
const releaseBranchTestSuites = testSuites.filter(testSuite => !testSuitesToRunInMasterOnly.includes(testSuite));
|
||||
|
||||
return {
|
||||
branch_name: sampleAppBranchName,
|
||||
matrix: shouldRunTestsForVersion ? testSuites : [],
|
||||
matrix: shouldRunTestsForVersion
|
||||
? releaseBranchTestSuites
|
||||
: []
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
branch_name: '',
|
||||
matrix: [],
|
||||
matrix: []
|
||||
};
|
||||
|
||||
# In order to check compatibility between Sample Apps and Embedding SDK, the job for each sample app:
|
||||
@@ -88,13 +98,12 @@ jobs:
|
||||
e2e-sample-apps-tests:
|
||||
needs: [get-sample-apps-data]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
test_suite: ${{ fromJSON(needs.get-sample-apps-data.outputs.matrix) }}
|
||||
environment: ["production", "development"]
|
||||
if: |
|
||||
!cancelled() && needs.get-sample-apps-data.result == 'success'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 45
|
||||
name: e2e-sample-app-${{ matrix.test_suite }}-tests-${{ matrix.environment }}-bundle
|
||||
env:
|
||||
@@ -106,8 +115,6 @@ jobs:
|
||||
SAMPLE_APP_ENVIRONMENT: ${{ matrix.environment }}
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -130,8 +137,7 @@ jobs:
|
||||
- name: Retrieve uberjar artifact for ee
|
||||
uses: ./.github/actions/fetch-artifact
|
||||
with:
|
||||
commit: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
edition: ee
|
||||
name: metabase-ee-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
|
||||
extract-path: "target/uberjar"
|
||||
output-name: "metabase.jar"
|
||||
|
||||
@@ -161,8 +167,28 @@ jobs:
|
||||
echo "sample_app_branch_exists=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Prepare and launch Sample Apps
|
||||
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' }}
|
||||
- name: Configure AWS credentials
|
||||
if: ${{ matrix.test_suite == 'shoppy-e2e' }}
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: ${{ secrets.SHOPPY_IAM_ROLE }}
|
||||
role-session-name: GitHub_to_AWS_via_FederatedOIDC
|
||||
aws-region: us-east-1
|
||||
|
||||
- name: Download Shoppy's App DB PostgreSQL dump from S3
|
||||
if: ${{ matrix.test_suite == 'shoppy-e2e' }}
|
||||
run: |
|
||||
aws s3 cp s3://${{ secrets.SHOPPY_METABASE_APP_DB_S3_BUCKET }}/${{ secrets.SHOPPY_METABASE_APP_DB_S3_KEY }} ./e2e/tmp/db_dumps/shoppy_metabase_app_db_dump.sql
|
||||
|
||||
- name: Prepare and launch Sample Apps for shoppy-e2e test suite
|
||||
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' && matrix.test_suite == 'shoppy-e2e' }}
|
||||
env:
|
||||
METABASE_APP_DB_USER: ${{ secrets.SHOPPY_METABASE_APP_DB_USER }}
|
||||
METABASE_APP_DB: ${{ secrets.SHOPPY_METABASE_APP_DB_DATABASE }}
|
||||
run: npx tsx ./e2e/runner/embedding-sdk/sample-apps/start-ci.ts shoppy-e2e
|
||||
|
||||
- name: Prepare and launch Sample Apps for other test suites
|
||||
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' && matrix.test_suite != 'shoppy-e2e' }}
|
||||
run: npx tsx ./e2e/runner/embedding-sdk/sample-apps/start-ci.ts ${{ matrix.test_suite }}
|
||||
|
||||
- name: Prune build cache
|
||||
@@ -183,7 +209,7 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' && steps.run-e2e-tests.outcome != 'success' }}
|
||||
with:
|
||||
name: cypress-recording-sample-app-${{ matrix.test_suite }}-${{ matrix.environment }}-latest
|
||||
name: cypress-recording-sample-app-${{ matrix.test_suite }}-latest
|
||||
path: |
|
||||
./e2e/tmp/${{ steps.get-sample-app-name.outputs.sample_app_name }}/cypress
|
||||
if-no-files-found: ignore
|
||||
@@ -193,153 +219,6 @@ jobs:
|
||||
run: |
|
||||
exit 1
|
||||
|
||||
# Shoppy e2e tests run separately using the shared composite action for setup.
|
||||
e2e-shoppy-tests:
|
||||
needs: [get-sample-apps-data]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
environment: ["production", "development"]
|
||||
if: |
|
||||
!cancelled() && needs.get-sample-apps-data.result == 'success' &&
|
||||
(github.ref == 'refs/heads/master' || github.base_ref == 'master')
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 45
|
||||
name: e2e-sample-app-shoppy-e2e-tests-${{ matrix.environment }}-bundle
|
||||
env:
|
||||
MB_EDITION: ee
|
||||
MB_RUN_MODE: e2e
|
||||
METASTORE_DEV_SERVER_URL: ${{ secrets.METASTORE_DEV_SERVER_URL }}
|
||||
CYPRESS_MB_ALL_FEATURES_TOKEN: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
|
||||
SAMPLE_APP_BRANCH_NAME: ${{ needs.get-sample-apps-data.outputs.branch_name }}
|
||||
SAMPLE_APP_ENVIRONMENT: ${{ matrix.environment }}
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Prepare Shoppy E2E environment
|
||||
uses: ./.github/actions/prepare-shoppy-e2e
|
||||
id: shoppy-prep
|
||||
with:
|
||||
commit: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
sample-app-branch-name: ${{ needs.get-sample-apps-data.outputs.branch_name }}
|
||||
sample-app-environment: ${{ matrix.environment }}
|
||||
staging-mb-all-features-token: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
|
||||
shoppy-iam-role: ${{ secrets.SHOPPY_IAM_ROLE }}
|
||||
shoppy-metabase-app-db-s3-bucket: ${{ secrets.SHOPPY_METABASE_APP_DB_S3_BUCKET }}
|
||||
shoppy-metabase-app-db-s3-key: ${{ secrets.SHOPPY_METABASE_APP_DB_S3_KEY }}
|
||||
shoppy-metabase-app-db-user: ${{ secrets.SHOPPY_METABASE_APP_DB_USER }}
|
||||
shoppy-metabase-app-db-database: ${{ secrets.SHOPPY_METABASE_APP_DB_DATABASE }}
|
||||
metastore-dev-server-url: ${{ secrets.METASTORE_DEV_SERVER_URL }}
|
||||
shoppy-datadog-application-id: ${{ secrets.SHOPPY_METABASE_APP_DATADOG_APPLICATION_ID }}
|
||||
shoppy-datadog-client-token: ${{ secrets.SHOPPY_METABASE_APP_DATADOG_CLIENT_TOKEN }}
|
||||
|
||||
- name: Run e2e tests for Shoppy
|
||||
id: run-e2e-tests
|
||||
if: ${{ steps.shoppy-prep.outputs.sample_app_branch_exists == 'true' }}
|
||||
continue-on-error: true
|
||||
env:
|
||||
CYPRESS_BROWSER: ${{ steps.shoppy-prep.outputs.chrome-path }}
|
||||
run: node e2e/runner/run_cypress_ci.js shoppy-e2e
|
||||
|
||||
- name: Upload Cypress Artifacts upon failure
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ steps.shoppy-prep.outputs.sample_app_branch_exists == 'true' && steps.run-e2e-tests.outcome != 'success' }}
|
||||
with:
|
||||
name: cypress-recording-sample-app-shoppy-e2e-${{ matrix.environment }}-latest
|
||||
path: |
|
||||
./e2e/tmp/shoppy/cypress
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Fail job if tests failed
|
||||
if: ${{ steps.shoppy-prep.outputs.sample_app_branch_exists == 'true' && steps.run-e2e-tests.outcome != 'success' }}
|
||||
run: |
|
||||
exit 1
|
||||
|
||||
# Synthetic monitoring tests run only on master merges, in parallel with the regular e2e tests.
|
||||
# They use the shoppy production environment and include performance measurement tests.
|
||||
e2e-shoppy-synthetic-tests:
|
||||
needs: [get-sample-apps-data]
|
||||
if: |
|
||||
!cancelled() && needs.get-sample-apps-data.result == 'success' &&
|
||||
github.event_name == 'push' &&
|
||||
github.ref == 'refs/heads/master'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 45
|
||||
name: e2e-shoppy-synthetic-monitoring-tests
|
||||
env:
|
||||
MB_EDITION: ee
|
||||
MB_RUN_MODE: e2e
|
||||
METASTORE_DEV_SERVER_URL: ${{ secrets.METASTORE_DEV_SERVER_URL }}
|
||||
CYPRESS_MB_ALL_FEATURES_TOKEN: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
|
||||
SAMPLE_APP_BRANCH_NAME: ${{ needs.get-sample-apps-data.outputs.branch_name }}
|
||||
SAMPLE_APP_ENVIRONMENT: production
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Prepare Shoppy E2E environment
|
||||
uses: ./.github/actions/prepare-shoppy-e2e
|
||||
id: shoppy-prep
|
||||
with:
|
||||
commit: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
sample-app-branch-name: ${{ needs.get-sample-apps-data.outputs.branch_name }}
|
||||
sample-app-environment: production
|
||||
staging-mb-all-features-token: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
|
||||
shoppy-iam-role: ${{ secrets.SHOPPY_IAM_ROLE }}
|
||||
shoppy-metabase-app-db-s3-bucket: ${{ secrets.SHOPPY_METABASE_APP_DB_S3_BUCKET }}
|
||||
shoppy-metabase-app-db-s3-key: ${{ secrets.SHOPPY_METABASE_APP_DB_S3_KEY }}
|
||||
shoppy-metabase-app-db-user: ${{ secrets.SHOPPY_METABASE_APP_DB_USER }}
|
||||
shoppy-metabase-app-db-database: ${{ secrets.SHOPPY_METABASE_APP_DB_DATABASE }}
|
||||
metastore-dev-server-url: ${{ secrets.METASTORE_DEV_SERVER_URL }}
|
||||
shoppy-datadog-application-id: ${{ secrets.SHOPPY_METABASE_APP_DATADOG_APPLICATION_ID }}
|
||||
shoppy-datadog-client-token: ${{ secrets.SHOPPY_METABASE_APP_DATADOG_CLIENT_TOKEN }}
|
||||
|
||||
- name: Get merged PR number
|
||||
if: ${{ steps.shoppy-prep.outputs.sample_app_branch_exists == 'true' }}
|
||||
id: get-merged-pr-number
|
||||
run: |
|
||||
PR=$(gh api repos/${{ github.repository }}/commits/${{ github.sha }}/pulls --jq '.[0].number' 2>/dev/null || echo "")
|
||||
if [ -z "$PR" ] || [ "$PR" = "null" ]; then
|
||||
echo "number=" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "number=$PR" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Run synthetic monitoring tests
|
||||
id: run-e2e-tests
|
||||
if: ${{ steps.shoppy-prep.outputs.sample_app_branch_exists == 'true' }}
|
||||
continue-on-error: true
|
||||
env:
|
||||
CYPRESS_BROWSER: ${{ steps.shoppy-prep.outputs.chrome-path }}
|
||||
PR_NUMBER: ${{ steps.get-merged-pr-number.outputs.number }}
|
||||
INCLUDE_SYNTHETIC_MONITORING_TESTS: "true"
|
||||
run: node e2e/runner/run_cypress_ci.js shoppy-e2e
|
||||
|
||||
- name: Upload Cypress Artifacts upon failure
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ steps.shoppy-prep.outputs.sample_app_branch_exists == 'true' && steps.run-e2e-tests.outcome != 'success' }}
|
||||
with:
|
||||
name: cypress-recording-shoppy-synthetic-tests-latest
|
||||
path: |
|
||||
./e2e/tmp/shoppy/cypress
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Fail job if tests failed
|
||||
if: ${{ steps.shoppy-prep.outputs.sample_app_branch_exists == 'true' && steps.run-e2e-tests.outcome != 'success' }}
|
||||
run: |
|
||||
exit 1
|
||||
|
||||
# The job checks integration of the Embedding SDK with consumer's apps that use different frameworks/bundlers,
|
||||
# or when we want to test some tricky integration cases like conflicting types.
|
||||
# The job
|
||||
@@ -349,7 +228,6 @@ jobs:
|
||||
# For more details see `Embedding SDK integration tests with Host Apps` in `enterprise/frontend/src/embedding-sdk-package/dev.md`
|
||||
e2e-host-apps-tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
test_suite:
|
||||
[
|
||||
@@ -359,7 +237,7 @@ jobs:
|
||||
"angular-20-host-app-e2e",
|
||||
]
|
||||
environment: ["production", "development"]
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 45
|
||||
name: e2e-host-app-${{ matrix.test_suite }}-tests-${{ matrix.environment }}-bundle
|
||||
env:
|
||||
@@ -367,8 +245,6 @@ jobs:
|
||||
HOST_APP_ENVIRONMENT: ${{ matrix.environment }}
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -376,8 +252,7 @@ jobs:
|
||||
- name: Retrieve uberjar artifact for ee
|
||||
uses: ./.github/actions/fetch-artifact
|
||||
with:
|
||||
commit: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
edition: ee
|
||||
name: metabase-ee-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
|
||||
extract-path: "target/uberjar"
|
||||
output-name: "metabase.jar"
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Check which files changed
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
frontend_embedding_sdk_package_sources: ${{ steps.changes.outputs.frontend_embedding_sdk_package_sources }}
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
!cancelled() &&
|
||||
(needs.files-changed.outputs.frontend_embedding_sdk_package_sources == 'true')
|
||||
name: Embedding SDK package release-notes/no-release-notes label reminder
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -10,7 +10,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build-sdk-package:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
# Remove the matchers to not report false positives on every PR
|
||||
# unrelated to embedding (EMB-765)
|
||||
echo "::remove-matcher owner=tsc::"
|
||||
bun run build-embedding-sdk-package
|
||||
yarn build-embedding-sdk-package
|
||||
- name: Prepare Embedding SDK artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
73
.github/workflows/embedding-sdk.yml
vendored
73
.github/workflows/embedding-sdk.yml
vendored
@@ -23,10 +23,6 @@ on:
|
||||
description: "Whether to run host and sample apps compatibility tests."
|
||||
required: true
|
||||
type: boolean
|
||||
bundle-size-check:
|
||||
description: "Whether to run bundle size check."
|
||||
required: true
|
||||
type: boolean
|
||||
skip:
|
||||
description: Whether to skip running tests due to one of the artifacts failing to build.
|
||||
required: true
|
||||
@@ -39,7 +35,7 @@ concurrency:
|
||||
jobs:
|
||||
embedding-sdk-cli-snippets-type-check:
|
||||
if: inputs.cli-snippets-type-check && !inputs.skip
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -51,11 +47,11 @@ jobs:
|
||||
name: embedding-sdk-${{ github.event.pull_request.head.sha || github.sha }}
|
||||
path: ${{ github.workspace }}/resources/embedding-sdk
|
||||
- name: Run frontend embedding SDK snippets type check
|
||||
run: bun run embedding-sdk:cli-snippets:type-check
|
||||
run: yarn run embedding-sdk:cli-snippets:type-check
|
||||
|
||||
embedding-sdk-docs-snippets-type-check:
|
||||
if: inputs.docs-snippets-type-check && !inputs.skip
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -67,11 +63,11 @@ jobs:
|
||||
name: embedding-sdk-${{ github.event.pull_request.head.sha || github.sha }}
|
||||
path: ${{ github.workspace }}/resources/embedding-sdk
|
||||
- name: Run frontend embedding SDK snippets type check
|
||||
run: bun run embedding-sdk:docs-snippets:type-check
|
||||
run: yarn run embedding-sdk:docs-snippets:type-check
|
||||
|
||||
embedding-sdk-api-documentation-generation-validation:
|
||||
if: inputs.documentation-validation && !inputs.skip
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -90,7 +86,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
set -o pipefail
|
||||
bun run embedding-sdk:docs:generate:pure --emit none 2>&1 \
|
||||
yarn embedding-sdk:docs:generate:pure --emit none 2>&1 \
|
||||
| tee /dev/stdout \
|
||||
| tee >( tail -n 10 \
|
||||
| sed '1s/^/output<<EOF\n/' \
|
||||
@@ -102,14 +98,14 @@ jobs:
|
||||
with:
|
||||
remove-previous-comment: true
|
||||
add-new-comment: ${{ steps.docs-generation.outcome == 'failure' && contains(steps.docs-generation.outputs.output, 'not included in the documentation') }}
|
||||
message: "📝 Some exported types from `@metabase/embedding-sdk-react` reference other types that are not exported, i.e. internal types.<br><br>The internal type reference breaks API documentation generation for the SDK and also may unexpectedly increase the output size of the type declaration file.<br><br>Please check the CI logs to find the name of such type.<br><br>- If a type should be publicly available in the SDK, add a description to it or a TypeDoc annotation and export it from SDK.<br>- If a type is internal and should _not_ be publicly exposed, refactor types to prevent referencing other types that are not exported from SDK.<br><br>You can run `bun run embedding-sdk:docs:generate` locally to verify that the issue has been resolved."
|
||||
message: "📝 Some exported types from `@metabase/embedding-sdk-react` reference other types that are not exported, i.e. internal types.<br><br>The internal type reference breaks API documentation generation for the SDK and also may unexpectedly increase the output size of the type declaration file.<br><br>Please check the CI logs to find the name of such type.<br><br>- If a type should be publicly available in the SDK, add a description to it or a TypeDoc annotation and export it from SDK.<br>- If a type is internal and should _not_ be publicly exposed, refactor types to prevent referencing other types that are not exported from SDK.<br><br>You can run `yarn embedding-sdk:docs:generate` locally to verify that the issue has been resolved."
|
||||
include-log: true
|
||||
- name: Comment on PR if 'Failed to resolve link' error happens
|
||||
uses: ./.github/actions/notify-pull-request
|
||||
with:
|
||||
remove-previous-comment: true
|
||||
add-new-comment: ${{ steps.docs-generation.outcome == 'failure' && contains(steps.docs-generation.outputs.output, 'Failed to resolve link') }}
|
||||
message: "📝 A typedoc `{@link <TYPE_NAME>}` annotation is found that can't be resolved.<br><br>Most likely it happens when the referenced type is not exported. Please check the CI logs to find the name of such type.<br><br>- If a referenced in `{@link}` type should be publicly available in the SDK, add a description to it or a TypeDoc annotation and export it from SDK.<br>- If a type is internal and should _not_ be publicly exposed, change types to prevent unexported type reference in `{@link}` annotation.<br><br>You can run `bun run embedding-sdk:docs:generate` locally to verify that the issue has been resolved."
|
||||
message: "📝 A typedoc `{@link <TYPE_NAME>}` annotation is found that can't be resolved.<br><br>Most likely it happens when the referenced type is not exported. Please check the CI logs to find the name of such type.<br><br>- If a referenced in `{@link}` type should be publicly available in the SDK, add a description to it or a TypeDoc annotation and export it from SDK.<br>- If a type is internal and should _not_ be publicly exposed, change types to prevent unexported type reference in `{@link}` annotation.<br><br>You can run `yarn embedding-sdk:docs:generate` locally to verify that the issue has been resolved."
|
||||
include-log: true
|
||||
- name: Fail Job
|
||||
if: ${{ steps.docs-generation.outcome == 'failure' }}
|
||||
@@ -128,65 +124,14 @@ jobs:
|
||||
uses: ./.github/workflows/embedding-sdk-host-sample-apps-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
embedding-sdk-bundle-size-check:
|
||||
if: inputs.bundle-size-check
|
||||
continue-on-error: true
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
BUNDLE_SIZE_THRESHOLD_PERCENT: 2
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/sdk-bundle-size-check
|
||||
id: compare
|
||||
with:
|
||||
edition: ee
|
||||
base-ref-commit: ${{ github.event.pull_request.base.sha }}
|
||||
threshold-percent: ${{ env.BUNDLE_SIZE_THRESHOLD_PERCENT }}
|
||||
|
||||
- name: Comment on PR if bundle size has increased
|
||||
uses: ./.github/actions/notify-pull-request
|
||||
if: success() && steps.compare.outputs.status == 'increased'
|
||||
with:
|
||||
remove-previous-comment: true
|
||||
add-new-comment: true
|
||||
message: "⚠️ Embedding SDK bundle size increased by ${{ steps.compare.outputs.size_change_percent }}% compared to the ${{ github.base_ref }} branch (threshold: ${{ env.BUNDLE_SIZE_THRESHOLD_PERCENT }}%). Please contact Embedding team."
|
||||
include-log: false
|
||||
|
||||
- name: Send notification to Slack via Zapier
|
||||
if: success() && steps.compare.outputs.status == 'increased'
|
||||
shell: bash
|
||||
env:
|
||||
ZAPIER_WEBHOOK_URL: ${{ secrets.EMBEDDING_ZAPIER_WEBHOOK_URL }}
|
||||
SIZE_CHANGE_PERCENT: ${{ steps.compare.outputs.size_change_percent }}
|
||||
BUNDLE_SIZE_THRESHOLD_PERCENT: ${{ env.BUNDLE_SIZE_THRESHOLD_PERCENT }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
run: |
|
||||
curl -s -X POST "$ZAPIER_WEBHOOK_URL" \
|
||||
-d "⚠️ Embedding SDK bundle size increased by ${SIZE_CHANGE_PERCENT}% (threshold: ${BUNDLE_SIZE_THRESHOLD_PERCENT}%)
|
||||
|
||||
PR #${PR_NUMBER} ${PR_TITLE} (${PR_URL})"
|
||||
|
||||
- name: Comment on PR if bundle size has decreased
|
||||
uses: ./.github/actions/notify-pull-request
|
||||
if: success() && steps.compare.outputs.status == 'decreased'
|
||||
with:
|
||||
remove-previous-comment: true
|
||||
add-new-comment: true
|
||||
message: "🎉 Embedding SDK bundle size decreased by ${{ steps.compare.outputs.size_change_percent }}% compared to the ${{ github.base_ref }} branch. Nice work!"
|
||||
include-log: false
|
||||
|
||||
embedding-sdk-tests-result:
|
||||
needs:
|
||||
- embedding-sdk-bundle-size-check
|
||||
- embedding-sdk-cli-snippets-type-check
|
||||
- embedding-sdk-docs-snippets-type-check
|
||||
- embedding-sdk-api-documentation-generation-validation
|
||||
- embedding-sdk-component-tests
|
||||
if: always() && !cancelled()
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
needs: ${{ toJson(needs) }}
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
jobs:
|
||||
on-escalation-label:
|
||||
if: github.event.label.name == '.Escalation'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Format issue title (escape quotes)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user