Compare commits
5 Commits
date-filte
...
python-run
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64977bffa6 | ||
|
|
25dddd8f32 | ||
|
|
ee806be8b7 | ||
|
|
0ea6e387c0 | ||
|
|
f6f128b87d |
@@ -1,12 +0,0 @@
|
||||
Generate a CI failure report for PR $PR_NUM_OR_URL (or current branch if no argument given).
|
||||
|
||||
Run `./bin/mage ci-report $PR_NUM_OR_URL` to generate the report, then:
|
||||
|
||||
1. **Summarize the failures** - What tests failed and in which jobs?
|
||||
2. **Categorize** - Are these real failures, flaky tests, or infrastructure issues?
|
||||
3. **Recommend next steps**:
|
||||
- If flaky: suggest rerunning with `gh run rerun <run-id> --failed -R metabase/metabase`
|
||||
- If real: identify the root cause and suggest fixes
|
||||
- If infrastructure: note the issue type (timeout, OOM, network)
|
||||
|
||||
Focus on actionable insights. If all checks are passing, confirm that briefly.
|
||||
@@ -1 +0,0 @@
|
||||
Run mutation testing for $ARGUMENTS
|
||||
@@ -1,86 +0,0 @@
|
||||
## Linting and Formatting
|
||||
|
||||
- **Lint PR:** `./bin/mage kondo-updated master` (or whatever target branch)
|
||||
- Call the command one time at the beginning, record the results, then work through the problems one at a time.
|
||||
- If the solution is obvious, then please apply the fix. Otherwise skip it.
|
||||
- If you fix all the issues (and verify by rerunning the kondo-updated command):
|
||||
- commit the change with a succinct and descriptive commit message
|
||||
- **Lint File:** `./bin/mage kondo <file or files>`
|
||||
- Use the linter as a way to know that you are adhering to conventions in place in the codebase
|
||||
- **Lint Changes:** `./bin/mage kondo-updated HEAD`
|
||||
- **Format:** `./bin/mage cljfmt-files [path]`
|
||||
|
||||
## Testing
|
||||
|
||||
- **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
|
||||
|
||||
- **Check Code Readability:** `./bin/mage -check-readable <file> [line-number]`
|
||||
- Run after every change to Clojure code
|
||||
- Check specific line first, then entire file if readable
|
||||
|
||||
## REPL Usage
|
||||
|
||||
> **Note:** If you have `clojure-mcp` tools available (check for tools like `clojure_eval`),
|
||||
> **always prefer those over `./bin/mage -repl`**. The MCP tools provide better integration,
|
||||
> richer feedback, and avoid shell escaping issues. Only use `./bin/mage -repl` as a fallback
|
||||
> when clojure-mcp is not available.
|
||||
|
||||
- **Evaluating Clojure Code:** `./bin/mage -repl '<code>'`
|
||||
- See "Sending Code to the REPL" section for more details
|
||||
|
||||
### Sending Code to the REPL
|
||||
|
||||
- Send code to the metabase process REPL using: `./bin/mage -repl '(+ 1 1)'` where `(+ 1 1)` is your Clojure code.
|
||||
- See `./bin/mage -repl -h` for more details.
|
||||
- If the Metabase backend is not running, you'll see an error message with instructions on how to start it.
|
||||
|
||||
#### Working with Files and Namespaces
|
||||
|
||||
1. **Load a file and call functions with fully qualified names**:
|
||||
|
||||
To call `your.namespace/your-function` on `arg1` and `arg2`:
|
||||
|
||||
```
|
||||
./bin/mage -repl --namespace your.namespace '(your-function arg1 arg2)'
|
||||
```
|
||||
|
||||
DO NOT use "require", "load-file" etc in the code string argument.
|
||||
|
||||
#### Understanding the Response
|
||||
|
||||
The `./bin/mage -repl` command returns three separate, independent outputs:
|
||||
|
||||
- `value`: The return value of the last expression (best for data structures)
|
||||
- `stdout`: Any printed output from `println` etc. (best for messages)
|
||||
- `stderr`: Any error messages (best for warnings and errors)
|
||||
|
||||
Example call:
|
||||
|
||||
```bash
|
||||
./bin/mage -repl '(println "Hello, world!") '\''({0 1, 1 3, 2 0, 3 2} {0 2, 1 0, 2 3, 3 1})'
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```
|
||||
ns: user
|
||||
session: 32a35206-871c-4553-9bc9-f49491173d1c
|
||||
value: ({0 1, 1 3, 2 0, 3 2} {0 2, 1 0, 2 3, 3 1})
|
||||
stdout: Hello, world!
|
||||
stderr:
|
||||
```
|
||||
|
||||
For effective REPL usage:
|
||||
|
||||
- Return data structures as function return values
|
||||
- Use `println` for human-readable messages
|
||||
- Print errors to stderr
|
||||
@@ -1,153 +0,0 @@
|
||||
# Metabase Clojure Style Guide
|
||||
|
||||
This guide covers Clojure and ClojureScript coding conventions for Metabase. See also: `CLOJURE_STYLE_GUIDE.adoc` for the Community Clojure Style Guide.
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
**General Naming:**
|
||||
|
||||
- Acceptable abbreviations: `acc`, `i`, `pred`, `coll`, `n`, `s`, `k`, `f`
|
||||
- Use `kebab-case` for all variables, functions, and constants
|
||||
|
||||
**Function Naming:**
|
||||
|
||||
- Pure functions should be nouns describing the value they return (e.g., `age` not `calculate-age` or `get-age`)
|
||||
- Functions with side effects must end with `!`
|
||||
- Don't repeat namespace alias in function names
|
||||
|
||||
**Destructuring:**
|
||||
|
||||
- Map destructuring should use kebab-case local bindings even if the map uses `snake_case` keys
|
||||
|
||||
## Documentation Standards
|
||||
|
||||
**Docstrings:**
|
||||
|
||||
- Every public var in `src` or `enterprise/backend/src` must have docstring
|
||||
- Format using Markdown conventions
|
||||
- Reference other vars with `[[other-var]]` not backticks
|
||||
|
||||
**Comments:**
|
||||
|
||||
- `TODO` format: `;; TODO (Name YYYY-MM-DD) -- description`
|
||||
|
||||
## Code Organization
|
||||
|
||||
**Visibility:**
|
||||
|
||||
- Make everything `^:private` unless it is used elsewhere
|
||||
- Try to organize namespaces to avoid `declare` (put public functions near the end)
|
||||
|
||||
**Size and Structure:**
|
||||
|
||||
- Break up functions > 20 lines
|
||||
- Lines ≤ 120 characters
|
||||
- No blank non-comment lines within definition forms (except pairwise `let`/`cond`)
|
||||
|
||||
## Style Conventions
|
||||
|
||||
**Keywords and Metadata:**
|
||||
|
||||
- Prefer namespaced keywords for internal use: `:query-type/normal` not `:normal`
|
||||
- Tag variables with `:arglists` metadata if they're functions but wouldn't otherwise have it
|
||||
|
||||
## Tests
|
||||
|
||||
**Organization:**
|
||||
|
||||
- Break large tests into separate `deftest` forms for logically separate test cases
|
||||
- Test names should end in `-test` or `-test-<number>`
|
||||
|
||||
**Performance:**
|
||||
|
||||
- Mark pure function tests `^:parallel`
|
||||
|
||||
## Modules
|
||||
|
||||
**OSS Modules:**
|
||||
|
||||
- Follow `metabase.<module>.*` pattern
|
||||
- Source in `src/metabase/<module>/`
|
||||
|
||||
**Enterprise Modules:**
|
||||
|
||||
- Follow `metabase-enterprise.<module>.*` pattern
|
||||
- Source in `enterprise/backend/src/metabase_enterprise/<module>/`
|
||||
|
||||
**Module Structure:**
|
||||
|
||||
- REST API endpoints go in `<module>.api` or `<module>.api.*` namespaces
|
||||
- Put module public API in `<module>.core` using Potemkin imports
|
||||
- Put Toucan models in `<module>.models.*`
|
||||
- Put settings in `<module>.settings`
|
||||
- Put schemas in `<module>.schema`
|
||||
|
||||
**Module Linters:**
|
||||
|
||||
- Do not cheat module linters with `:clj-kondo/ignore [:metabase/modules]`
|
||||
|
||||
## REST API Endpoints
|
||||
|
||||
**Required Elements:**
|
||||
|
||||
- All new endpoints must have response schemas (`:- <schema>` after route string)
|
||||
- All endpoints need Malli schemas for parameters (detailed and complete)
|
||||
- All new REST API endpoints MUST HAVE TESTS
|
||||
|
||||
**Naming Conventions:**
|
||||
|
||||
- Query parameters use kebab-case
|
||||
- Request bodies use `snake_case`
|
||||
- Routes use singular nouns (e.g., `/api/dashboard/:id`)
|
||||
|
||||
**Behavior:**
|
||||
|
||||
- `GET` endpoints should not have side effects (except analytics)
|
||||
- `defendpoint` forms should be small wrappers around Toucan model code
|
||||
|
||||
## MBQL (Metabase Query Language)
|
||||
|
||||
**Restrictions:**
|
||||
|
||||
- No raw MBQL introspection outside of `lib`, `lib-be`, or `query-processor` modules
|
||||
- Use Lib and MBQL 5 in new source code; avoid legacy MBQL
|
||||
|
||||
## Database and Models
|
||||
|
||||
**Naming:**
|
||||
|
||||
- Model names and table names should be singular nouns
|
||||
- Application database uses `snake_case` identifiers
|
||||
|
||||
**Best Practices:**
|
||||
|
||||
- Use `t2/select-one-fn` instead of fetching entire rows for one column
|
||||
- Put correct behavior in Toucan methods, not separate helper functions
|
||||
|
||||
## Drivers
|
||||
|
||||
**Documentation:**
|
||||
|
||||
- New driver multimethods must be mentioned in `docs/developers-guide/driver-changelog.md`
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- Driver implementations should pass `driver` argument to other driver multimethods
|
||||
- Don't hardcode driver names in implementations
|
||||
- Minimize logic inside `read-column-thunk` in JDBC-based drivers
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
**Examples:**
|
||||
|
||||
- Example data should be bird-themed if possible
|
||||
|
||||
**Linter Suppressions:**
|
||||
|
||||
- Use proper format for kondo suppressions
|
||||
- No `#_:clj-kondo/ignore` (keyword form)
|
||||
|
||||
**Configurable Options:**
|
||||
|
||||
- Don't define configurable options that can only be set with environment variables
|
||||
- Use `:internal` `defsetting` instead
|
||||
@@ -1,8 +0,0 @@
|
||||
# Autonomous Development Workflow
|
||||
|
||||
- Do not attempt to read or edit files outside the project folder
|
||||
- Add failing tests first, then fix them
|
||||
- Work autonomously in small, testable increments
|
||||
- Run targeted tests, and lint continuously during development
|
||||
- Prioritize understanding existing patterns before implementing
|
||||
- Don't commit changes, leave it for the user to review and make commits
|
||||
@@ -1,84 +0,0 @@
|
||||
# Metabase Writing Style Guide
|
||||
|
||||
## Core Principles
|
||||
|
||||
Write like you're talking to a colleague. Be conversational, not formal. Get people what they need quickly. Know your audience and match the complexity.
|
||||
|
||||
## Tone and Voice
|
||||
|
||||
**Do:**
|
||||
|
||||
- Use contractions ("can't" not "cannot")
|
||||
- Say "people" or "companies" instead of "users"
|
||||
- Be friendly but not peppy
|
||||
- Acknowledge limitations honestly ("that's on us, not them")
|
||||
- Jokes and Easter eggs are okay (permit them, don't suggest them)
|
||||
|
||||
**Don't:**
|
||||
|
||||
- Use exclamation points excessively
|
||||
- Rely on tired tropes about nerdiness
|
||||
- Use corporate jargon ("utilize", "offerings", "actionable insights")
|
||||
- Tell people something is cool (show them instead)
|
||||
|
||||
## Structure and Clarity
|
||||
|
||||
**Lead with the important stuff:**
|
||||
|
||||
- Most important information first
|
||||
- Lead with the ask, then provide context
|
||||
- Cut text that adds little value (when in doubt, cut it)
|
||||
- Each paragraph should have one clear purpose
|
||||
|
||||
**Make headings do the work:**
|
||||
|
||||
- Convey your actual point, not just the topic
|
||||
- "Use headings to highlight key points" not "How to write a good heading"
|
||||
- Use sentence case, no punctuation except question marks
|
||||
- No links in headings unless entire heading is a link
|
||||
|
||||
## Instructions and Examples
|
||||
|
||||
**Tell people what to do:**
|
||||
|
||||
- Give the action before explaining why
|
||||
- Put commands in execution order
|
||||
- Provide context, steps, and reasoning
|
||||
- Don't describe tasks as "easy" or "simple"
|
||||
|
||||
**Answer unasked questions:**
|
||||
|
||||
- Include answers to "dumb" questions you had when learning
|
||||
- Don't literally include the question, just give the answer
|
||||
|
||||
## Formatting
|
||||
|
||||
**Code vs. UI elements:**
|
||||
|
||||
- Backticks for code, variable names, parameters only
|
||||
- **Bold** for UI elements and labels (e.g., "Click the **Save** button")
|
||||
- Don't use backticks or quotes for UI elements
|
||||
|
||||
## Writing Mechanics
|
||||
|
||||
- Limit pronouns when introducing new terms (repeat the term to reinforce it)
|
||||
- Ampersands only in proper nouns, never as substitute for "and"
|
||||
|
||||
## Terminology Preferences
|
||||
|
||||
Use familiar terms from tools people already know:
|
||||
|
||||
- "Summarize" (like Excel) instead of "aggregate"
|
||||
- "Take a look at" instead of "reference"
|
||||
- "Filter" instead of technical database terms when appropriate
|
||||
|
||||
## Red Flags
|
||||
|
||||
Avoid these patterns:
|
||||
|
||||
- Multiple exclamation points
|
||||
- Linking "here"
|
||||
- Bullet lists to explain (use prose)
|
||||
- Numbers that will change (guard against change)
|
||||
- Describing things as "easy" or "simple"
|
||||
- Stock photography-worthy content
|
||||
@@ -1,21 +0,0 @@
|
||||
## Linting and Formatting
|
||||
|
||||
- **Lint:** `bun run lint-eslint-pure`
|
||||
- Run ESLint on the codebase
|
||||
- **Format:** `bun run prettier`
|
||||
- Format code using Prettier
|
||||
- **Type Check:** `bun run 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"`
|
||||
- Runs tests matching the given pattern
|
||||
|
||||
### ClojureScript Tests
|
||||
|
||||
- **Test ClojureScript:** `bun run test-cljs`
|
||||
- Run ClojureScript tests
|
||||
@@ -1,420 +0,0 @@
|
||||
---
|
||||
name: add-malli-schemas
|
||||
description: Efficiently add Malli schemas to API endpoints in the Metabase codebase with proper patterns, validation timing, and error handling
|
||||
---
|
||||
|
||||
# Add Malli Schemas to API Endpoints
|
||||
|
||||
This skill helps you efficiently and uniformly add Malli schemas to API endpoints in the Metabase codebase.
|
||||
|
||||
## Reference Files (Best Examples)
|
||||
|
||||
- `src/metabase/warehouses/api.clj` - Most comprehensive schemas, custom error messages
|
||||
- `src/metabase/api_keys/api.clj` - Excellent response schemas
|
||||
- `src/metabase/collections/api.clj` - Great named schema patterns
|
||||
- `src/metabase/timeline/api/timeline.clj` - Clean, simple examples
|
||||
|
||||
## Quick Checklist
|
||||
|
||||
When adding Malli schemas to an endpoint:
|
||||
|
||||
- [ ] Route params have schemas
|
||||
- [ ] Query params have schemas with `:optional true` and `:default` where appropriate
|
||||
- [ ] Request body has a schema (for POST/PUT)
|
||||
- [ ] Response schema is defined (using `:-` after route string)
|
||||
- [ ] Use existing schema types from `ms` namespace when possible
|
||||
- [ ] Consider creating named schemas for reusable or complex types
|
||||
- [ ] Add contextual error messages for validation failures
|
||||
|
||||
## Basic Structure
|
||||
|
||||
### Complete Endpoint Example
|
||||
|
||||
```clojure
|
||||
(mr/def ::Color [:enum "red" "blue" "green"])
|
||||
|
||||
(mr/def ::ResponseSchema
|
||||
[:map
|
||||
[:id pos-int?]
|
||||
[:name string?]
|
||||
[:color ::Color]
|
||||
[:created_at ms/TemporalString]])
|
||||
|
||||
(api.macros/defendpoint :post "/:name" :- ::ResponseSchema
|
||||
"Create a resource with a given name."
|
||||
[;; Route Params:
|
||||
{:keys [name]} :- [:map [:name ms/NonBlankString]]
|
||||
;; Query Params:
|
||||
{:keys [include archived]} :- [:map
|
||||
[:include {:optional true} [:maybe [:= "details"]]]
|
||||
[:archived {:default false} [:maybe ms/BooleanValue]]]
|
||||
;; Body Params:
|
||||
{:keys [color]} :- [:map [:color ::Color]]
|
||||
]
|
||||
;; endpoint implementation, ex:
|
||||
{:id 99
|
||||
:name (str "mr or mrs " name)
|
||||
:color ({"red" "blue" "blue" "green" "green" "red"} color)
|
||||
:created_at (t/format (t/formatter "yyyy-MM-dd'T'HH:mm:ssXXX") (t/zoned-date-time))}
|
||||
)
|
||||
```
|
||||
|
||||
## Common Schema Patterns
|
||||
|
||||
1. Route Params (the 5 in `api/user/id/5`)
|
||||
2. Query Params (the sort+asc pair in `api/users?sort=asc`)
|
||||
3. Body Params (the contents of a request body. Almost always decoded from json into edn)
|
||||
4. The Raw Request map
|
||||
|
||||
Of the 4 arguments, deprioritize usage of the raw request unless necessary.
|
||||
|
||||
### Route Params
|
||||
|
||||
Always required, typically just a map with an ID:
|
||||
|
||||
```clojure
|
||||
[{:keys [id]} :- [:map [:id ms/PositiveInt]]]
|
||||
```
|
||||
|
||||
For multiple route params:
|
||||
|
||||
```clojure
|
||||
[{:keys [id field-id]} :- [:map
|
||||
[:id ms/PositiveInt]
|
||||
[:field-id ms/PositiveInt]]]
|
||||
```
|
||||
|
||||
### Query Params
|
||||
|
||||
Add properties for `{:optional true ...}` and `:default` values:
|
||||
|
||||
```clojure
|
||||
{:keys [archived include limit offset]} :- [:map
|
||||
[:archived {:default false} [:maybe ms/BooleanValue]]
|
||||
[:include {:optional true} [:maybe [:= "tables"]]]
|
||||
[:limit {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:offset {:optional true} [:maybe ms/PositiveInt]]]
|
||||
```
|
||||
|
||||
### Request Body (POST/PUT)
|
||||
|
||||
```clojure
|
||||
{:keys [name description parent_id]} :- [:map
|
||||
[:name ms/NonBlankString]
|
||||
[:description {:optional true} [:maybe ms/NonBlankString]]
|
||||
[:parent_id {:optional true} [:maybe ms/PositiveInt]]]
|
||||
```
|
||||
|
||||
### Response Schemas
|
||||
|
||||
#### Simple inline response:
|
||||
|
||||
```clojure
|
||||
(api.macros/defendpoint :get "/:id" :- [:map
|
||||
[:id pos-int?]
|
||||
[:name string?]]
|
||||
"Get a thing"
|
||||
...)
|
||||
```
|
||||
|
||||
#### Named schema for reuse:
|
||||
|
||||
```clojure
|
||||
(mr/def ::Thing
|
||||
[:map
|
||||
[:id pos-int?]
|
||||
[:name string?]
|
||||
[:description [:maybe string?]]])
|
||||
|
||||
(api.macros/defendpoint :get "/:id" :- ::Thing
|
||||
"Get a thing"
|
||||
...)
|
||||
|
||||
(api.macros/defendpoint :get "/" :- [:sequential ::Thing]
|
||||
"Get all things"
|
||||
...)
|
||||
```
|
||||
|
||||
## Common Schema Types
|
||||
|
||||
### From `metabase.util.malli.schema` (aliased as `ms`)
|
||||
|
||||
Prefer the schemas in the ms/* namespace, since they work better with our api infrastructure.
|
||||
|
||||
For example use `ms/PositiveInt` instead of `pos-int?`.
|
||||
|
||||
```clojure
|
||||
ms/PositiveInt ;; Positive integer
|
||||
ms/NonBlankString ;; Non-empty string
|
||||
ms/BooleanValue ;; String "true"/"false" or boolean
|
||||
ms/MaybeBooleanValue ;; BooleanValue or nil
|
||||
ms/TemporalString ;; ISO-8601 date/time string (for REQUEST params only!)
|
||||
ms/Map ;; Any map
|
||||
ms/JSONString ;; JSON-encoded string
|
||||
ms/PositiveNum ;; Positive number
|
||||
ms/IntGreaterThanOrEqualToZero ;; 0 or positive
|
||||
```
|
||||
|
||||
**IMPORTANT:** For response schemas, use `:any` for temporal fields, not `ms/TemporalString`!
|
||||
Response schemas validate BEFORE JSON serialization, so they see Java Time objects.
|
||||
|
||||
### Built-in Malli Types
|
||||
|
||||
```clojure
|
||||
:string ;; Any string
|
||||
:boolean ;; true/false
|
||||
:int ;; Any integer
|
||||
:keyword ;; Clojure keyword
|
||||
pos-int? ;; Positive integer predicate
|
||||
[:maybe X] ;; X or nil
|
||||
[:enum "a" "b" "c"] ;; One of these values
|
||||
[:or X Y] ;; Schema that satisfies X or Y
|
||||
[:and X Y] ;; Schema that satisfies X and Y
|
||||
[:sequential X] ;; Sequential of Xs
|
||||
[:set X] ;; Set of Xs
|
||||
[:map-of K V] ;; Map with keys w/ schema K and values w/ schema V
|
||||
[:tuple X Y Z] ;; Fixed-length tuple of schemas X Y Z
|
||||
```
|
||||
|
||||
Avoid using sequence schemas unless completely necessary.
|
||||
|
||||
## Step-by-Step: Adding Schemas to an Endpoint
|
||||
|
||||
### Example: Adding return schema to `GET /api/field/:id/related`
|
||||
|
||||
**Before:**
|
||||
```clojure
|
||||
(api.macros/defendpoint :get "/:id/related"
|
||||
"Return related entities."
|
||||
[{:keys [id]} :- [:map [:id ms/PositiveInt]]]
|
||||
(-> (t2/select-one :model/Field :id id) api/read-check xrays/related))
|
||||
```
|
||||
|
||||
**Step 1:** Check what the function returns (look at `xrays/related`)
|
||||
|
||||
**Step 2:** Define response schema based on return type:
|
||||
|
||||
```clojure
|
||||
(mr/def ::RelatedEntity
|
||||
[:map
|
||||
[:tables [:sequential [:map [:id pos-int?] [:name string?]]]]
|
||||
[:fields [:sequential [:map [:id pos-int?] [:name string?]]]]])
|
||||
```
|
||||
|
||||
**Step 3:** Add response schema to endpoint:
|
||||
|
||||
```clojure
|
||||
(api.macros/defendpoint :get "/:id/related" :- ::RelatedEntity
|
||||
"Return related entities."
|
||||
[{:keys [id]} :- [:map [:id ms/PositiveInt]]]
|
||||
(-> (t2/select-one :model/Field :id id) api/read-check xrays/related))
|
||||
```
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Custom Error Messages
|
||||
|
||||
```clojure
|
||||
(def DBEngineString
|
||||
"Schema for a valid database engine name."
|
||||
(mu/with-api-error-message
|
||||
[:and
|
||||
ms/NonBlankString
|
||||
[:fn
|
||||
{:error/message "Valid database engine"}
|
||||
#(u/ignore-exceptions (driver/the-driver %))]]
|
||||
(deferred-tru "value must be a valid database engine.")))
|
||||
```
|
||||
|
||||
### Enum with Documentation
|
||||
|
||||
```clojure
|
||||
(def PinnedState
|
||||
(into [:enum {:error/message "pinned state must be 'all', 'is_pinned', or 'is_not_pinned'"}]
|
||||
#{"all" "is_pinned" "is_not_pinned"}))
|
||||
```
|
||||
|
||||
### Complex Nested Response
|
||||
|
||||
```clojure
|
||||
(mr/def ::DashboardQuestionCandidate
|
||||
[:map
|
||||
[:id ms/PositiveInt]
|
||||
[:name ms/NonBlankString]
|
||||
[:description [:maybe string?]]
|
||||
[:sole_dashboard_info
|
||||
[:map
|
||||
[:id ms/PositiveInt]
|
||||
[:name ms/NonBlankString]
|
||||
[:description [:maybe string?]]]]])
|
||||
|
||||
(mr/def ::DashboardQuestionCandidatesResponse
|
||||
[:map
|
||||
[:data [:sequential ::DashboardQuestionCandidate]]
|
||||
[:total ms/PositiveInt]])
|
||||
```
|
||||
|
||||
### Paginated Response Pattern
|
||||
|
||||
```clojure
|
||||
(mr/def ::PaginatedResponse
|
||||
[:map
|
||||
[:data [:sequential ::Item]]
|
||||
[:total integer?]
|
||||
[:limit {:optional true} [:maybe integer?]]
|
||||
[:offset {:optional true} [:maybe integer?]]])
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Don't: Forget `:maybe` for nullable fields
|
||||
|
||||
```clojure
|
||||
[:description ms/NonBlankString] ;; WRONG - fails if nil
|
||||
[:description [:maybe ms/NonBlankString]] ;; RIGHT - allows nil
|
||||
```
|
||||
|
||||
### Don't: Forget `:optional true` for optional query params
|
||||
|
||||
```clojure
|
||||
[:limit ms/PositiveInt] ;; WRONG - required but shouldn't be
|
||||
[:limit {:optional true} [:maybe ms/PositiveInt]] ;; RIGHT
|
||||
```
|
||||
|
||||
### Don't: Forget `:default` values for known params
|
||||
|
||||
```clojure
|
||||
[:limit ms/PositiveInt] ;; WRONG - required but shouldn't be
|
||||
[:limit {:optional true :default 0} [:maybe ms/PositiveInt]] ;; RIGHT
|
||||
```
|
||||
|
||||
|
||||
### Don't: Mix up route params, query params, and body
|
||||
|
||||
```clojure
|
||||
;; WRONG - all in one map
|
||||
[{:keys [id name archived]} :- [:map ...]]
|
||||
|
||||
;; RIGHT - separate destructuring
|
||||
[{:keys [id]} :- [:map [:id ms/PositiveInt]]
|
||||
{:keys [archived]} :- [:map [:archived {:default false} ms/BooleanValue]]
|
||||
{:keys [name]} :- [:map [:name ms/NonBlankString]]]
|
||||
```
|
||||
|
||||
### Don't: Use `ms/TemporalString` for Java Time objects in response schemas
|
||||
|
||||
```clojure
|
||||
;; WRONG - Java Time objects aren't strings yet
|
||||
[:date_joined ms/TemporalString]
|
||||
|
||||
;; RIGHT - schemas validate BEFORE JSON serialization
|
||||
[:date_joined :any] ;; Java Time object, serialized to string by middleware
|
||||
[:last_login [:maybe :any]] ;; Java Time object or nil
|
||||
```
|
||||
|
||||
**Why:** Response schemas validate the internal Clojure data structures BEFORE they are serialized to JSON. Java Time objects like `OffsetDateTime` get converted to ISO-8601 strings by the JSON middleware, so the schema needs to accept the raw Java objects.
|
||||
|
||||
### Don't: Use `[:sequential X]` when the data is actually a set
|
||||
|
||||
```clojure
|
||||
;; WRONG - group_ids is actually a set
|
||||
[:group_ids {:optional true} [:sequential pos-int?]]
|
||||
|
||||
;; RIGHT - matches the actual data structure
|
||||
[:group_ids {:optional true} [:maybe [:set pos-int?]]]
|
||||
```
|
||||
|
||||
**Why:** Toucan hydration methods often return sets. The JSON middleware will serialize sets to arrays, but the schema validates before serialization.
|
||||
|
||||
### Don't: Create anonymous schemas for reused structures
|
||||
|
||||
Use `mr/def` for schemas used in multiple places:
|
||||
|
||||
```clojure
|
||||
(mr/def ::User
|
||||
[:map
|
||||
[:id pos-int?]
|
||||
[:email string?]
|
||||
[:name string?]])
|
||||
```
|
||||
|
||||
## Finding Return Types
|
||||
|
||||
1. **Look at the function being called**
|
||||
|
||||
```clojure
|
||||
(api.macros/defendpoint :get "/:id"
|
||||
[{:keys [id]}]
|
||||
(t2/select-one :model/Field :id id)) ;; Returns a Field instance
|
||||
```
|
||||
|
||||
2. **Check Toucan models for structure**
|
||||
|
||||
Look in `src/metabase/*/models/*.clj` for model definitions.
|
||||
|
||||
3. **Use clojure-mcp or REPL to inspect**
|
||||
|
||||
```bash
|
||||
./bin/mage -repl '(require '\''metabase.xrays.core) (doc metabase.xrays.core/related)'
|
||||
```
|
||||
|
||||
4. **Check tests**
|
||||
|
||||
Tests often show the expected response structure.
|
||||
|
||||
## Understanding Schema Validation Timing
|
||||
|
||||
**CRITICAL CONCEPT:** Schemas validate at different points in the request/response lifecycle:
|
||||
|
||||
### Request Parameter Schemas (Query/Body/Route)
|
||||
- Validate AFTER JSON parsing
|
||||
- Data is already deserialized (strings, numbers, booleans)
|
||||
- Use `ms/TemporalString` for date/time inputs
|
||||
- Use `ms/BooleanValue` for boolean query params
|
||||
|
||||
### Response Schemas
|
||||
- Validate BEFORE JSON serialization
|
||||
- Data is still in Clojure format (Java Time objects, sets, keywords)
|
||||
- Use `:any` for Java Time objects
|
||||
- Use `[:set X]` for sets
|
||||
- Use `[:enum :keyword]` for keyword enums
|
||||
|
||||
### Serialization Flow
|
||||
|
||||
```
|
||||
Request: JSON string → Parse → Coerce → Handler
|
||||
Response: Handler → Schema Check → Encode → Serialize → JSON string
|
||||
```
|
||||
|
||||
## Workflow Summary
|
||||
|
||||
1. **Read the endpoint** - understand what it does
|
||||
2. **Identify params** - route, query, body
|
||||
3. **Add parameter schemas** - use existing types from `ms`
|
||||
4. **Determine return type** - check the implementation
|
||||
5. **Define response schema** - inline or named with `mr/def`
|
||||
6. **Test** - ensure the endpoint works and validates correctly
|
||||
|
||||
## Testing Your Schemas
|
||||
|
||||
After adding schemas, verify:
|
||||
|
||||
1. **Valid requests work** - test with correct data
|
||||
2. **Invalid requests fail gracefully** - test with wrong types
|
||||
3. **Optional params work** - test with/without optional params
|
||||
4. **Error messages are clear** - check validation error responses
|
||||
|
||||
## Tips
|
||||
|
||||
- **Start simple** - begin with basic types, refine later
|
||||
- **Reuse schemas** - if you see the same structure twice, make it a named schema
|
||||
- **Be specific** - use `ms/PositiveInt` instead of `pos-int?`
|
||||
- **Document intent** - add docstrings to named schemas
|
||||
- **Follow conventions** - look at similar endpoints in the same namespace
|
||||
- **Check the actual data** - use REPL to inspect what's actually returned before serialization
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Malli Documentation](https://github.com/metosin/malli)
|
||||
- Metabase Malli utilities: `src/metabase/util/malli/schema.clj`
|
||||
- Metabase schema registry: `src/metabase/util/malli/registry.clj`
|
||||
@@ -1,504 +0,0 @@
|
||||
---
|
||||
name: analytics-events
|
||||
description: Add product analytics events to track user interactions in the Metabase frontend
|
||||
allowed-tools: Read, Write, Edit, Grep, Glob
|
||||
---
|
||||
|
||||
# Frontend Analytics Events Skill
|
||||
|
||||
This skill helps you add product analytics (Snowplow) events to track user interactions in the Metabase frontend codebase.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
Analytics events in Metabase use Snowplow with typed event schemas. All events must be defined in TypeScript types before use.
|
||||
|
||||
**Key Files:**
|
||||
- `frontend/src/metabase-types/analytics/event.ts` - Event type definitions
|
||||
- `frontend/src/metabase-types/analytics/schema.ts` - Schema registry
|
||||
- `frontend/src/metabase/lib/analytics.ts` - Core tracking functions
|
||||
- Feature-specific `analytics.ts` files - Tracking function wrappers
|
||||
|
||||
## Quick Checklist
|
||||
|
||||
When adding a new analytics event:
|
||||
|
||||
- [ ] Define event type in `frontend/src/metabase-types/analytics/event.ts`
|
||||
- [ ] Add event to appropriate union type (e.g., `DataStudioEvent`, `SimpleEvent`)
|
||||
- [ ] Create tracking function in feature's `analytics.ts` file
|
||||
- [ ] Import and call tracking function at the interaction point
|
||||
- [ ] Use `trackSimpleEvent()` for basic events (most common)
|
||||
|
||||
## Event Schema Types
|
||||
|
||||
### 1. Simple Events (Most Common)
|
||||
|
||||
Use `SimpleEventSchema` for straightforward tracking. It supports these standard fields:
|
||||
|
||||
```typescript
|
||||
type SimpleEventSchema = {
|
||||
event: string; // Required: Event name (snake_case)
|
||||
target_id?: number | null; // Optional: ID of affected entity
|
||||
triggered_from?: string | null; // Optional: UI location/context
|
||||
duration_ms?: number | null; // Optional: Duration in milliseconds
|
||||
result?: string | null; // Optional: Outcome (e.g., "success", "failure")
|
||||
event_detail?: string | null; // Optional: Additional detail/variant
|
||||
};
|
||||
```
|
||||
|
||||
**When to use:** 90% of events fit this schema. Use for clicks, opens, closes, creates, deletes, etc.
|
||||
|
||||
### 2. Custom Schemas (legacy, no events are being added)
|
||||
|
||||
Consider adding new event schema only in very special cases.
|
||||
|
||||
**Examples:** `DashboardEventSchema`, `CleanupEventSchema`, `QuestionEventSchema`
|
||||
|
||||
## Step-by-Step: Adding a Simple Event
|
||||
|
||||
### Example: Track when a user applies filters in a table picker
|
||||
|
||||
#### Step 1: Define Event Types
|
||||
|
||||
Add event type definitions to `frontend/src/metabase-types/analytics/event.ts`:
|
||||
|
||||
```typescript
|
||||
export type DataStudioTablePickerFiltersAppliedEvent = ValidateEvent<{
|
||||
event: "data_studio_table_picker_filters_applied";
|
||||
}>;
|
||||
|
||||
export type DataStudioTablePickerFiltersClearedEvent = ValidateEvent<{
|
||||
event: "data_studio_table_picker_filters_cleared";
|
||||
}>;
|
||||
```
|
||||
|
||||
#### Step 2: Add to Union Type
|
||||
|
||||
Find or create the appropriate union type and add your events:
|
||||
|
||||
```typescript
|
||||
export type DataStudioEvent =
|
||||
| DataStudioLibraryCreatedEvent
|
||||
| DataStudioTablePublishedEvent
|
||||
| DataStudioGlossaryCreatedEvent
|
||||
| DataStudioGlossaryEditedEvent
|
||||
| DataStudioGlossaryDeletedEvent
|
||||
| DataStudioTablePickerFiltersAppliedEvent // <- Add here
|
||||
| DataStudioTablePickerFiltersClearedEvent; // <- Add here
|
||||
```
|
||||
|
||||
#### Step 3: Create Tracking Functions
|
||||
|
||||
In your feature's `analytics.ts` file (e.g., `enterprise/frontend/src/metabase-enterprise/data-studio/analytics.ts`):
|
||||
|
||||
```typescript
|
||||
import { trackSimpleEvent } from "metabase/lib/analytics";
|
||||
|
||||
export const trackDataStudioTablePickerFiltersApplied = () => {
|
||||
trackSimpleEvent({
|
||||
event: "data_studio_table_picker_filters_applied",
|
||||
});
|
||||
};
|
||||
|
||||
export const trackDataStudioTablePickerFiltersCleared = () => {
|
||||
trackSimpleEvent({
|
||||
event: "data_studio_table_picker_filters_cleared",
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
#### Step 4: Use in Components
|
||||
|
||||
Import and call the tracking function at the interaction point:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
trackDataStudioTablePickerFiltersApplied,
|
||||
trackDataStudioTablePickerFiltersCleared,
|
||||
} from "metabase-enterprise/data-studio/analytics";
|
||||
|
||||
function FilterPopover({ filters, onSubmit }) {
|
||||
const handleReset = () => {
|
||||
trackDataStudioTablePickerFiltersCleared(); // <- Track here
|
||||
onSubmit(emptyFilters);
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
trackDataStudioTablePickerFiltersApplied(); // <- Track here
|
||||
onSubmit(form);
|
||||
}}
|
||||
>
|
||||
{/* form content */}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Using SimpleEventSchema Fields
|
||||
|
||||
### Example: Event with target_id
|
||||
|
||||
```typescript
|
||||
// Type definition
|
||||
export type DataStudioLibraryCreatedEvent = ValidateEvent<{
|
||||
event: "data_studio_library_created";
|
||||
target_id: number | null;
|
||||
}>;
|
||||
|
||||
// Tracking function
|
||||
export const trackDataStudioLibraryCreated = (id: CollectionId) => {
|
||||
trackSimpleEvent({
|
||||
event: "data_studio_library_created",
|
||||
target_id: Number(id),
|
||||
});
|
||||
};
|
||||
|
||||
// Usage
|
||||
trackDataStudioLibraryCreated(newLibrary.id);
|
||||
```
|
||||
|
||||
### Example: Event with triggered_from
|
||||
|
||||
```typescript
|
||||
// Type definition
|
||||
export type NewButtonClickedEvent = ValidateEvent<{
|
||||
event: "new_button_clicked";
|
||||
triggered_from: "app-bar" | "empty-collection";
|
||||
}>;
|
||||
|
||||
// Tracking function
|
||||
export const trackNewButtonClicked = (location: "app-bar" | "empty-collection") => {
|
||||
trackSimpleEvent({
|
||||
event: "new_button_clicked",
|
||||
triggered_from: location,
|
||||
});
|
||||
};
|
||||
|
||||
// Usage
|
||||
<Button onClick={() => {
|
||||
trackNewButtonClicked("app-bar");
|
||||
handleCreate();
|
||||
}}>
|
||||
New
|
||||
</Button>
|
||||
```
|
||||
|
||||
### Example: Event with event_detail
|
||||
|
||||
```typescript
|
||||
// Type definition
|
||||
export type MetadataEditEvent = ValidateEvent<{
|
||||
event: "metadata_edited";
|
||||
event_detail: "type_casting" | "semantic_type_change" | "visibility_change";
|
||||
triggered_from: "admin" | "data_studio";
|
||||
}>;
|
||||
|
||||
// Tracking function
|
||||
export const trackMetadataChange = (
|
||||
detail: "type_casting" | "semantic_type_change" | "visibility_change",
|
||||
location: "admin" | "data_studio"
|
||||
) => {
|
||||
trackSimpleEvent({
|
||||
event: "metadata_edited",
|
||||
event_detail: detail,
|
||||
triggered_from: location,
|
||||
});
|
||||
};
|
||||
|
||||
// Usage
|
||||
trackMetadataChange("semantic_type_change", "data_studio");
|
||||
```
|
||||
|
||||
### Example: Event with result and duration
|
||||
|
||||
```typescript
|
||||
// Type definition
|
||||
export type MoveToTrashEvent = ValidateEvent<{
|
||||
event: "moved-to-trash";
|
||||
target_id: number | null;
|
||||
triggered_from: "collection" | "detail_page" | "cleanup_modal";
|
||||
duration_ms: number | null;
|
||||
result: "success" | "failure";
|
||||
event_detail: "question" | "model" | "metric" | "dashboard";
|
||||
}>;
|
||||
|
||||
// Tracking function
|
||||
export const trackMoveToTrash = (params: {
|
||||
targetId: number | null;
|
||||
triggeredFrom: "collection" | "detail_page" | "cleanup_modal";
|
||||
durationMs: number | null;
|
||||
result: "success" | "failure";
|
||||
itemType: "question" | "model" | "metric" | "dashboard";
|
||||
}) => {
|
||||
trackSimpleEvent({
|
||||
event: "moved-to-trash",
|
||||
target_id: params.targetId,
|
||||
triggered_from: params.triggeredFrom,
|
||||
duration_ms: params.durationMs,
|
||||
result: params.result,
|
||||
event_detail: params.itemType,
|
||||
});
|
||||
};
|
||||
|
||||
// Usage with timing
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
await moveToTrash(item);
|
||||
trackMoveToTrash({
|
||||
targetId: item.id,
|
||||
triggeredFrom: "collection",
|
||||
durationMs: Date.now() - startTime,
|
||||
result: "success",
|
||||
itemType: "question",
|
||||
});
|
||||
} catch (error) {
|
||||
trackMoveToTrash({
|
||||
targetId: item.id,
|
||||
triggeredFrom: "collection",
|
||||
durationMs: Date.now() - startTime,
|
||||
result: "failure",
|
||||
itemType: "question",
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
### Event Names (snake_case)
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
"data_studio_library_created"
|
||||
"table_picker_filters_applied"
|
||||
"metabot_chat_opened"
|
||||
|
||||
// Bad
|
||||
"DataStudioLibraryCreated" // Wrong case
|
||||
"tablePickerFiltersApplied" // Wrong case
|
||||
"filters-applied" // Use underscore, not hyphen
|
||||
```
|
||||
|
||||
### Event Type Names (PascalCase with "Event" suffix)
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
DataStudioLibraryCreatedEvent
|
||||
TablePickerFiltersAppliedEvent
|
||||
MetabotChatOpenedEvent
|
||||
|
||||
// Bad
|
||||
dataStudioLibraryCreated // Wrong case
|
||||
DataStudioLibraryCreated // Missing "Event" suffix
|
||||
```
|
||||
|
||||
### Tracking Function Names (camelCase with "track" prefix)
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
trackDataStudioLibraryCreated
|
||||
trackTablePickerFiltersApplied
|
||||
trackMetabotChatOpened
|
||||
|
||||
// Bad
|
||||
DataStudioLibraryCreated // Missing "track" prefix
|
||||
track_library_created // Wrong case
|
||||
logLibraryCreated // Use "track" prefix
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Feature-Specific Union Types
|
||||
|
||||
Group related events together:
|
||||
|
||||
```typescript
|
||||
export type DataStudioEvent =
|
||||
| DataStudioLibraryCreatedEvent
|
||||
| DataStudioTablePublishedEvent
|
||||
| DataStudioGlossaryCreatedEvent;
|
||||
|
||||
export type MetabotEvent =
|
||||
| MetabotChatOpenedEvent
|
||||
| MetabotRequestSentEvent
|
||||
| MetabotFixQueryClickedEvent;
|
||||
|
||||
// Then add to SimpleEvent union
|
||||
export type SimpleEvent =
|
||||
| /* other events */
|
||||
| DataStudioEvent
|
||||
| MetabotEvent
|
||||
| /* more events */;
|
||||
```
|
||||
|
||||
### Pattern 2: Conditional Tracking
|
||||
|
||||
Track different events based on user action:
|
||||
|
||||
```typescript
|
||||
const handleSave = async () => {
|
||||
if (isNewItem) {
|
||||
await createItem(data);
|
||||
trackItemCreated(newItem.id);
|
||||
} else {
|
||||
await updateItem(id, data);
|
||||
trackItemUpdated(id);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Don't: Add custom fields to SimpleEvent
|
||||
|
||||
```typescript
|
||||
// WRONG - SimpleEvent doesn't support custom fields
|
||||
export const trackFiltersApplied = (filters: FilterState) => {
|
||||
trackSimpleEvent({
|
||||
event: "filters_applied",
|
||||
data_layer: filters.dataLayer, // ❌ Not in SimpleEventSchema
|
||||
data_source: filters.dataSource, // ❌ Not in SimpleEventSchema
|
||||
with_owner: filters.hasOwner, // ❌ Not in SimpleEventSchema
|
||||
});
|
||||
};
|
||||
|
||||
// RIGHT - Use only standard SimpleEventSchema fields
|
||||
export const trackFiltersApplied = () => {
|
||||
trackSimpleEvent({
|
||||
event: "filters_applied",
|
||||
});
|
||||
};
|
||||
|
||||
// Or use event_detail for a single variant
|
||||
export const trackFilterApplied = (filterType: string) => {
|
||||
trackSimpleEvent({
|
||||
event: "filter_applied",
|
||||
event_detail: filterType, // ✓ "data_layer", "data_source", etc.
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### Don't: Forget to add event to union type
|
||||
|
||||
```typescript
|
||||
// Define the event
|
||||
export type NewFeatureClickedEvent = ValidateEvent<{
|
||||
event: "new_feature_clicked";
|
||||
}>;
|
||||
|
||||
// ❌ WRONG - Forgot to add to SimpleEvent union
|
||||
// Event won't be recognized by TypeScript
|
||||
|
||||
// ✓ RIGHT - Add to appropriate union
|
||||
export type SimpleEvent =
|
||||
| /* other events */
|
||||
| NewFeatureClickedEvent;
|
||||
```
|
||||
|
||||
### Don't: Mix up event name formats
|
||||
|
||||
```typescript
|
||||
// WRONG
|
||||
event: "dataStudioLibraryCreated" // camelCase
|
||||
event: "data-studio-library-created" // kebab-case
|
||||
event: "Data_Studio_Library_Created" // Mixed case
|
||||
|
||||
// RIGHT
|
||||
event: "data_studio_library_created" // snake_case
|
||||
```
|
||||
|
||||
### Don't: Track PII or sensitive data
|
||||
|
||||
```typescript
|
||||
// WRONG - Don't track user emails, names, or sensitive data
|
||||
trackSimpleEvent({
|
||||
event: "user_logged_in",
|
||||
event_detail: user.email, // ❌ PII
|
||||
});
|
||||
|
||||
// RIGHT - Track non-sensitive identifiers only
|
||||
trackSimpleEvent({
|
||||
event: "user_logged_in",
|
||||
target_id: user.id, // ✓ Just the ID
|
||||
});
|
||||
```
|
||||
|
||||
### Don't: Forget to track both success and failure
|
||||
|
||||
```typescript
|
||||
// WRONG - Only tracking success
|
||||
try {
|
||||
await saveData();
|
||||
trackDataSaved();
|
||||
} catch (error) {
|
||||
// ❌ No tracking for failure case
|
||||
}
|
||||
|
||||
// RIGHT - Track both outcomes
|
||||
try {
|
||||
await saveData();
|
||||
trackDataSaved({ result: "success" });
|
||||
} catch (error) {
|
||||
trackDataSaved({ result: "failure" });
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Analytics Events
|
||||
|
||||
While developing, you can verify events are firing:
|
||||
|
||||
1. **Check browser console** - When `SNOWPLOW_ENABLED=true` in dev, events are logged
|
||||
2. **Use shouldLogAnalytics** - Set in `metabase/env` to see all analytics in console
|
||||
3. **Check Snowplow debugger** - Browser extension for Snowplow events
|
||||
|
||||
Example console output:
|
||||
|
||||
```
|
||||
[SNOWPLOW EVENT | event sent:true], data_studio_table_picker_filters_applied
|
||||
```
|
||||
|
||||
## File Organization
|
||||
|
||||
### Where to put tracking functions:
|
||||
|
||||
```
|
||||
Feature-specific analytics functions:
|
||||
frontend/src/metabase/{feature}/analytics.ts
|
||||
enterprise/frontend/src/metabase-enterprise/{feature}/analytics.ts
|
||||
|
||||
Event type definitions (all in one place):
|
||||
frontend/src/metabase-types/analytics/event.ts
|
||||
|
||||
Core tracking utilities:
|
||||
frontend/src/metabase/lib/analytics.ts
|
||||
```
|
||||
|
||||
## Real-World Examples
|
||||
|
||||
See these files for reference:
|
||||
|
||||
- **Simple events**: `enterprise/frontend/src/metabase-enterprise/data-studio/analytics.ts`
|
||||
- **Events with variants**: `frontend/src/metabase/dashboard/analytics.ts`
|
||||
- **Complex events**: `frontend/src/metabase/query_builder/analytics.js`
|
||||
- **Event type examples**: `frontend/src/metabase-types/analytics/event.ts`
|
||||
|
||||
## Workflow Summary
|
||||
|
||||
1. **Identify the user interaction** to track
|
||||
2. **Decide on event name** (snake_case, descriptive)
|
||||
3. **Define event type** in `event.ts` using `ValidateEvent`
|
||||
4. **Add to union type** (create feature union if needed)
|
||||
5. **Create tracking function** in feature's `analytics.ts`
|
||||
6. **Import and call** at the interaction point
|
||||
7. **Test** that events fire correctly
|
||||
|
||||
## Tips
|
||||
|
||||
- **Be specific** - `filters_applied` is better than `action_performed`
|
||||
- **Use past tense** - `library_created` not `create_library`
|
||||
- **Group related events** - Create feature-specific event union types
|
||||
- **Track meaningful actions** - Not every click needs tracking
|
||||
- **Consider the data** - What would you want to analyze later?
|
||||
- **Stay consistent** - Follow existing naming patterns in the codebase
|
||||
- **Document context** - Use `triggered_from` to track where the action happened
|
||||
@@ -1,144 +0,0 @@
|
||||
---
|
||||
name: clojure-review
|
||||
description: Review Clojure and ClojureScript code changes for compliance with Metabase coding standards, style violations, and code quality issues. Use when reviewing pull requests or diffs containing Clojure/ClojureScript code.
|
||||
allowed-tools: Read, Grep, Bash, Glob
|
||||
---
|
||||
|
||||
# Clojure Code Review Skill
|
||||
|
||||
@./../_shared/clojure-style-guide.md
|
||||
@./../_shared/clojure-commands.md
|
||||
|
||||
## Review guidelines
|
||||
|
||||
**What to flag:**
|
||||
|
||||
- Check compliance with the Metabase Clojure style guide (included above)
|
||||
- If `CLOJURE_STYLE_GUIDE.adoc` exists in the working directory, also check compliance with the community Clojure style guide
|
||||
- Flag all style guide violations
|
||||
|
||||
**What NOT to post:**
|
||||
|
||||
- Do not post comments congratulating someone for trivial changes or for following style guidelines
|
||||
- Do not post comments confirming things "look good" or telling them they did something correctly
|
||||
- Only post comments about style violations or potential issues
|
||||
|
||||
Example bad code review comments to avoid:
|
||||
|
||||
> This TODO comment is properly formatted with author and date - nice work!
|
||||
|
||||
> Good addition of limit 1 to the query - this makes the test more efficient without changing its behavior.
|
||||
|
||||
> The kondo ignore comment is appropriately placed here
|
||||
|
||||
> Test name properly ends with -test as required by the style guide.
|
||||
|
||||
**Special cases:**
|
||||
|
||||
- Do not post comments about missing parentheses (these will be caught by the linter)
|
||||
|
||||
## Quick review checklist
|
||||
|
||||
Use this to scan through changes efficiently:
|
||||
|
||||
### Naming
|
||||
|
||||
- [ ] Descriptive names (no `tbl`, `zs'`)
|
||||
- [ ] Pure functions named as nouns describing their return value
|
||||
- [ ] `kebab-case` for all variables and functions
|
||||
- [ ] Side-effect functions end with `!`
|
||||
- [ ] No namespace-alias repetition in function names
|
||||
|
||||
### Documentation
|
||||
|
||||
- [ ] Public vars in `src` or `enterprise/backend/src` have useful docstrings
|
||||
- [ ] Docstrings use Markdown conventions
|
||||
- [ ] References use `[[other-var]]` not backticks
|
||||
- [ ] `TODO` comments include author and date: `;; TODO (Name 2025-01-01) -- description`
|
||||
|
||||
### Code Organization
|
||||
|
||||
- [ ] Everything `^:private` unless used elsewhere
|
||||
- [ ] No `declare` when avoidable (public functions near end)
|
||||
- [ ] Functions under 20 lines when possible
|
||||
- [ ] No blank, non-comment lines within definition forms (except pairwise constructs in `let`/`cond`)
|
||||
- [ ] Lines ≤ 120 characters
|
||||
|
||||
### Tests
|
||||
|
||||
- [ ] Separate `deftest` forms for distinct test cases
|
||||
- [ ] Pure tests marked `^:parallel`
|
||||
- [ ] Test names end in `-test` or `-test-<number>`
|
||||
|
||||
### Modules
|
||||
|
||||
- [ ] Correct module patterns (OSS: `metabase.<module>.*`, EE: `metabase-enterprise.<module>.*`)
|
||||
- [ ] API endpoints in `<module>.api` namespaces
|
||||
- [ ] Public API in `<module>.core` with Potemkin
|
||||
- [ ] No cheating module linters with `:clj-kondo/ignore [:metabase/modules]`
|
||||
|
||||
### REST API
|
||||
|
||||
- [ ] Response schemas present (`:- <schema>`)
|
||||
- [ ] Query params use kebab-case, bodies use `snake_case`
|
||||
- [ ] Routes use singular nouns (e.g., `/api/dashboard/:id`)
|
||||
- [ ] `GET` has no side effects (except analytics)
|
||||
- [ ] Malli schemas detailed and complete
|
||||
- [ ] All new endpoints have tests
|
||||
|
||||
### MBQL
|
||||
|
||||
- [ ] No raw MBQL manipulation outside `lib`, `lib-be`, or `query-processor` modules
|
||||
- [ ] Uses Lib and MBQL 5, not legacy MBQL
|
||||
|
||||
### Database
|
||||
|
||||
- [ ] Model and table names are singular nouns
|
||||
- [ ] Uses `t2/select-one-fn` instead of selecting full rows for one column
|
||||
- [ ] Logic in Toucan methods, not helper functions
|
||||
|
||||
### Drivers
|
||||
|
||||
- [ ] New multimethods documented in `docs/developers-guide/driver-changelog.md`
|
||||
- [ ] Passes `driver` argument to other driver methods (no hardcoded driver names)
|
||||
- [ ] Minimal logic in `read-column-thunk`
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- [ ] Example data is bird-themed when possible
|
||||
- [ ] Kondo linter suppressions use proper format (not `#_:clj-kondo/ignore` keyword form)
|
||||
|
||||
## Pattern matching table
|
||||
|
||||
Quick scan for common issues:
|
||||
|
||||
| Pattern | Issue |
|
||||
| -------------------------------------------- | ----------------------------------------------------------- |
|
||||
| `calculate-age`, `get-user` | Pure functions should be nouns: `age`, `user` |
|
||||
| `update-db`, `save-model` | Missing `!` for side effects: `update-db!`, `save-model!` |
|
||||
| `snake_case_var` | Should use kebab-case |
|
||||
| Public var without docstring | Add docstring explaining purpose |
|
||||
| `;; TODO fix this` | Missing author/date: `;; TODO (Name 2025-01-01) -- description` |
|
||||
| `(defn foo ...)` in namespace used elsewhere | Should be `(defn ^:private foo ...)` |
|
||||
| Function > 20 lines | Consider breaking up into smaller functions |
|
||||
| `/api/dashboards/:id` | Use singular: `/api/dashboard/:id` |
|
||||
| Query params with `snake_case` | Use kebab-case for query params |
|
||||
| New API endpoint without tests | Add tests for the endpoint |
|
||||
|
||||
## Feedback format examples
|
||||
|
||||
**For style violations:**
|
||||
|
||||
> This pure function should be named as a noun describing its return value. Consider `user` instead of `get-user`.
|
||||
|
||||
**For missing documentation:**
|
||||
|
||||
> This public var needs a docstring explaining its purpose, inputs, and outputs.
|
||||
|
||||
**For organization issues:**
|
||||
|
||||
> This function is only used in this namespace, so it should be marked `^:private`.
|
||||
|
||||
**For API conventions:**
|
||||
|
||||
> Query parameters should use kebab-case. Change `user_id` to `user-id`.
|
||||
@@ -1,69 +0,0 @@
|
||||
---
|
||||
name: clojure-write
|
||||
description: Guide Clojure and ClojureScript development using REPL-driven workflow, coding conventions, and best practices. Use when writing, developing, or refactoring Clojure/ClojureScript code.
|
||||
---
|
||||
|
||||
# Clojure Development Skill
|
||||
|
||||
## Tool Preference
|
||||
|
||||
When `clojure-mcp` tools are available (e.g., `clojure_eval`, `clojure_edit`), **always use them**
|
||||
instead of shell commands like `./bin/mage -repl`. The MCP tools provide:
|
||||
- Direct REPL integration without shell escaping issues
|
||||
- Better error messages and feedback
|
||||
- Structural Clojure editing that prevents syntax errors
|
||||
|
||||
Only fall back to `./bin/mage` commands when clojure-mcp is not available.
|
||||
|
||||
@./../_shared/development-workflow.md
|
||||
@./../_shared/clojure-style-guide.md
|
||||
@./../_shared/clojure-commands.md
|
||||
|
||||
## REPL-Driven Development Workflow
|
||||
|
||||
- Start with small, fundamental functions:
|
||||
- Identify the core features or functionalities required for your task.
|
||||
- Break each feature down into the smallest, most basic functions that can be developed and tested independently.
|
||||
- Write and test in the REPL:
|
||||
- Write the code for each small function directly in the REPL (Read-Eval-Print Loop).
|
||||
- Test it thoroughly with a variety of inputs, including typical use cases and relevant edge cases, to ensure it
|
||||
behaves as expected.
|
||||
- Integrate into source code:
|
||||
- Once a function works correctly in the REPL, move it from the REPL environment into your source code files (e.g.,
|
||||
within appropriate namespaces).
|
||||
- Gradually increase complexity:
|
||||
- Build upon tested, basic functions to create more complex functions or components.
|
||||
- Compose smaller functions together, testing each new composition in the REPL to verify correctness step by step.
|
||||
- Ensure dependency testing:
|
||||
- Make sure every function is fully tested in the REPL before it is depended upon by other functions.
|
||||
- This ensures that each layer of your application is reliable before you build on it.
|
||||
- Use the REPL fully:
|
||||
- Use the REPL as your primary tool to experiment with different approaches, iterate quickly, and get immediate
|
||||
feedback on your code.
|
||||
- Follow functional programming principles:
|
||||
- Keep functions small, focused, and composable.
|
||||
- Use Clojure's functional programming features—like immutability, higher-order functions, and the standard
|
||||
library—to write concise, effective code.
|
||||
|
||||
## How to Evaluate Code
|
||||
|
||||
### Bottom-up Dev Loop
|
||||
|
||||
1. Write code into a file.
|
||||
2. Evaluate the file's namespace and make sure it loads correctly with:
|
||||
|
||||
```
|
||||
./bin/mage -repl --namespace metabase.app-db.connection
|
||||
```
|
||||
|
||||
3. Call functions in the namespace with test inputs, and observe that the outputs are correct
|
||||
Feel free to copy these REPL session trials into actual test cases using `deftest` and `is`.
|
||||
4. Once you know these functions are good, return to 1, and compose them into the task that you need to build.
|
||||
|
||||
## Critical Rules for Editing
|
||||
|
||||
- Be careful with parentheses counts when editing Clojure code
|
||||
- After EVERY change to Clojure code, verify readability with `-check-readable`
|
||||
- End all files with a newline
|
||||
- When editing tabular code, where the columns line up, try to keep them aligned
|
||||
- Spaces on a line with nothing after it is not allowed
|
||||
@@ -1,163 +0,0 @@
|
||||
---
|
||||
name: docs-review
|
||||
description: Review documentation changes for compliance with the Metabase writing style guide. Use when reviewing pull requests, files, or diffs containing documentation markdown files.
|
||||
allowed-tools: Read, Grep, Bash, Glob
|
||||
---
|
||||
|
||||
# Documentation Review Skill
|
||||
|
||||
@./../_shared/metabase-style-guide.md
|
||||
|
||||
## Review mode detection
|
||||
|
||||
**IMPORTANT: Before starting the review, determine which mode to use:**
|
||||
|
||||
1. **PR review mode**: If the `mcp__github__create_pending_pull_request_review` tool is available, you are reviewing a GitHub PR
|
||||
- Use the pending review workflow to post all issues as one cohesive review
|
||||
- Follow the workflow steps in "PR review mode format" below
|
||||
|
||||
2. **Local review mode**: If the MCP tool is NOT available, output issues in the conversation
|
||||
- Format all issues in a numbered markdown list (as described in "Feedback format" below)
|
||||
|
||||
## Review process
|
||||
|
||||
1. **Detect review mode** - Check if `mcp__github__create_pending_pull_request_review` is available
|
||||
2. Read the changes through once to understand intent
|
||||
3. Check all issues that violate style guide or significantly impact readability
|
||||
4. Only flag issues worth mentioning - if it won't make a material difference to the reader, skip it
|
||||
5. **REQUIRED: Number ALL feedback sequentially** - Start from Issue 1 and increment for each issue found
|
||||
|
||||
## Review checklist
|
||||
|
||||
Run through the diff looking for these issues:
|
||||
|
||||
**Tone and voice:**
|
||||
|
||||
- [ ] Formal/corporate language ("utilize" not "use", "offerings", etc.)
|
||||
- [ ] "Users" instead of "people" or "companies"
|
||||
- [ ] Excessive exclamation points or overly peppy tone
|
||||
- [ ] Telling readers something is cool instead of showing them
|
||||
|
||||
**Structure and clarity:**
|
||||
|
||||
- [ ] Important information buried instead of leading
|
||||
- [ ] Verbose text that adds little value
|
||||
- [ ] Paragraphs without clear purpose
|
||||
- [ ] Vague headings that don't convey the point
|
||||
- [ ] Instructions explain "why" before telling "what to do"
|
||||
- [ ] Tasks described as "easy" or "simple"
|
||||
|
||||
**Links and references:**
|
||||
|
||||
- [ ] Linking the word "here" instead of descriptive text
|
||||
- [ ] Links in headings (unless entire heading is a link)
|
||||
|
||||
**Formatting:**
|
||||
|
||||
- [ ] Ampersands as "and" substitute (except proper nouns)
|
||||
- [ ] Inconsistent list formatting
|
||||
|
||||
**Code and examples:**
|
||||
|
||||
- [ ] Code examples that don't work or would error
|
||||
- [ ] Commands not in execution order
|
||||
- [ ] Full-width screenshots instead of scoped UI elements
|
||||
- [ ] Excessive or unnecessary images
|
||||
|
||||
**Sentence construction:**
|
||||
|
||||
- [ ] Overuse of pronouns when introducing new terms
|
||||
|
||||
## Quick scan table
|
||||
|
||||
| Pattern | Issue |
|
||||
| ----------------------------- | --------------------------------------------- |
|
||||
| we can do X, our feature | Should be "Metabase" or "it" |
|
||||
| click here, read more here | Need descriptive link text |
|
||||
| easy, simple, just | Remove condescending qualifiers |
|
||||
| users | Should be "people" or "companies" if possible |
|
||||
|
||||
## Feedback format
|
||||
|
||||
**MANDATORY REQUIREMENT: Every single issue MUST be numbered sequentially starting from Issue 1.**
|
||||
|
||||
This numbered format is NON-NEGOTIABLE. It allows users to efficiently reference specific issues (e.g., "fix issues 1, 3, and 5") and track which feedback has been addressed.
|
||||
|
||||
### Local review mode format
|
||||
|
||||
When outputting issues in the conversation (local mode), use this format:
|
||||
|
||||
```markdown
|
||||
## Issues
|
||||
|
||||
**Issue 1: [Brief title]**
|
||||
Line X: Succinct description of the issue
|
||||
[code or example]
|
||||
Suggested fix or succinct explanation
|
||||
|
||||
**Issue 2: [Brief title]**
|
||||
Line Y: Description of the issue
|
||||
Suggested fix or explanation
|
||||
|
||||
**Issue 3: [Brief title]**
|
||||
...
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
|
||||
> **Issue 1: Formal tone**
|
||||
> Line 15: This could be more conversational. Consider: "You can't..." instead of "You cannot..."
|
||||
|
||||
> **Issue 2: Vague heading**
|
||||
> Line 8: The heading could be more specific. Try stating the point directly: "Run migrations before upgrading" vs "Upgrade process"
|
||||
|
||||
### PR review mode format
|
||||
|
||||
When posting to GitHub (PR mode), use the **pending review workflow**:
|
||||
|
||||
**Workflow steps:**
|
||||
|
||||
1. **Start a review**: Use `mcp__github__create_pending_pull_request_review` to begin a pending review
|
||||
- This creates a draft review that won't be visible until submitted
|
||||
|
||||
2. **Get diff information**: Use `mcp__github__get_pull_request_diff` to understand the code changes and line numbers
|
||||
- This helps you determine the correct file paths and line numbers for comments
|
||||
|
||||
3. **Identify ALL issues**: Read through all changes and identify every issue worth mentioning
|
||||
- Collect all issues before posting any comments
|
||||
- Number them sequentially (Issue 1, Issue 2, Issue 3, etc.)
|
||||
|
||||
4. **Add review comments**: Use `mcp__github__add_pull_request_review_comment_to_pending_review` for each issue
|
||||
- **CRITICAL**: Post ALL comments in a SINGLE response using multiple tool calls in parallel
|
||||
- Each comment should reference a specific file path and line number from the diff
|
||||
- Start each comment body with `**Issue N: [Brief title]**`
|
||||
- Include the description and suggested fix
|
||||
|
||||
5. **Submit the review**: Use `mcp__github__submit_pending_pull_request_review` to publish all comments at once
|
||||
- Use event type `"COMMENT"` (NOT "REQUEST_CHANGES") to make it non-blocking
|
||||
- **Do NOT include a body message** - Leave the body empty or omit it entirely
|
||||
- All comments will appear together as one cohesive review
|
||||
|
||||
**Comment format example:**
|
||||
|
||||
```
|
||||
**Issue 1: Formal tone**
|
||||
|
||||
This could be more conversational. Consider: "You can't..." instead of "You cannot..."
|
||||
```
|
||||
|
||||
**IMPORTANT**:
|
||||
- Each issue gets its own review comment attached to the pending review
|
||||
- Number ALL comments sequentially (Issue 1, Issue 2, Issue 3, etc.)
|
||||
- Always start the comment body with `**Issue N: [Brief title]**`
|
||||
- **MUST add all comments in parallel in a single response** - Do NOT add them one after another in separate responses
|
||||
- Do NOT output a summary message to the conversation - only post GitHub review comments
|
||||
- When submitting the review, do NOT include a body parameter (or leave it empty) to avoid cluttering the PR with summary text
|
||||
- The review will appear as a single review with multiple comments when submitted
|
||||
|
||||
## Final check
|
||||
|
||||
1. Remove any issues from your assessment that won't make a material difference to the reader if addressed. Only flag issues worth the author's time to fix.
|
||||
2. **Verify all issues are numbered sequentially** starting from Issue 1 with no gaps in numbering.
|
||||
3. Confirm the format exactly matches: `**Issue N: [Brief title]**` where N is the issue number.
|
||||
4. **In PR mode**: Verify each issue was posted as a separate GitHub comment (not output to conversation).
|
||||
@@ -1,96 +0,0 @@
|
||||
---
|
||||
name: docs-write
|
||||
description: Write documentation following Metabase's conversational, clear, and user-focused style. Use when creating or editing documentation files (markdown, MDX, etc.).
|
||||
allowed-tools: Read, Write, Grep, Bash, Glob
|
||||
---
|
||||
|
||||
# Documentation Writing Skill
|
||||
|
||||
@./../_shared/metabase-style-guide.md
|
||||
|
||||
## When writing documentation
|
||||
|
||||
### Start here
|
||||
|
||||
1. **Who is this for?** Match complexity to audience. Don't oversimplify hard things or overcomplicate simple ones.
|
||||
2. **What do they need?** Get them to the answer fast. Nobody wants to be in docs longer than necessary.
|
||||
3. **What did you struggle with?** Those common questions you had when learning? Answer them (without literally including the question).
|
||||
|
||||
### Writing process
|
||||
|
||||
**Draft:**
|
||||
|
||||
- Write out the steps/explanation as you'd tell a colleague
|
||||
- Lead with what to do, then explain why
|
||||
- Use headings that state your point: "Set SAML before adding users" not "SAML configuration timing"
|
||||
|
||||
**Edit:**
|
||||
|
||||
- Read aloud. Does it sound like you talking? If it's too formal, simplify.
|
||||
- Cut anything that doesn't directly help the reader
|
||||
- Check each paragraph has one clear purpose
|
||||
- Verify examples actually work (don't give examples that error)
|
||||
|
||||
**Polish:**
|
||||
|
||||
- Make links descriptive (never "here")
|
||||
- Backticks only for code/variables, **bold** for UI elements
|
||||
- American spelling, serial commas
|
||||
- Keep images minimal and scoped tight
|
||||
|
||||
**Format:**
|
||||
|
||||
- Run prettier on the file after making edits: `bun run prettier --write <file-path>`
|
||||
- This ensures consistent formatting across all documentation
|
||||
|
||||
### Common patterns
|
||||
|
||||
**Instructions:**
|
||||
|
||||
```markdown
|
||||
Run:
|
||||
\`\`\`
|
||||
command-to-run
|
||||
\`\`\`
|
||||
|
||||
Then:
|
||||
\`\`\`
|
||||
next-command
|
||||
\`\`\`
|
||||
|
||||
This ensures you're getting the latest changes.
|
||||
```
|
||||
|
||||
Not: "(remember to run X before Y...)" buried in a paragraph.
|
||||
|
||||
**Headings:**
|
||||
|
||||
- "Use environment variables for configuration" ✅
|
||||
- "Environment variables" ❌ (too vague)
|
||||
- "How to use environment variables for configuration" ❌ (too wordy)
|
||||
|
||||
**Links:**
|
||||
|
||||
- "Check out the [SAML documentation](link)" ✅
|
||||
- "Read the docs [here](link)" ❌
|
||||
|
||||
### Watch out for
|
||||
|
||||
- Describing tasks as "easy" (you don't know the reader's context)
|
||||
- Using "we" when talking about Metabase features (use "Metabase" or "it")
|
||||
- Formal language: "utilize", "reference", "offerings"
|
||||
- Too peppy: multiple exclamation points
|
||||
- Burying the action in explanation
|
||||
- Code examples that don't work
|
||||
- Numbers that will become outdated
|
||||
|
||||
### Quick reference
|
||||
|
||||
| Write This | Not This |
|
||||
| -------------------------- | ------------------ |
|
||||
| people, companies | users |
|
||||
| summarize | aggregate |
|
||||
| take a look at | reference |
|
||||
| can't, don't | cannot, do not |
|
||||
| **Filter** button | \`Filter\` button |
|
||||
| Check out [the docs](link) | Click [here](link) |
|
||||
@@ -1,231 +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 on a target namespace, writes tests to kill surviving mutations, and creates draft PRs with Linear issues for each function.
|
||||
|
||||
## 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
|
||||
|
||||
## Reference Files
|
||||
|
||||
- `dev/src/dev/coverage.clj` — mutation testing tool (generates reports, runs mutations)
|
||||
- `dev/src/dev/mutation_testing.clj` — Linear API client and PR template helpers
|
||||
- `mutation-testing-report.lib.card.md` — example report from the pilot
|
||||
|
||||
## Invocation
|
||||
|
||||
The argument is a Clojure namespace, e.g.:
|
||||
|
||||
```
|
||||
/mutation-testing metabase.lib.order-by
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Parse the namespace
|
||||
|
||||
From the argument (e.g., `metabase.lib.order-by`), derive:
|
||||
- **Test namespace**: append `-test` (e.g., `metabase.lib.order-by-test`)
|
||||
- **Short name**: the last segment (e.g., `order-by`)
|
||||
- **Source path**: `src/metabase/lib/order_by.cljc` (replace `-` with `_` in path segments)
|
||||
- **Test path**: `test/metabase/lib/order_by_test.cljc`
|
||||
|
||||
### Step 2: Set up Linear and load tools
|
||||
|
||||
```clojure
|
||||
(require '[dev.coverage :as cov] :reload)
|
||||
(require '[dev.mutation-testing :as mut-test] :reload)
|
||||
```
|
||||
|
||||
If this is the first invocation (or the REPL was restarted), set up the Linear team:
|
||||
|
||||
```clojure
|
||||
;; Find the team
|
||||
(mut-test/list-teams!)
|
||||
;; Set team ID from the output
|
||||
(mut-test/set-config! {:team-id "<team-id>"})
|
||||
```
|
||||
|
||||
If config is already set from a previous invocation, skip this.
|
||||
|
||||
### Step 3: Generate baseline report and create Linear project
|
||||
|
||||
```clojure
|
||||
(cov/generate-report
|
||||
'<target-ns>
|
||||
['<test-ns>]
|
||||
"mutation-testing-report.lib.<short-name>.before.md")
|
||||
```
|
||||
|
||||
Read the generated report file. It has three sections:
|
||||
- **Uncovered Functions** — never called by any test
|
||||
- **Partially Covered Functions** — called but with surviving mutations
|
||||
- **Fully Covered Functions** — all mutations killed
|
||||
|
||||
Then create a Linear project for this namespace (uses report stats in the description):
|
||||
|
||||
```clojure
|
||||
(mut-test/create-project-for-namespace!
|
||||
"<target-ns>"
|
||||
"mutation-testing-report.lib.<short-name>.before.md")
|
||||
;; => {:project-id "...", :name "Mutation Testing: metabase.lib.order-by"}
|
||||
;; Automatically sets :project-id in config for subsequent create-issue! calls
|
||||
```
|
||||
|
||||
### Step 4: Plan the work
|
||||
|
||||
Before creating branches, analyze the report to plan how to group the work:
|
||||
|
||||
1. **Identify killable functions.** For each function with surviving mutations, assess whether any mutations are killable. Run `cov/test-mutations` to see the full mutation list. If ALL mutations for a function are unkillable (schema-only, semantically equivalent, etc.), skip that function — no branch or PR needed.
|
||||
|
||||
2. **Group private functions by public entry point.** Private functions cannot be tested directly. Group them with the public function that exercises them. Create one branch/PR per group, not per private function.
|
||||
|
||||
3. **Track mutations per group.** The `:mutations-before` and `:not-killed` fields in the PR should only reflect mutations for functions covered by that specific PR — never mutations from other functions.
|
||||
|
||||
### Step 5: Process each group
|
||||
|
||||
For each group of functions that has killable mutations:
|
||||
|
||||
#### 5a. Create a branch
|
||||
|
||||
```clojure
|
||||
(mut-test/create-branch! "<target-ns>" "<primary-fn-name>")
|
||||
;; => "mutation-testing-lib-order-by-orderable-columns"
|
||||
```
|
||||
|
||||
Use the primary public function name for the branch. If the group covers private functions too, name the branch after the public entry point.
|
||||
|
||||
#### 5b. Read context
|
||||
|
||||
- Read the source function(s) from the source file
|
||||
- Read the full test file to understand existing test patterns, helpers, and metadata providers used
|
||||
- If the function is private, identify which public function(s) call it
|
||||
|
||||
#### 5c. Write tests
|
||||
|
||||
Generate the **simplest tests** that kill the surviving mutations while still making semantic sense. Guidelines:
|
||||
|
||||
- **Never call private functions directly.** Always test through public API functions. Use the coverage data to identify which public functions exercise the private function.
|
||||
- **Keep tests simple.** Each test should verify one meaningful behavior. Don't over-engineer tests just to chase mutation kills.
|
||||
- **Follow existing patterns.** Match the style of the existing tests: same metadata providers, same helper functions, same assertion patterns.
|
||||
- **Insert tests near related existing tests** for the same function, not at the end of the file. This minimizes merge conflicts between branches. If there are no existing tests for the function, find the most logical location based on the order of functions in the source file.
|
||||
|
||||
**IMPORTANT: Use `file_edit` (not `clojure_edit`) when editing test files.** The `clojure_edit` tool reformats the entire file, introducing unnecessary whitespace changes that pollute the diff. Use `file_edit` with exact string matching to make surgical insertions that only touch the lines you intend to change.
|
||||
|
||||
#### 5d. Verify mutations are killed
|
||||
|
||||
Use the REPL to check that the new tests kill the targeted mutations:
|
||||
|
||||
```clojure
|
||||
;; Reload the test namespace to pick up new tests
|
||||
(require '<test-ns> :reload)
|
||||
|
||||
;; Test mutations for the specific function
|
||||
(cov/test-mutations
|
||||
'<target-ns>/<fn-name>
|
||||
;; Set of test names that cover this function
|
||||
#{'<test-ns>/<test-name-1> '<test-ns>/<test-name-2>})
|
||||
```
|
||||
|
||||
Check the `:survived` key in the result. If mutations still survive:
|
||||
- Try writing additional tests
|
||||
- If a mutation is truly unkillable (semantically equivalent), note it for the PR description
|
||||
- If a mutation reveals dead/unreachable code, consider suggesting removal as a code improvement
|
||||
|
||||
#### 5e. Handle unkillable mutations and improvements
|
||||
|
||||
**Do NOT edit the source namespace just for documentation.** Instead:
|
||||
|
||||
- **Unkillable mutations**: Note them in the PR description with rationale
|
||||
- **Code improvements** (dead code removal, expression simplification) that directly relate to surviving mutants:
|
||||
1. Make the change in the source file
|
||||
2. Verify all tests still pass with the change
|
||||
3. Note the file path, line range, and the improved code
|
||||
4. **Reset the change**: `git checkout -- <source-file>` — do NOT commit it
|
||||
5. After creating the PR, use `add-suggested-change!` to post the improvement as a GitHub suggested change (Step 5h)
|
||||
6. The reviewer decides whether to accept it
|
||||
|
||||
#### 5f. Commit and push
|
||||
|
||||
```clojure
|
||||
;; Only commit the test file — never commit source changes for improvements
|
||||
(mut-test/commit-and-push! "<target-ns>" "<primary-fn-name>"
|
||||
["test/metabase/lib/<short_name>_test.cljc"])
|
||||
```
|
||||
|
||||
#### 5g. Create Linear issue and draft PR
|
||||
|
||||
```clojure
|
||||
;; Create a Linear issue (use the primary function name)
|
||||
(def issue (mut-test/create-issue-for-function! "<target-ns>" "<primary-fn-name>"))
|
||||
;; => {:identifier "QUE-1234", :url "https://linear.app/...", ...}
|
||||
|
||||
;; Create draft PR linked to the Linear issue
|
||||
;; :fn-names lists ALL functions covered by this PR
|
||||
;; :mutations-before counts only mutations for functions in this PR
|
||||
;; :not-killed lists only unkillable mutations for functions in this PR
|
||||
(def pr-url
|
||||
(mut-test/create-draft-pr!
|
||||
{:target-ns "<target-ns>"
|
||||
:fn-name "<primary-fn-name>"
|
||||
:linear-identifier (:identifier issue)
|
||||
:mutations-before 9 ;; surviving mutations for these functions only
|
||||
:tests-added 2 ;; number of new test functions
|
||||
:killed ["Replace :segment-id with :segment-id__"
|
||||
"Replace cycle-path with nil"]
|
||||
:not-killed [{:description "Replace acc with nil"
|
||||
:rationale "reduce always has single iteration"}]
|
||||
:suggested-changes []}))
|
||||
;; => "https://github.com/metabase/metabase/pull/12345"
|
||||
```
|
||||
|
||||
#### 5h. Add code improvement suggestions (if any)
|
||||
|
||||
If you identified code improvements in step 5e (and reset them), post each as a GitHub suggested change:
|
||||
|
||||
```clojure
|
||||
(mut-test/add-suggested-change!
|
||||
{:pr-url pr-url
|
||||
:path "src/metabase/lib/<short_name>.cljc"
|
||||
:start-line 42 ;; first line of the code to replace
|
||||
:end-line 45 ;; last line of the code to replace
|
||||
:suggestion "(improved-code-here)" ;; the replacement code
|
||||
:comment "**Suggested improvement:** <description of the change and why it's related to the surviving mutant>"})
|
||||
```
|
||||
|
||||
### Step 6: Return to master and repeat
|
||||
|
||||
```clojure
|
||||
(mut-test/return-to-master!)
|
||||
```
|
||||
|
||||
Repeat Step 5 for the next group.
|
||||
|
||||
### Step 7: Summary
|
||||
|
||||
Print a summary of what was done:
|
||||
- Link to the Linear project for this namespace
|
||||
- Number of functions processed
|
||||
- Number of functions skipped (all mutations unkillable)
|
||||
- Number of draft PRs created
|
||||
- Number of mutations killed vs. unkillable
|
||||
- List of test PRs with links
|
||||
- List of mutation rule PRs with links (if any were created)
|
||||
|
||||
## Suggesting New Mutation Rules
|
||||
|
||||
If you notice patterns in surviving mutations that suggest the mutation testing tool should have additional rules (e.g., a common Clojure form that isn't being mutated), open a separate PR proposing additions to `dev.coverage/mutation-rules` with an explanation.
|
||||
|
||||
## Configuration
|
||||
|
||||
The `dev.mutation-testing` namespace reads `LINEAR_API_KEY` from the environment and stores team/project IDs in an atom.
|
||||
|
||||
- **Team ID**: Set once per REPL session via `(mut-test/list-teams!)` and `(mut-test/set-config! {:team-id "..."})` (Step 2)
|
||||
- **Project ID**: Set automatically when `create-project-for-namespace!` is called (Step 3) — one project per namespace
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: typescript-review
|
||||
description: Review TypeScript and JavaScript code changes for compliance with Metabase coding standards, style violations, and code quality issues. Use when reviewing pull requests or diffs containing TypeScript/JavaScript code.
|
||||
allowed-tools: Read, Grep, Bash, Glob
|
||||
---
|
||||
|
||||
# TypeScript/JavaScript Code Review Skill
|
||||
|
||||
@./../_shared/typescript-commands.md
|
||||
|
||||
## Code Review Guidelines
|
||||
|
||||
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
|
||||
- Type safety and proper TypeScript usage
|
||||
- React best practices (when applicable)
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
name: typescript-write
|
||||
description: Write TypeScript and JavaScript code following Metabase coding standards and best practices. Use when developing or refactoring TypeScript/JavaScript code.
|
||||
---
|
||||
|
||||
# TypeScript/JavaScript Development Skill
|
||||
|
||||
@./../_shared/development-workflow.md
|
||||
@./../_shared/typescript-commands.md
|
||||
@@ -1,7 +1,7 @@
|
||||
;; -*- comment-column: 100; -*-
|
||||
{:config-paths ["macros" "src" "config"]
|
||||
:linters
|
||||
{;;
|
||||
{ ;;
|
||||
;; enabled optional linters (these are disabled by default)
|
||||
;;
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
:reduce-without-init {:level :warning}
|
||||
:redundant-call {:level :warning}
|
||||
:redundant-fn-wrapper {:level :warning}
|
||||
:redundant-str-call {:level :warning}
|
||||
:self-requiring-namespace {:level :warning}
|
||||
:single-key-in {:level :warning}
|
||||
:unsorted-imports {:level :warning}
|
||||
@@ -50,31 +49,26 @@
|
||||
;; 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}
|
||||
:metabase/discourage-millis-duration {:level :warning}
|
||||
:metabase/mbql-query-first-arg {:level :warning}
|
||||
:metabase/missing-test-expr-requires-in-cljs {:level :warning}
|
||||
:metabase/modules {:level :warning} ; more config in `.clj-kondo/config/modules/config.edn`
|
||||
:metabase/namespace-name {:level :warning}
|
||||
:metabase/no-jsqlparser-imports {:level :warning}
|
||||
:metabase/require-shape-checker {:level :warning}
|
||||
:metabase/tests-must-live-in-known-modules {:level :warning}
|
||||
:metabase/validate-defendpoint-route-uses-kebab-case {:level :warning}
|
||||
:metabase/validate-defendpoint-query-params-use-kebab-case {:level :warning}
|
||||
:metabase/validate-logging {:level :warning}
|
||||
:metabase/validate-mb-driver-tests {:level :warning}
|
||||
:metabase/validate-mb-once {:level :warning}
|
||||
:metabase/check-defmulti-arglists {:level :warning}
|
||||
:metabase/check-me-humanize {:level :warning}
|
||||
:metabase/defsetting-must-specify-export {:level :warning}
|
||||
:metabase/defsetting-namespace {:level :warning}
|
||||
:metabase/mbql-query-first-arg {:level :warning}
|
||||
:metabase/missing-test-expr-requires-in-cljs {:level :warning}
|
||||
:metabase/modules {:level :warning} ; more config in `.clj-kondo/config/modules/config.edn`
|
||||
:metabase/namespace-name {:level :warning}
|
||||
:metabase/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/validate-logging {:level :warning}
|
||||
:metabase/validate-mb-driver-tests {:level :warning}
|
||||
:metabase/validate-mb-once {:level :warning}
|
||||
:metabase/discourage-millis-duration {:level :warning}
|
||||
|
||||
:metabase/validate-deftest
|
||||
{:level :warning
|
||||
@@ -180,11 +174,6 @@
|
||||
metabase.notification.models/create-notification!
|
||||
metabase.pulse.send/send-pulse!
|
||||
metabase-enterprise.transforms-python.python-runner-test/execute!
|
||||
;; Workspace isolation functions are safe in parallel tests because they operate on
|
||||
;; isolated, test-specific workspaces and databases that don't affect other tests
|
||||
metabase-enterprise.workspaces.isolation/destroy-workspace-isolation!
|
||||
metabase-enterprise.workspaces.isolation/ensure-database-isolation!
|
||||
metabase-enterprise.workspaces.test-util/create-ready-ws!
|
||||
;; Safe in tests, where we typically use a private, temporary index per thread.
|
||||
metabase.search.core/reindex!
|
||||
metabase.util/run-count!
|
||||
@@ -198,7 +187,7 @@
|
||||
:metabase/disallow-hardcoded-driver-names-in-tests
|
||||
;; don't include `:h2` since that will probably lead to a mountain of tests where we are specifically testing
|
||||
;; middleware or compilation behavior with just H2 since it's the default.
|
||||
{:drivers #{;; core drivers
|
||||
{:drivers #{ ;; core drivers
|
||||
#_:h2
|
||||
:postgres
|
||||
:mysql
|
||||
@@ -226,9 +215,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]} ; 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]}}
|
||||
|
||||
@@ -239,6 +226,9 @@
|
||||
{:exclude
|
||||
[(build.uberjar/with-duration-ms)
|
||||
(cljs.test/is [=? malli=])
|
||||
(clojure.core.logic/fresh)
|
||||
(clojure.core.logic/matcha)
|
||||
(clojure.core.logic/run)
|
||||
(clojure.test/is [=? malli= qv=])
|
||||
(clojure.tools.macro/macrolet)
|
||||
instaparse.core/transform
|
||||
@@ -247,13 +237,11 @@
|
||||
(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.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.query-processor.middleware.cache-backend.interface/with-cached-results)
|
||||
@@ -285,7 +273,6 @@
|
||||
clojure.java.jdbc/with-db-connection {:message "Use sql-jdbc.execute/do-with-connection-with-options (for drivers) or t2/with-connection (for the app DB) instead of jdbc/with-db-connection"}
|
||||
clojure.java.jdbc/with-db-metadata {:message "Use sql-jdbc.execute/do-with-connection-with-options + .getMetaData instead of jdbc/with-db-metadata"}
|
||||
clojure.pprint/pprint {:message "Use metabase.util.log instead of clojure.pprint/pprint"}
|
||||
clojure.pprint/print-table {:message "Use metabase.util.log instead of printing directly to the console"}
|
||||
clojure.string/lower-case {:message "Use metabase.util/lower-case-en instead of clojure.string/lower-case"}
|
||||
clojure.string/upper-case {:message "Use metabase.util/upper-case-en instead of clojure.string/upper-case"}
|
||||
compojure.core/context {:message "Use metabase.api.util.handlers/route-map-handler instead of compojure.core/context, it is much faster and supports OpenAPI spec generation"}
|
||||
@@ -307,13 +294,9 @@
|
||||
metabase.lib.convert/->legacy-MBQL {:message "Conversion to legacy MBQL is discouraged, please use Lib to manipulate MBQL queries"}
|
||||
metabase.lib.core/->legacy-MBQL {:message "Conversion to legacy MBQL is discouraged, please use Lib to manipulate MBQL queries"}
|
||||
metabase.lib.equality/find-column-indexes-for-refs {:message "Use lib.equality/closest-matches-in-metadata or lib.equality/find-closest-matches-for-refs instead of lib.equality/find-column-indexes-for-refs"}
|
||||
metabase.lib.util.match/match {:message "Use lib.util.match/match-many instead of lib.util.match/match."}
|
||||
metabase.lib.util.match/match-one {:message "Use lib.util.match/match-lite instead of lib.util.match/match-one."}
|
||||
metabase-enterprise.transforms.interface/execute! {:message "Use metabase-enterprise.transforms.execute/execute! instead of metabase-enterprise.transforms.interface/execute!"}
|
||||
next.jdbc/active-tx? {:message "Use metabase.app-db.core/in-transaction? to see if we are in a transaction"}
|
||||
toucan2.core/debug {:message "Don't commit code using t2/debug"}
|
||||
toucan2.tools.debug/debug {:message "Don't commit code using t2/debug"}
|
||||
}
|
||||
toucan2.tools.debug/debug {:message "Don't commit code using t2/debug"}}
|
||||
|
||||
:discouraged-namespace
|
||||
{camel-snake-kebab.core {:message "CSK is not Turkish-safe, use the versions in metabase.util instead."}
|
||||
@@ -423,6 +406,7 @@
|
||||
:lint-as
|
||||
{cljs.cache/defcache clojure.core/deftype
|
||||
clojure.core.cache/defcache clojure.core/deftype
|
||||
clojure.core.logic/defne clj-kondo.lint-as/def-catch-all
|
||||
clojure.test.check.clojure-test/defspec clojure.test/deftest
|
||||
clojure.test.check.properties/for-all clojure.core/let
|
||||
clojurewerkz.quartzite.jobs/defjob clojure.core/defn
|
||||
@@ -432,10 +416,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
|
||||
@@ -463,8 +448,8 @@
|
||||
metabase.premium-features.settings/define-premium-feature clojure.core/def
|
||||
metabase.pulse.dashboard-subscription-test/with-dashboard-sub-for-card clojure.core/let
|
||||
metabase.pulse.send-test/with-pulse-for-card clojure.core/let
|
||||
metabase.query-processor.pipeline-queries-test/pmbql-query clojure.core/->
|
||||
metabase.query-processor.pipeline-queries-test/run-pmbql-query clojure.core/->
|
||||
metabase.query-processor-test.pipeline-queries-test/pmbql-query clojure.core/->
|
||||
metabase.query-processor-test.pipeline-queries-test/run-pmbql-query clojure.core/->
|
||||
metabase.query-processor.error-type/deferror clojure.core/def
|
||||
metabase.query-processor.middleware.cache.impl/with-reducible-deserialized-results clojure.core/let
|
||||
metabase.query-processor.middleware.process-userland-query-test/with-query-execution! clojure.core/let
|
||||
@@ -503,9 +488,8 @@
|
||||
metabase.util.malli/defn- schema.core/defn
|
||||
metabase.util.malli/fn schema.core/fn
|
||||
metabase.util.namespaces/import-fns potemkin/import-vars
|
||||
metabase.util.performance/doseq clojure.core/doseq
|
||||
metabase.util.performance/for clojure.core/for
|
||||
metabase.util/with-timer-ms clojure.core/fn
|
||||
metabase.xrays.domain-entities.malli/defn schema.core/defn
|
||||
metabuild-common.core/with-open-jar-file-system clojure.core/let
|
||||
metabuild-common.files/with-open-jar-file-system clojure.core/let
|
||||
monger.operators/defoperator clojure.core/def
|
||||
@@ -523,16 +507,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
|
||||
@@ -544,10 +519,7 @@
|
||||
metabase-enterprise.advanced-permissions.models.permissions.application-permissions-test/with-new-group-and-current-graph hooks.common/with-two-top-level-bindings
|
||||
metabase-enterprise.audit-app.pages-test/with-temp-objects hooks.common/with-one-binding
|
||||
metabase-enterprise.cache.config-test/with-temp-persist-models hooks.common/with-seven-bindings
|
||||
metabase-enterprise.dependencies.api-test/with-dependents-test! hooks.common/with-two-bindings
|
||||
metabase-enterprise.metabot-v3.tools.dependencies-test/with-dependent-transforms! hooks.common/with-two-bindings
|
||||
metabase-enterprise.semantic-search.db.connection/with-migrate-tx hooks.common/let-one-with-optional-value
|
||||
metabase-enterprise.serialization.api-test/with-serialization-test-data! hooks.common/with-three-bindings
|
||||
metabase-enterprise.serialization.test-util/with-temp-dpc hooks.toucan2.tools.with-temp/with-temp
|
||||
metabase-enterprise.transforms-python.drivers-test/with-test-table hooks.common/with-two-bindings
|
||||
metabase.api.common.internal-test/with-jetty-server hooks.common/with-one-binding
|
||||
@@ -558,25 +530,23 @@
|
||||
metabase.channel.template.handlebars-test/with-temp-template! hooks.common/with-vec-first-binding
|
||||
metabase.classloader.core/require hooks.clojure.core.require/lint-require
|
||||
metabase.classloader.impl/require hooks.clojure.core.require/lint-require
|
||||
metabase.collections-rest.api-test/with-french-user-and-personal-collection! hooks.common/with-two-top-level-bindings
|
||||
metabase.collections.api-test/with-french-user-and-personal-collection! hooks.common/with-two-top-level-bindings
|
||||
metabase.collections.models.collection-test/with-collection-hierarchy! hooks.common/let-one-with-optional-value
|
||||
metabase.collections.models.collection-test/with-personal-and-impersonal-collections hooks.common/with-two-bindings
|
||||
metabase.dashboards-rest.api-test/with-chain-filter-fixtures hooks.common/let-one-with-optional-value
|
||||
metabase.dashboards-rest.api-test/with-simple-dashboard-with-tabs hooks.common/with-one-binding
|
||||
metabase.dashboards.api-test/with-chain-filter-fixtures hooks.common/let-one-with-optional-value
|
||||
metabase.dashboards.api-test/with-simple-dashboard-with-tabs hooks.common/with-one-binding
|
||||
metabase.dashboards.models.dashboard-tab-test/with-dashtab-in-personal-collection hooks.common/with-one-top-level-binding
|
||||
metabase.dashboards.models.dashboard-test/with-dash-in-collection! hooks.common/with-three-bindings
|
||||
metabase.driver.bigquery-cloud-sdk-test/calculate-bird-scarcity hooks.metabase.query-processor.expressions-test/calculate-bird-scarcity
|
||||
metabase.driver.bigquery-cloud-sdk-test/calculate-bird-scarcity hooks.metabase.query-processor-test.expressions-test/calculate-bird-scarcity
|
||||
metabase.driver.mongo-test/with-describe-table-for-sample hooks.metabase.driver.mongo-test/with-describe-table-for-sample
|
||||
metabase.driver.oracle-test/with-temp-user hooks.common/let-one-with-optional-value
|
||||
metabase.driver/register! hooks.metabase.driver/register!
|
||||
metabase.driver-api/defenterprise hooks.metabase.premium-features.defenterprise/defenterprise
|
||||
metabase.embedding-rest.api.embed-test/do-response-formats hooks.common/with-two-bindings
|
||||
metabase.embedding-rest.api.embed-test/with-chain-filter-fixtures! hooks.common/let-one-with-optional-value
|
||||
metabase.embedding-rest.api.embed-test/with-temp-card hooks.common/let-one-with-optional-value
|
||||
metabase.embedding-rest.api.embed-test/with-temp-dashcard hooks.common/let-one-with-optional-value
|
||||
metabase.embedding.api.embed-test/do-response-formats hooks.common/with-two-bindings
|
||||
metabase.embedding.api.embed-test/with-chain-filter-fixtures! hooks.common/let-one-with-optional-value
|
||||
metabase.embedding.api.embed-test/with-temp-card hooks.common/let-one-with-optional-value
|
||||
metabase.embedding.api.embed-test/with-temp-dashcard hooks.common/let-one-with-optional-value
|
||||
metabase.legacy-mbql.schema.macros/defclause hooks.metabase.legacy-mbql.schemas.macros/defclause
|
||||
metabase.legacy-mbql.schema.macros/defclause* hooks.metabase.legacy-mbql.schemas.macros/defclause*
|
||||
metabase.legacy-mbql.schema.macros/one-of hooks.metabase.legacy-mbql.schemas.macros/one-of
|
||||
metabase.lib.schema.mbql-clause/define-catn-mbql-clause hooks.metabase.lib.schema.mbql-clause/define-mbql-clause
|
||||
metabase.lib.schema.mbql-clause/define-mbql-clause hooks.metabase.lib.schema.mbql-clause/define-mbql-clause
|
||||
metabase.lib.schema.mbql-clause/define-tuple-mbql-clause hooks.metabase.lib.schema.mbql-clause/define-mbql-clause
|
||||
@@ -593,20 +563,20 @@
|
||||
metabase.permissions.core/disable-perms-cache hooks.common/do*
|
||||
metabase.premium-features.core/defenterprise hooks.metabase.premium-features.defenterprise/defenterprise
|
||||
metabase.premium-features.defenterprise/defenterprise hooks.metabase.premium-features.defenterprise/defenterprise
|
||||
metabase.public-sharing-rest.api-test/with-required-param-card! hooks.common/with-one-binding
|
||||
metabase.public-sharing-rest.api-test/with-temp-public-card hooks.common/let-one-with-optional-value
|
||||
metabase.public-sharing-rest.api-test/with-temp-public-dashboard hooks.common/let-one-with-optional-value
|
||||
metabase.public-sharing-rest.api-test/with-temp-public-dashboard-and-card hooks.common/with-three-bindings
|
||||
metabase.public-sharing.api-test/with-required-param-card! hooks.common/with-one-binding
|
||||
metabase.public-sharing.api-test/with-temp-public-card hooks.common/let-one-with-optional-value
|
||||
metabase.public-sharing.api-test/with-temp-public-dashboard hooks.common/let-one-with-optional-value
|
||||
metabase.public-sharing.api-test/with-temp-public-dashboard-and-card hooks.common/with-three-bindings
|
||||
metabase.pulse.api.alert-test/with-alert-in-collection! hooks.common/with-four-bindings
|
||||
metabase.pulse.dashboard-subscription-test/with-link-card-fixture-for-dashboard hooks.common/let-second
|
||||
metabase.pulse.models.pulse-test/with-dashboard-subscription-in-collection! hooks.common/with-four-bindings
|
||||
metabase.pulse.models.pulse-test/with-pulse-in-collection! hooks.common/with-four-bindings
|
||||
metabase.pulse.test-util/checkins-query-card hooks.metabase.test.data/$ids
|
||||
metabase.queries-rest.api.card-test/with-card-param-values-fixtures hooks.common/let-one-with-optional-value
|
||||
metabase.queries-rest.api.card-test/with-temp-native-card hooks.common/with-two-bindings
|
||||
metabase.queries-rest.api.card-test/with-temp-native-card-with-params hooks.common/with-two-bindings
|
||||
metabase.query-processor.expressions-test/calculate-bird-scarcity hooks.metabase.query-processor.expressions-test/calculate-bird-scarcity
|
||||
metabase.query-processor.filter-test/count-with-filter-clause hooks.metabase.test.data/$ids
|
||||
metabase.queries.api.card-test/with-card-param-values-fixtures hooks.common/let-one-with-optional-value
|
||||
metabase.queries.api.card-test/with-temp-native-card! hooks.common/with-two-bindings
|
||||
metabase.queries.api.card-test/with-temp-native-card-with-params! hooks.common/with-two-bindings
|
||||
metabase.query-processor-test.expressions-test/calculate-bird-scarcity hooks.metabase.query-processor-test.expressions-test/calculate-bird-scarcity
|
||||
metabase.query-processor-test.filter-test/count-with-filter-clause hooks.metabase.test.data/$ids
|
||||
metabase.query-processor.middleware.cache-test/with-mock-cache! hooks.common/with-two-bindings
|
||||
metabase.request.core/with-current-request hooks.common/with-used-first-arg
|
||||
metabase.request.core/with-current-user hooks.common/with-used-first-arg
|
||||
@@ -658,7 +628,7 @@
|
||||
metabase.test/with-temp hooks.toucan2.tools.with-temp/with-temp
|
||||
metabase.test/with-temp-file hooks.metabase.test.util/with-temp-file
|
||||
metabase.test/with-temporary-setting-values hooks.metabase.test.util/with-temporary-setting-values
|
||||
metabase.users-rest.api-test/with-temp-user-email! hooks.common/with-one-binding
|
||||
metabase.users.api-test/with-temp-user-email! hooks.common/with-one-binding
|
||||
metabase.util.i18n/deferred-trs hooks.metabase.util.i18n/deferred-trs
|
||||
metabase.util.i18n/deferred-tru hooks.metabase.util.i18n/deferred-tru
|
||||
metabase.util.i18n/trs hooks.metabase.util.i18n/trs
|
||||
@@ -682,43 +652,44 @@
|
||||
metabase.xrays.api.automagic-dashboards-test/with-indexed-model! hooks.metabase.xrays.api.automagic-dashboards-test/with-indexed-model!}
|
||||
|
||||
:macroexpand
|
||||
{clojurewerkz.quartzite.jobs/build macros.quartz/build-job
|
||||
clojurewerkz.quartzite.schedule.calendar-interval/build macros.quartz/build-job
|
||||
clojurewerkz.quartzite.schedule.cron/schedule macros.quartz/schedule
|
||||
clojurewerkz.quartzite.schedule.simple/schedule macros.quartz/simple-schedule
|
||||
clojurewerkz.quartzite.triggers/build macros.quartz/build-trigger
|
||||
metabase-enterprise.database-routing.e2e-test/with-temp-dbs! macros.metabase-enterprise.database-routing.e2e-test/with-temp-dbs!
|
||||
metabase-enterprise.impersonation.util-test/with-impersonations! macros.metabase-enterprise.impersonation.util-test/with-impersonations!
|
||||
metabase-enterprise.query-reference-validation.api-test/with-test-setup! macros.metabase-enterprise.query-reference-validation.api-test/with-test-setup!
|
||||
metabase-enterprise.sandbox.test-util/with-gtaps! macros.metabase-enterprise.sandbox.test-util/with-gtaps!
|
||||
metabase-enterprise.sandbox.test-util/with-gtaps-for-user! macros.metabase-enterprise.sandbox.test-util/with-gtaps!
|
||||
metabase-enterprise.serialization.test-util/with-world macros.metabase-enterprise.serialization.test-util/with-world
|
||||
metabase-enterprise.test/with-gtaps! macros.metabase-enterprise.sandbox.test-util/with-gtaps!
|
||||
metabase-enterprise.test/with-gtaps-for-user! macros.metabase-enterprise.sandbox.test-util/with-gtaps!
|
||||
metabase.collections-rest.api-test/with-collection-hierarchy! macros.metabase.collections-rest.api-test/with-collection-hierarchy!
|
||||
metabase.collections-rest.api-test/with-some-children-of-collection! macros.metabase.collections-rest.api-test/with-some-children-of-collection!
|
||||
metabase.embedding-rest.api.embed-test/with-embedding-enabled-and-temp-card-referencing! macros.metabase.embedding-rest.api.embed-test/with-embedding-enabled-and-temp-card-referencing!
|
||||
metabase.embedding-rest.api.embed-test/with-embedding-enabled-and-temp-dashcard-referencing! macros.metabase.embedding-rest.api.embed-test/with-embedding-enabled-and-temp-dashcard-referencing!
|
||||
metabase.lib.common/defop macros.metabase.lib.common/defop
|
||||
metabase.lib.filter/deffilter macros.metabase.lib.filter/deffilter
|
||||
metabase.parameters.chain-filter-test/chain-filter macros.metabase.parameters.chain-filter-test/chain-filter
|
||||
metabase.parameters.chain-filter-test/chain-filter-search macros.metabase.parameters.chain-filter-test/chain-filter
|
||||
metabase.public-sharing-rest.api-test/with-sharing-enabled-and-temp-card-referencing! macros.metabase.public-sharing-rest.api-test/with-sharing-enabled-and-temp-card-referencing!
|
||||
metabase.public-sharing-rest.api-test/with-sharing-enabled-and-temp-dashcard-referencing! macros.metabase.public-sharing-rest.api-test/with-sharing-enabled-and-temp-dashcard-referencing!
|
||||
metabase.queries-rest.api.card-test/with-ordered-items macros.metabase.queries-rest.api.card-test/with-ordered-items
|
||||
metabase.query-analysis.models.query-analysis-test/with-test-setup macros.metabase.query-analysis.models.query-analysis-test/with-test-setup
|
||||
metabase.query-analysis.task.test-setup/with-test-setup! macros.metabase.query-analysis.task.test-setup/with-test-setup!
|
||||
metabase.query-processor.streaming/streaming-response macros.metabase.query-processor.streaming/streaming-response
|
||||
metabase.server.statistics-handler-test/with-server macros.metabase.server.statistics-handler-test/with-server
|
||||
metabase.test.data.users/with-group-for-user macros.metabase.test.data.users/with-group-for-user
|
||||
metabase.test.util/with-temp-env-var-value! macros.metabase.test.util/with-temp-env-var-value!
|
||||
metabase.test.util/with-temporary-raw-setting-values macros.metabase.test.util/with-temporary-raw-setting-values
|
||||
metabase.test/with-group-for-user macros.metabase.test.data.users/with-group-for-user
|
||||
metabase.test/with-temp-env-var-value! macros.metabase.test.util/with-temp-env-var-value!
|
||||
metabase.test/with-temporary-raw-setting-values macros.metabase.test.util/with-temporary-raw-setting-values
|
||||
metabase.users.models.user-test/with-groups! macros.metabase.users.models.user-test/with-groups!
|
||||
metabase.util.namespaces/import-fn macros.metabase.util.namespaces/import-fn
|
||||
metabase.xrays.related-test/with-world macros.metabase.xrays.related-test/with-world}}
|
||||
{clojurewerkz.quartzite.jobs/build macros.quartz/build-job
|
||||
clojurewerkz.quartzite.schedule.calendar-interval/build macros.quartz/build-job
|
||||
clojurewerkz.quartzite.schedule.cron/schedule macros.quartz/schedule
|
||||
clojurewerkz.quartzite.schedule.simple/schedule macros.quartz/simple-schedule
|
||||
clojurewerkz.quartzite.triggers/build macros.quartz/build-trigger
|
||||
metabase-enterprise.database-routing.e2e-test/with-temp-dbs! macros.metabase-enterprise.database-routing.e2e-test/with-temp-dbs!
|
||||
metabase-enterprise.impersonation.util-test/with-impersonations! macros.metabase-enterprise.impersonation.util-test/with-impersonations!
|
||||
metabase-enterprise.query-reference-validation.api-test/with-test-setup! macros.metabase-enterprise.query-reference-validation.api-test/with-test-setup!
|
||||
metabase-enterprise.sandbox.test-util/with-gtaps! macros.metabase-enterprise.sandbox.test-util/with-gtaps!
|
||||
metabase-enterprise.sandbox.test-util/with-gtaps-for-user! macros.metabase-enterprise.sandbox.test-util/with-gtaps!
|
||||
metabase-enterprise.serialization.test-util/with-world macros.metabase-enterprise.serialization.test-util/with-world
|
||||
metabase-enterprise.test/with-gtaps! macros.metabase-enterprise.sandbox.test-util/with-gtaps!
|
||||
metabase-enterprise.test/with-gtaps-for-user! macros.metabase-enterprise.sandbox.test-util/with-gtaps!
|
||||
metabase.collections.api-test/with-collection-hierarchy! macros.metabase.collections.api-test/with-collection-hierarchy!
|
||||
metabase.collections.api-test/with-some-children-of-collection! macros.metabase.collections.api-test/with-some-children-of-collection!
|
||||
metabase.embedding.api.embed-test/with-embedding-enabled-and-temp-card-referencing! macros.metabase.embedding.api.embed-test/with-embedding-enabled-and-temp-card-referencing!
|
||||
metabase.embedding.api.embed-test/with-embedding-enabled-and-temp-dashcard-referencing! macros.metabase.embedding.api.embed-test/with-embedding-enabled-and-temp-dashcard-referencing!
|
||||
metabase.lib.common/defop macros.metabase.lib.common/defop
|
||||
metabase.lib.filter/deffilter macros.metabase.lib.filter/deffilter
|
||||
metabase.parameters.chain-filter-test/chain-filter macros.metabase.parameters.chain-filter-test/chain-filter
|
||||
metabase.parameters.chain-filter-test/chain-filter-search macros.metabase.parameters.chain-filter-test/chain-filter
|
||||
metabase.public-sharing.api-test/with-sharing-enabled-and-temp-card-referencing! macros.metabase.public-sharing.api-test/with-sharing-enabled-and-temp-card-referencing!
|
||||
metabase.public-sharing.api-test/with-sharing-enabled-and-temp-dashcard-referencing! macros.metabase.public-sharing.api-test/with-sharing-enabled-and-temp-dashcard-referencing!
|
||||
metabase.queries.api.card-test/with-ordered-items macros.metabase.queries.api.card-test/with-ordered-items
|
||||
metabase.query-analysis.models.query-analysis-test/with-test-setup macros.metabase.query-analysis.models.query-analysis-test/with-test-setup
|
||||
metabase.query-analysis.task.test-setup/with-test-setup! macros.metabase.query-analysis.task.test-setup/with-test-setup!
|
||||
metabase.query-processor.streaming/streaming-response macros.metabase.query-processor.streaming/streaming-response
|
||||
metabase.server.statistics-handler-test/with-server macros.metabase.server.statistics-handler-test/with-server
|
||||
metabase.test.data.users/with-group-for-user macros.metabase.test.data.users/with-group-for-user
|
||||
metabase.test.util/with-temp-env-var-value! macros.metabase.test.util/with-temp-env-var-value!
|
||||
metabase.test.util/with-temporary-raw-setting-values macros.metabase.test.util/with-temporary-raw-setting-values
|
||||
metabase.test/with-group-for-user macros.metabase.test.data.users/with-group-for-user
|
||||
metabase.test/with-temp-env-var-value! macros.metabase.test.util/with-temp-env-var-value!
|
||||
metabase.test/with-temporary-raw-setting-values macros.metabase.test.util/with-temporary-raw-setting-values
|
||||
metabase.users.models.user-test/with-groups! macros.metabase.users.models.user-test/with-groups!
|
||||
metabase.util.namespaces/import-fn macros.metabase.util.namespaces/import-fn
|
||||
metabase.xrays.domain-entities.malli/define-getters-and-setters macros.metabase.xrays.domain-entities.malli/define-getters-and-setters
|
||||
metabase.xrays.related-test/with-world macros.metabase.xrays.related-test/with-world}}
|
||||
|
||||
:config-in-comment
|
||||
{:linters {:unresolved-symbol {:level :off}}}
|
||||
@@ -745,8 +716,6 @@
|
||||
:name metabase-lib}
|
||||
{:pattern "^metabase\\.lib\\..*"
|
||||
:name metabase-lib-excluding-legacy-mbql}
|
||||
{:pattern "^metabase\\.legacy-mbql\\..*"
|
||||
:name legacy-mbql-namespaces}
|
||||
;;
|
||||
;; QP and driver source namespaces: `metabase.query-processor.*`, `metabase-enterprise.sandbox.query-processor.*`,
|
||||
;; and `metabase.driver.*`, excluding `-test` namespaces.
|
||||
@@ -761,7 +730,7 @@
|
||||
;;
|
||||
;; driver namespaces, including tests
|
||||
;;
|
||||
{:pattern "metabase\\.driver(?:-api)?.*$"
|
||||
{:pattern "metabase\\.driver.*$"
|
||||
:name driver-namespaces}
|
||||
;;
|
||||
;; Perf-sensitive namespaces are currently the union of metabase.lib.*, driver namespaces, and QP (without tests).
|
||||
@@ -774,46 +743,40 @@
|
||||
;; `metabase-enterprise.sandbox.query-processor*-test`
|
||||
;;
|
||||
{:pattern "^metabase(?:((?:-enterprise\\.sandbox)?\\.query-processor)|(?:\\.driver\\.sql(?!server)(?!ite))).*-test$"
|
||||
:name general-qp-and-driver-test-namespaces}
|
||||
|
||||
{:pattern "(?:^mage.*)"
|
||||
:name mage-namespaces}]
|
||||
:name general-qp-and-driver-test-namespaces}]
|
||||
|
||||
:config-in-ns
|
||||
{test-namespaces
|
||||
{:linters
|
||||
{:inline-def {:level :off}
|
||||
:missing-docstring {:level :off}
|
||||
:private-call {:level :off}
|
||||
:metabase/defsetting-must-specify-export {:level :off}
|
||||
:metabase/defsetting-namespace {:level :off}
|
||||
:metabase/test-helpers-use-non-thread-safe-functions {:level :warning} ; more config in `.clj-kondo/config/test-helpers/config.edn
|
||||
{:inline-def {:level :off}
|
||||
:missing-docstring {:level :off}
|
||||
:private-call {:level :off}
|
||||
:metabase/defsetting-must-specify-export {:level :off}
|
||||
:metabase/defsetting-namespace {:level :off}
|
||||
|
||||
:discouraged-var
|
||||
{metabase.driver/database-supports? {:level :off}
|
||||
metabase.lib.convert/->legacy-MBQL {:level :off}
|
||||
metabase.lib.core/->legacy-MBQL {:level :off}}}}
|
||||
metabase.lib.core/->legacy-MBQL {:level :off}}}
|
||||
|
||||
:hooks
|
||||
{:analyze-call
|
||||
{clojure.core/defmacro hooks.clojure.core.defn/non-thread-safe-form-should-end-with-exclamation
|
||||
clojure.core/defn hooks.clojure.core.defn/non-thread-safe-form-should-end-with-exclamation
|
||||
clojure.core/defn- hooks.clojure.core.defn/non-thread-safe-form-should-end-with-exclamation}}}
|
||||
|
||||
source-namespaces
|
||||
{:linters
|
||||
{:metabase/discourage-millis-duration {:level :warning}
|
||||
:metabase/validate-defendpoint-has-response-schema {:level :warning}
|
||||
:metabase/validate-escaped-single-quotes-in-i18n {:level :warning}
|
||||
:metabase/validate-string-or-str-args-to-i18n {:level :warning}
|
||||
{:metabase/validate-escaped-single-quotes-in-i18n {:level :warning}
|
||||
:metabase/validate-string-or-str-args-to-i18n {:level :warning}
|
||||
:metabase/discourage-millis-duration {:level :warning}
|
||||
|
||||
:discouraged-var
|
||||
{clojure.core/with-redefs {:message "Don't use with-redefs outside of tests"}
|
||||
clojure.core/eval {:message "Don't use eval outside of tests"}}
|
||||
|
||||
:discouraged-namespace
|
||||
{metabase.legacy-mbql.jvm-util {:message "Avoid legacy MBQL utils, use Lib instead"}
|
||||
metabase.legacy-mbql.normalize {:message "Avoid legacy MBQL normalization, use MBQL 5 + lib/normalize instead"}
|
||||
metabase.legacy-mbql.predicates {:message "Avoid legacy MBQL utils, use Lib instead"}
|
||||
metabase.legacy-mbql.schema {:message "Avoid the legacy MBQL schema, use MBQL 5 + lib.schema instead"}
|
||||
metabase.legacy-mbql.schema.helpers {:message "Avoid the legacy MBQL schema, use MBQL 5 + lib.schema instead"}
|
||||
metabase.legacy-mbql.schema.macros {:message "Avoid the legacy MBQL schema, use MBQL 5 + lib.schema instead"}
|
||||
metabase.legacy-mbql.util {:message "Avoid legacy MBQL utils, use Lib instead"}
|
||||
metabase.query-processor.store {:message "Please do not use the QP Store outside of legacy QP or driver code. Use MBQL 5/Lib or lib.metadata.jvm/applicaton-database-metadata-provider instead."}}}}
|
||||
{metabase.query-processor.store {:message "Please do not use the QP Store outside of legacy QP or driver code. Use MBQL 5/Lib or lib.metadata.jvm/applicaton-database-metadata-provider instead."}}}}
|
||||
|
||||
printable-namespaces
|
||||
{:linters
|
||||
@@ -855,89 +818,34 @@
|
||||
toucan2.core {:message "Don't use application database inside Lib code"}}}}
|
||||
|
||||
metabase-lib-excluding-legacy-mbql
|
||||
{}
|
||||
|
||||
legacy-mbql-namespaces
|
||||
{:linters
|
||||
{:discouraged-namespace
|
||||
{metabase.legacy-mbql.jvm-util {:level :off}
|
||||
metabase.legacy-mbql.normalize {:level :off}
|
||||
metabase.legacy-mbql.predicates {:level :off}
|
||||
metabase.legacy-mbql.schema {:level :off}
|
||||
metabase.legacy-mbql.schema.helpers {:level :off}
|
||||
metabase.legacy-mbql.schema.macros {:level :off}
|
||||
metabase.legacy-mbql.util {:level :off}
|
||||
metabase.query-processor.store {:level :off}}
|
||||
{:discouraged-var
|
||||
{metabase.legacy-mbql.util/unique-name-generator {:message "Use metabase.lib.util/unique-name-generator or non-truncating-unique-name-generator"}}}
|
||||
|
||||
;; the `metabase.legacy-mbql` stuff is all deprecated, so we don't need to warn inside the module about other stuff
|
||||
;; in the module being marked deprecated or discouraged.
|
||||
:deprecated-namespace {:level :off}
|
||||
:deprecated-var {:level :off}}}
|
||||
:discouraged-namespace
|
||||
{metabase.legacy-mbql.js {:message "Avoid legacy MBQL utils in Lib; move things into Lib if needded."}
|
||||
metabase.legacy-mbql.jvm-util {:message "Avoid legacy MBQL utils in Lib; move things into Lib if needded."}
|
||||
metabase.legacy-mbql.predicates {:message "Avoid legacy MBQL utils in Lib; move things into Lib if needded."}
|
||||
metabase.legacy-mbql.util {:message "Avoid legacy MBQL utils in Lib; move things into Lib if needded."}}}
|
||||
|
||||
driver-namespaces
|
||||
{:linters
|
||||
{:discouraged-var
|
||||
{metabase.driver/database-supports? {:level :off}}
|
||||
|
||||
;; don't warn about the legacy MBQL namespaces being deprecated or discouraged in driver code now, we don't have a
|
||||
;; real alternative YET so it's not a helpful warning.
|
||||
:discouraged-namespace
|
||||
{metabase.legacy-mbql.jvm-util {:level :off}
|
||||
metabase.legacy-mbql.normalize {:level :off}
|
||||
metabase.legacy-mbql.predicates {:level :off}
|
||||
metabase.legacy-mbql.schema {:level :off}
|
||||
metabase.legacy-mbql.schema.helpers {:level :off}
|
||||
metabase.legacy-mbql.schema.macros {:level :off}
|
||||
metabase.legacy-mbql.util {:level :off}
|
||||
metabase.query-processor.store {:level :off}}
|
||||
|
||||
:deprecated-namespace
|
||||
{:exclude
|
||||
[metabase.driver.common.parameters
|
||||
metabase.driver.common.parameters.dates
|
||||
metabase.driver.common.parameters.operators
|
||||
metabase.driver.common.parameters.parse
|
||||
metabase.driver.common.parameters.values
|
||||
metabase.legacy-mbql.jvm-util
|
||||
metabase.legacy-mbql.normalize
|
||||
metabase.legacy-mbql.predicates
|
||||
metabase.legacy-mbql.schema
|
||||
metabase.legacy-mbql.schema.helpers
|
||||
metabase.legacy-mbql.schema.macros
|
||||
metabase.legacy-mbql.util
|
||||
metabase.query-processor.store]}
|
||||
|
||||
:deprecated-var
|
||||
{:exclude
|
||||
{metabase.legacy-mbql.schema.helpers/is-clause? {:namespaces ["metabase\\.driver.*"]}
|
||||
metabase.legacy-mbql.schema.helpers/mbql-clause? {:namespaces ["metabase\\.driver.*"]}
|
||||
metabase.legacy-mbql.util/query->source-table-id {:namespaces ["metabase\\.driver.*"]}
|
||||
metabase.legacy-mbql.util/aggregation-at-index {:namespaces ["metabase\\.driver.*"]}
|
||||
metabase.legacy-mbql.util/assoc-field-options {:namespaces ["metabase\\.driver.*"]}
|
||||
metabase.legacy-mbql.util/expression-with-name {:namespaces ["metabase\\.driver.*"]}
|
||||
metabase.legacy-mbql.util/field-options {:namespaces ["metabase\\.driver.*"]}
|
||||
metabase.legacy-mbql.util/negate-filter-clause {:namespaces ["metabase\\.driver.*"]}
|
||||
metabase.legacy-mbql.util/normalize-token {:namespaces ["metabase\\.driver.*"]}
|
||||
metabase.legacy-mbql.util/update-field-options {:namespaces ["metabase\\.driver.*"]}}}}}
|
||||
{metabase.driver/database-supports? {:level :off}}}}
|
||||
|
||||
performance-sensitive
|
||||
{:linters
|
||||
{:discouraged-var
|
||||
{clojure.core/empty? {:message "Use metabase.util.performance/empty?"}
|
||||
clojure.core/not-empty {:message "Use metabase.util.performance/not-empty"}
|
||||
clojure.core/every? {:message "Use metabase.util.performance/every?"}
|
||||
{clojure.core/every? {:message "Use metabase.util.performance/every?"}
|
||||
clojure.core/mapv {:message "Use metabase.util.performance/mapv"}
|
||||
clojure.core/run! {:message "Use metabase.util.performance/run!"}
|
||||
clojure.core/get-in {:message "Use metabase.util.performance/get-in"}
|
||||
clojure.core/select-keys {:message "Use metabase.util.performance/select-keys"}
|
||||
clojure.core/some {:message "Use metabase.util.performance/some"}
|
||||
clojure.core/update-keys {:message "Use metabase.util.performance/update-keys"}
|
||||
clojure.walk/keywordize-keys {:message "Use metabase.util.performance/keywordize-keys"}
|
||||
clojure.walk/postwalk {:message "Use metabase.util.performance/postwalk"}
|
||||
clojure.walk/prewalk {:message "Use metabase.util.performance/prewalk"}
|
||||
clojure.walk/walk {:message "Use metabase.util.performance/walk"}
|
||||
clojure.walk/for {:message "Use metabase.util.performance/for"}
|
||||
clojure.walk/doseq {:message "Use metabase.util.performance/doseq"}}}}
|
||||
clojure.walk/walk {:message "Use metabase.util.performance/walk"}}}}
|
||||
|
||||
qp-and-driver-source-namespaces
|
||||
{:linters
|
||||
@@ -958,7 +866,16 @@
|
||||
metabase.lib.core/->legacy-MBQL {:level :off}}}}
|
||||
|
||||
qp-source-namespaces
|
||||
{}
|
||||
{:linters
|
||||
{:discouraged-namespace
|
||||
{metabase.legacy-mbql.js {:message "Avoid legacy MBQL namespaces in the QP, use Lib instead"}
|
||||
metabase.legacy-mbql.jvm-util {:message "Avoid legacy MBQL namespaces in the QP, use Lib instead"}
|
||||
metabase.legacy-mbql.normalize {:message "Avoid legacy MBQL namespaces in the QP, use Lib instead"}
|
||||
metabase.legacy-mbql.predicates {:message "Avoid legacy MBQL namespaces in the QP, use Lib instead"}
|
||||
metabase.legacy-mbql.schema {:message "Avoid legacy MBQL namespaces in the QP, use Lib instead"}
|
||||
metabase.legacy-mbql.schema.helpers {:message "Avoid legacy MBQL namespaces in the QP, use Lib instead"}
|
||||
metabase.legacy-mbql.schema.macros {:message "Avoid legacy MBQL namespaces in the QP, use Lib instead"}
|
||||
metabase.legacy-mbql.util {:message "Avoid legacy MBQL namespaces in the QP, use Lib instead"}}}}
|
||||
|
||||
general-qp-and-driver-test-namespaces
|
||||
{:linters
|
||||
@@ -969,11 +886,4 @@
|
||||
toucan2.tools.with-temp/with-temp {:message "Use lib.tu/mock-metadata-provider in QP tests. Exceptions: tests for code that doesn't use MPs or models not supported by MPs"}}
|
||||
|
||||
:metabase/disallow-hardcoded-driver-names-in-tests
|
||||
{:level :warning}}}
|
||||
|
||||
mage-namespaces
|
||||
{:linters
|
||||
{:discouraged-java-method
|
||||
{java.lang.System {exit
|
||||
{:level :error
|
||||
:message "Don't use System/exit directly. Instead use mage.util/exit and let mage handle exiting the process"}}}}}}}
|
||||
{:level :warning}}}}}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -100,7 +100,7 @@
|
||||
metabase.permissions.models.permissions/update-data-perms-graph!
|
||||
metabase.permissions.models.permissions/update-group-permissions!
|
||||
metabase.permissions.test-util/with-restored-perms!
|
||||
metabase.public-sharing-rest.api-test/add-card-to-dashboard!
|
||||
metabase.public-sharing.api-test/add-card-to-dashboard!
|
||||
metabase.pulse.send/send-notifications!
|
||||
metabase.pulse.send/send-pulse!
|
||||
metabase.query-processor.streaming.interface/begin!
|
||||
|
||||
24
.clj-kondo/macros/metabase/collections/api_test.clj
Normal file
24
.clj-kondo/macros/metabase/collections/api_test.clj
Normal file
@@ -0,0 +1,24 @@
|
||||
(ns macros.metabase.collections.api-test)
|
||||
|
||||
(defmacro with-collection-hierarchy! [bindings & body]
|
||||
`(let [~'a nil
|
||||
~'b nil
|
||||
~'c nil
|
||||
~'d nil
|
||||
~'e nil
|
||||
~'f nil
|
||||
~'g nil]
|
||||
~'a
|
||||
~'b
|
||||
~'c
|
||||
~'d
|
||||
~'e
|
||||
~'f
|
||||
~'g
|
||||
~bindings
|
||||
~@body))
|
||||
|
||||
(defmacro with-some-children-of-collection! [collection-or-id-or-nil & body]
|
||||
`(let [~'&ids ~collection-or-id-or-nil]
|
||||
~'&ids
|
||||
~@body))
|
||||
@@ -1,24 +0,0 @@
|
||||
(ns macros.metabase.collections-rest.api-test)
|
||||
|
||||
(defmacro with-collection-hierarchy! [bindings & body]
|
||||
`(let [~'a nil
|
||||
~'b nil
|
||||
~'c nil
|
||||
~'d nil
|
||||
~'e nil
|
||||
~'f nil
|
||||
~'g nil]
|
||||
~'a
|
||||
~'b
|
||||
~'c
|
||||
~'d
|
||||
~'e
|
||||
~'f
|
||||
~'g
|
||||
~bindings
|
||||
~@body))
|
||||
|
||||
(defmacro with-some-children-of-collection! [collection-or-id-or-nil & body]
|
||||
`(let [~'&ids ~collection-or-id-or-nil]
|
||||
~'&ids
|
||||
~@body))
|
||||
13
.clj-kondo/macros/metabase/embedding/api/embed_test.clj
Normal file
13
.clj-kondo/macros/metabase/embedding/api/embed_test.clj
Normal file
@@ -0,0 +1,13 @@
|
||||
(ns macros.metabase.embedding.api.embed-test)
|
||||
|
||||
(defmacro with-embedding-enabled-and-temp-card-referencing!
|
||||
[table-kw field-kw [card-binding] & body]
|
||||
`(let [~card-binding [~table-kw ~field-kw]]
|
||||
~@body))
|
||||
|
||||
(defmacro with-embedding-enabled-and-temp-dashcard-referencing!
|
||||
[table-kw field-kw [dash-binding card-binding dashcard-binding] & body]
|
||||
`(let [~dash-binding [~table-kw ~field-kw]
|
||||
~(or card-binding '_) nil
|
||||
~(or dashcard-binding '_) nil]
|
||||
~@body))
|
||||
@@ -1,13 +0,0 @@
|
||||
(ns macros.metabase.embedding-rest.api.embed-test)
|
||||
|
||||
(defmacro with-embedding-enabled-and-temp-card-referencing!
|
||||
[table-kw field-kw [card-binding] & body]
|
||||
`(let [~card-binding [~table-kw ~field-kw]]
|
||||
~@body))
|
||||
|
||||
(defmacro with-embedding-enabled-and-temp-dashcard-referencing!
|
||||
[table-kw field-kw [dash-binding card-binding dashcard-binding] & body]
|
||||
`(let [~dash-binding [~table-kw ~field-kw]
|
||||
~(or card-binding '_) nil
|
||||
~(or dashcard-binding '_) nil]
|
||||
~@body))
|
||||
13
.clj-kondo/macros/metabase/public_sharing/api_test.clj
Normal file
13
.clj-kondo/macros/metabase/public_sharing/api_test.clj
Normal file
@@ -0,0 +1,13 @@
|
||||
(ns macros.metabase.public-sharing.api-test)
|
||||
|
||||
(defmacro with-sharing-enabled-and-temp-card-referencing!
|
||||
[table-kw field-kw [card-binding] & body]
|
||||
`(let [~card-binding [~table-kw ~field-kw]]
|
||||
~@body))
|
||||
|
||||
(defmacro with-sharing-enabled-and-temp-dashcard-referencing!
|
||||
[table-kw field-kw [dash-binding card-binding dashcard-binding] & body]
|
||||
`(let [~dash-binding [~table-kw ~field-kw]
|
||||
~(or card-binding '_) nil
|
||||
~(or dashcard-binding '_) nil]
|
||||
~@body))
|
||||
@@ -1,13 +0,0 @@
|
||||
(ns macros.metabase.public-sharing-rest.api-test)
|
||||
|
||||
(defmacro with-sharing-enabled-and-temp-card-referencing!
|
||||
[table-kw field-kw [card-binding] & body]
|
||||
`(let [~card-binding [~table-kw ~field-kw]]
|
||||
~@body))
|
||||
|
||||
(defmacro with-sharing-enabled-and-temp-dashcard-referencing!
|
||||
[table-kw field-kw [dash-binding card-binding dashcard-binding] & body]
|
||||
`(let [~dash-binding [~table-kw ~field-kw]
|
||||
~(or card-binding '_) nil
|
||||
~(or dashcard-binding '_) nil]
|
||||
~@body))
|
||||
13
.clj-kondo/macros/metabase/queries/api/card_test.clj
Normal file
13
.clj-kondo/macros/metabase/queries/api/card_test.clj
Normal file
@@ -0,0 +1,13 @@
|
||||
(ns macros.metabase.queries.api.card-test)
|
||||
|
||||
(defmacro with-ordered-items
|
||||
[collection model-and-name-syms & body]
|
||||
`(let ~(into []
|
||||
(comp (partition-all 2)
|
||||
(mapcat (fn [[model binding]]
|
||||
[binding model])))
|
||||
model-and-name-syms)
|
||||
~collection
|
||||
~@(->> (partition-all 2 model-and-name-syms)
|
||||
(map second))
|
||||
~@body))
|
||||
@@ -1,13 +0,0 @@
|
||||
(ns macros.metabase.queries-rest.api.card-test)
|
||||
|
||||
(defmacro with-ordered-items
|
||||
[collection model-and-name-syms & body]
|
||||
`(let ~(into []
|
||||
(comp (partition-all 2)
|
||||
(mapcat (fn [[model binding]]
|
||||
[binding model])))
|
||||
model-and-name-syms)
|
||||
~collection
|
||||
~@(->> (partition-all 2 model-and-name-syms)
|
||||
(map second))
|
||||
~@body))
|
||||
@@ -0,0 +1,9 @@
|
||||
(ns macros.metabase.server.statistics-handler-test)
|
||||
|
||||
(defmacro with-server
|
||||
[_ _ [connector chan-start-handle chan-finish-handle chan-done-request] & body]
|
||||
`(let [~connector nil
|
||||
~chan-start-handle nil
|
||||
~chan-finish-handle nil
|
||||
~chan-done-request nil]
|
||||
~@body))
|
||||
16
.clj-kondo/macros/metabase/xrays/domain_entities/malli.clj
Normal file
16
.clj-kondo/macros/metabase/xrays/domain_entities/malli.clj
Normal file
@@ -0,0 +1,16 @@
|
||||
(ns macros.metabase.xrays.domain-entities.malli)
|
||||
|
||||
(defmacro define-getters-and-setters [schema & defs]
|
||||
`(do
|
||||
(comment ~schema) ;; Reference the schema
|
||||
~@(for [[sym _path] (partition 2 defs)]
|
||||
`(do
|
||||
(defn ~(vary-meta sym
|
||||
assoc :export true)
|
||||
"docs" [_x#] nil)
|
||||
(defn ~(vary-meta (symbol (str "with-" (name sym)))
|
||||
assoc :export true)
|
||||
"docs" [x# _new#] x#)
|
||||
(defn ~(vary-meta (symbol (str (name sym) "-js"))
|
||||
assoc :export true)
|
||||
"docs" [x#] x#)))))
|
||||
@@ -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
|
||||
|
||||
@@ -99,48 +99,12 @@
|
||||
:message "Metabase namespaces should have the form metabase[-enterprise].<module>.* [:metabase/namespace-name]"
|
||||
:type :metabase/namespace-name)))))))
|
||||
|
||||
(defn- ns-form-node->import-node [ns-form-node]
|
||||
(some (fn [node]
|
||||
(when (and (hooks/list-node? node)
|
||||
(let [first-child (first (:children node))]
|
||||
(and (hooks/keyword-node? first-child)
|
||||
(= (hooks/sexpr first-child) :import))))
|
||||
node))
|
||||
(:children ns-form-node)))
|
||||
|
||||
(defn- jsqlparser-import?
|
||||
"Check if a symbol or string represents a jsqlparser import."
|
||||
[s]
|
||||
(str/starts-with? (str s) "net.sf.jsqlparser"))
|
||||
|
||||
(defn- lint-jsqlparser-imports [ns-form-node]
|
||||
(when-let [import-node (ns-form-node->import-node ns-form-node)]
|
||||
(doseq [node (rest (:children import-node))
|
||||
:when (not (contains? (hooks.common/ignored-linters node) :metabase/no-jsqlparser-imports))]
|
||||
(cond
|
||||
;; Handle (net.sf.jsqlparser.foo Bar Baz) form
|
||||
(hooks/list-node? node)
|
||||
(let [package-node (first (:children node))
|
||||
package-name (when package-node (str (hooks/sexpr package-node)))]
|
||||
(when (jsqlparser-import? package-name)
|
||||
(hooks/reg-finding! (assoc (meta node)
|
||||
:message "Don't import net.sf.jsqlparser classes directly. This code should be agnostic to the parsing library. [:metabase/no-jsqlparser-imports]"
|
||||
:type :metabase/no-jsqlparser-imports))))
|
||||
|
||||
;; Handle net.sf.jsqlparser.foo.Bar form (single class import)
|
||||
(hooks/token-node? node)
|
||||
(when (jsqlparser-import? (hooks/sexpr node))
|
||||
(hooks/reg-finding! (assoc (meta node)
|
||||
:message "Don't import net.sf.jsqlparser classes directly. This code should be agnostic to the parsing library. [:metabase/no-jsqlparser-imports]"
|
||||
:type :metabase/no-jsqlparser-imports)))))))
|
||||
|
||||
(defn lint-ns [x]
|
||||
(doto (:node x)
|
||||
lint-require-shapes
|
||||
lint-requires-on-new-lines
|
||||
(lint-modules (modules/config x))
|
||||
lint-namespace-name
|
||||
lint-jsqlparser-imports)
|
||||
lint-namespace-name)
|
||||
x)
|
||||
|
||||
(comment
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[clojure.string :as str]
|
||||
[hooks.common]
|
||||
[hooks.common.modules :as modules]))
|
||||
[hooks.common]))
|
||||
|
||||
(defn- warn-about-disallowed-parallel-forms [form config]
|
||||
(letfn [(error! [form message]
|
||||
@@ -103,39 +102,7 @@
|
||||
(:children node))))]
|
||||
(walk node)))
|
||||
|
||||
(defn- test-namespace-lives-outside-of-module-system? [ns-symb]
|
||||
(some (fn [prefix]
|
||||
(str/starts-with? (name ns-symb) prefix))
|
||||
["build-drivers."
|
||||
"build."
|
||||
"i18n." ; bin/i18n
|
||||
"lint-migrations-file-test"
|
||||
"main-test" ; bin/release-list
|
||||
"metabase.deps-edn-test"
|
||||
"metabase.driver."
|
||||
"metabase.test."
|
||||
"metabase.test.data."
|
||||
"metabuild-common."])) ; utils for build scripts
|
||||
|
||||
(let [warned-namespaces (atom #{})]
|
||||
(defn deftest-check-in-valid-module
|
||||
"Check whether a `deftest` lives in a known module -- important so our system to only run a subset of tests based on
|
||||
which modules have changes picks them up correctly. (DEV-1125)"
|
||||
[{:keys [node], ns-symb :ns, :as input}]
|
||||
(when (and ns-symb
|
||||
(not (test-namespace-lives-outside-of-module-system? ns-symb))
|
||||
;; only warn once per namespace
|
||||
(not (contains? @warned-namespaces ns-symb)))
|
||||
(swap! warned-namespaces conj ns-symb)
|
||||
(let [known-modules (set (keys (:metabase/modules (modules/config input))))
|
||||
current-module (modules/module ns-symb)]
|
||||
(when (or (not current-module)
|
||||
(not (contains? known-modules current-module)))
|
||||
(hooks/reg-finding! (assoc (meta node)
|
||||
:message (format "All tests must live in a known module; %s is not a known module" (pr-str current-module))
|
||||
:type :metabase/tests-must-live-in-known-modules)))))))
|
||||
|
||||
(defn deftest [{:keys [node cljc lang config], :as input}]
|
||||
(defn deftest [{:keys [node cljc lang config]}]
|
||||
;; run [[deftest-check-parallel]] only once... if this is a `.cljc` file only run it for the `:clj` analysis, no point
|
||||
;; in running it twice.
|
||||
(when (or (not cljc)
|
||||
@@ -143,8 +110,7 @@
|
||||
(deftest-check-parallel node (get-in config [:linters :metabase/validate-deftest])))
|
||||
(deftest-check-not-horrifically-long node (get-in config [:linters :metabase/i-like-making-cams-eyes-bleed-with-horrifically-long-tests]))
|
||||
(deftest-check-no-driver-keywords node (get-in config [:linters :metabase/disallow-hardcoded-driver-names-in-tests]))
|
||||
(deftest-check-in-valid-module input)
|
||||
input)
|
||||
{:node node})
|
||||
|
||||
;;; this is a hacky way to determine whether these namespaces are required in the `ns` form or not... basically `:ns`
|
||||
;;; will come back as `nil` if they are not.
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
(ns hooks.common.modules
|
||||
(:require
|
||||
[clojure.string :as str]))
|
||||
(ns hooks.common.modules)
|
||||
|
||||
(defn ignored-namespace? [config ns-symb]
|
||||
(some
|
||||
@@ -19,19 +17,14 @@
|
||||
(module 'metabase-enterprise.whatever.core) => enterprise/whatever"
|
||||
[ns-symb]
|
||||
{:pre [(simple-symbol? ns-symb)]}
|
||||
;; treat something like `metabase.driver-test` (for a module that hasn't fully been updated to use `.core`
|
||||
;; namespaces) as being in the `driver` module
|
||||
(let [ns-symb (if (str/ends-with? (name ns-symb) "-test")
|
||||
(symbol (str/replace (name ns-symb) #"-test$" ""))
|
||||
ns-symb)]
|
||||
(or (some->> (re-find #"^metabase-enterprise\.([^.]+)" (str ns-symb))
|
||||
second
|
||||
(symbol "enterprise"))
|
||||
(some-> (re-find #"^metabase\.([^.]+)" (str ns-symb))
|
||||
second
|
||||
symbol))))
|
||||
(or (some->> (re-find #"^metabase-enterprise\.([^.]+)" (str ns-symb))
|
||||
second
|
||||
(symbol "enterprise"))
|
||||
(some-> (re-find #"^metabase\.([^.]+)" (str ns-symb))
|
||||
second
|
||||
symbol)))
|
||||
|
||||
(defn- module-api-namespaces
|
||||
(defn module-api-namespaces
|
||||
"Set of API namespace symbols for a given module. `:any` means you can use anything, there are no API namespaces for
|
||||
this module (yet). If unspecified, the default is just the `<module>.core` namespace."
|
||||
[config module]
|
||||
@@ -51,12 +44,6 @@
|
||||
(symbol (str ns-prefix ".core"))
|
||||
(symbol (str ns-prefix ".init"))}))))
|
||||
|
||||
(defn- module-friends
|
||||
[config module]
|
||||
"Set of modules that are `:friends` of `module`, i.e. allowed to use *any* namespace from the module, not just the
|
||||
designated [[module-api-namespaces]]."
|
||||
(set (get-in config [:metabase/modules module :friends])))
|
||||
|
||||
(defn allowed-modules
|
||||
"Set of namespace symbols that `module` is allowed to use. `:any` means it's allowed to use anything."
|
||||
[config module]
|
||||
@@ -67,22 +54,11 @@
|
||||
(or (= allowed-modules :any)
|
||||
(contains? (set allowed-modules) required-module))))
|
||||
|
||||
(defn- allowed-module-namespace? [config current-module ns-symb]
|
||||
(defn allowed-module-namespace? [config ns-symb]
|
||||
(let [module (module ns-symb)
|
||||
module-api-namespaces (module-api-namespaces config module)
|
||||
module-friends (module-friends config module)]
|
||||
module-api-namespaces (module-api-namespaces config module)]
|
||||
(or (empty? module-api-namespaces)
|
||||
(contains? module-api-namespaces ns-symb)
|
||||
(contains? module-friends current-module))))
|
||||
|
||||
(defn- rest-module? [module]
|
||||
(str/ends-with? module "-rest"))
|
||||
|
||||
(defn- routes-module? [module]
|
||||
(str/ends-with? module "-routes"))
|
||||
|
||||
(defn- core-module? [module]
|
||||
(str/ends-with? module "core"))
|
||||
(contains? module-api-namespaces ns-symb))))
|
||||
|
||||
(defn usage-error
|
||||
"Find usage errors when a `required-namespace` is required in the `current-module`. Returns a string describing the
|
||||
@@ -98,19 +74,8 @@
|
||||
current-module
|
||||
current-module)
|
||||
|
||||
(not (allowed-module-namespace? config current-module required-namespace))
|
||||
(not (allowed-module-namespace? config required-namespace))
|
||||
(format "Namespace %s is not an allowed external API namespace for the %s module. [:metabase/modules %s :api]"
|
||||
required-namespace
|
||||
required-module
|
||||
required-module)
|
||||
|
||||
;; (for now) rest modules are allowed to use one another; `routes` is ok because it collects routes together
|
||||
;; and `core` is ok because [[metabase.core.init]] might need to init some of the `-routes` modules'
|
||||
;; namespaces
|
||||
(and (not ((some-fn rest-module? routes-module? core-module?) current-module))
|
||||
(rest-module? required-module))
|
||||
(format "Do not use -rest modules (%s) in non-rest modules (%s) -- move things from %s to %s if needed"
|
||||
required-module
|
||||
current-module
|
||||
required-module
|
||||
(symbol (str/replace required-module #"-rest$" "")))))))
|
||||
required-module)))))
|
||||
|
||||
@@ -1,67 +1,7 @@
|
||||
(ns hooks.metabase.api.macros
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as api]
|
||||
[clojure.string :as str]))
|
||||
|
||||
(defn- validate-route!
|
||||
"Make sure the route uses kebab-case rather than snake_case."
|
||||
[route]
|
||||
(letfn [(validate-route-string! [route-string]
|
||||
(when (and (api/string-node? route-string)
|
||||
(str/includes? (api/sexpr route-string) "_"))
|
||||
(api/reg-finding! (assoc (meta route-string)
|
||||
:message "REST API routes and query params should use kebab-case, not snake_case"
|
||||
:type :metabase/validate-defendpoint-route-uses-kebab-case))))]
|
||||
(cond
|
||||
(api/string-node? route)
|
||||
(validate-route-string! route)
|
||||
;; otherwise route is a vector like ["/abc/:d/" :d <regex>]
|
||||
(api/vector-node? route)
|
||||
(validate-route-string! (first (:children route))))))
|
||||
|
||||
(defn- validate-query-params!
|
||||
"Make sure query params use kebab-case rather than snake_case.
|
||||
|
||||
This only works on inline schemas for now. If you define the schema elsewhere then we'd have to implement this
|
||||
validation in Clojure-land since we can't resolve the schemas in Kondo. It should however still work on complicated
|
||||
schemas like
|
||||
|
||||
[:merge <map-1> <map-2>]"
|
||||
[query-param-schema-node]
|
||||
(letfn [(map-schema? [node]
|
||||
(and (api/vector-node? node)
|
||||
(when-let [first-child (first (:children node))]
|
||||
(and (api/keyword-node? first-child)
|
||||
(= (api/sexpr first-child) :map)))))
|
||||
(map-schema-keys [{:keys [children], :as _node}]
|
||||
(let [child-schemas (if (api/map-node? (second children))
|
||||
(drop 2 children)
|
||||
(drop 1 children))]
|
||||
(keep (fn [child-schema]
|
||||
(when (api/vector-node? child-schema)
|
||||
(when-let [k (first (:children child-schema))]
|
||||
(when (api/keyword-node? k)
|
||||
k))))
|
||||
child-schemas)))
|
||||
(validate-map-schema! [form]
|
||||
;; only warn once per endpoint -- just once on the first bad node
|
||||
(when-let [bad-node (some (fn [key-node]
|
||||
(when (str/includes? (str (api/sexpr key-node)) "_")
|
||||
key-node))
|
||||
(map-schema-keys form))]
|
||||
(api/reg-finding! (assoc (meta bad-node)
|
||||
:message "REST API routes and query params should use kebab-case, not snake_case"
|
||||
:type :metabase/validate-defendpoint-query-params-use-kebab-case))))
|
||||
(validate! [node]
|
||||
(cond
|
||||
(and (api/vector-node? node)
|
||||
(map-schema? node))
|
||||
(validate-map-schema! node)
|
||||
|
||||
(api/vector-node? node)
|
||||
(doseq [child (:children node)]
|
||||
(validate! child))))]
|
||||
(validate! query-param-schema-node)))
|
||||
[hooks.common]))
|
||||
|
||||
(defn defendpoint
|
||||
[arg]
|
||||
@@ -78,25 +18,16 @@
|
||||
(cons nil more))
|
||||
[params & body] more
|
||||
[bindings schemas] (when (api/vector-node? params)
|
||||
(loop [bindings [], schemas [], [x y z & more] (:children params)]
|
||||
(loop [bindings [], schemas [], [x y & more] (:children params)]
|
||||
(cond
|
||||
(not x)
|
||||
[bindings schemas]
|
||||
|
||||
(and (api/keyword-node? y)
|
||||
(= (api/sexpr y) :-))
|
||||
(recur (conj bindings x) (conj schemas z) more)
|
||||
(= (api/sexpr x) :-)
|
||||
(recur bindings (conj schemas y) more)
|
||||
|
||||
:else
|
||||
(recur (conj bindings x) (conj schemas nil) (list* y z more)))))]
|
||||
(validate-route! route)
|
||||
(when-not result-schema
|
||||
(api/reg-finding! (assoc (meta node)
|
||||
:message "All REST API endpoints should have a detailed response schema"
|
||||
:type :metabase/validate-defendpoint-has-response-schema)))
|
||||
(let [[_route-param-schema query-param-schema _body-param-schema] schemas]
|
||||
(when query-param-schema
|
||||
(validate-query-params! query-param-schema)))
|
||||
(recur (conj bindings x) schemas (cons y more)))))]
|
||||
(-> (api/list-node
|
||||
(list
|
||||
(api/token-node 'do)
|
||||
@@ -123,27 +54,3 @@
|
||||
body))))
|
||||
(with-meta (meta node)))))]
|
||||
(update arg :node update-defendpoint)))
|
||||
|
||||
(comment
|
||||
(defn -defendpoint [form]
|
||||
(-> {:node (-> form pr-str api/parse-string)}
|
||||
defendpoint
|
||||
:node
|
||||
api/sexpr))
|
||||
|
||||
(defn x []
|
||||
(-defendpoint '(api.macros/defendpoint :get "/:id/syncable_schemas" ; snake_case route should trigger a warning
|
||||
"Returns a list of all syncable schemas found for the database `id`."
|
||||
[{:keys [id]} :- [:map
|
||||
[:id ms/PositiveInt]]
|
||||
;; snake_case query parameter, should trigger a warning
|
||||
{:keys [filter_by]} :- [:map
|
||||
[:filter_by {:optional true} :string]]]
|
||||
(let [db (get-database id)]
|
||||
(api/check-403 (or (:is_attached_dwh db)
|
||||
(and (mi/can-write? db)
|
||||
(mi/can-read? db))))
|
||||
(->> db
|
||||
(driver/syncable-schemas (:engine db))
|
||||
(vec)
|
||||
(sort)))))))
|
||||
|
||||
@@ -33,36 +33,18 @@
|
||||
(defclause [ag:var var] field-or-expression FieldOrExpressionDef)
|
||||
=>
|
||||
(def ag:var [FieldOrExpressionDef])"
|
||||
[{:keys [node], :as x}]
|
||||
(let [[_defclause clause-name & arg-specs] (:children node)]
|
||||
(assoc x :node (-> (hooks/list-node
|
||||
(list
|
||||
(hooks/token-node 'do)
|
||||
(-> (hooks/keyword-node (keyword "metabase.legacy-mbql.schema" (name (hooks/sexpr clause-name))))
|
||||
(with-meta (meta clause-name))
|
||||
(hooks/reg-keyword! 'metabase.legacy-mbql.schemas.macros/defclause))
|
||||
(collect-defclause-body-schemas arg-specs)))
|
||||
(with-meta (meta node))))))
|
||||
|
||||
(defn defclause*
|
||||
"e.g.
|
||||
|
||||
(defclause [ag:var var] field-or-expression FieldOrExpressionDef)
|
||||
=>
|
||||
(def ag:var [FieldOrExpressionDef])"
|
||||
[{:keys [node], :as x}]
|
||||
(let [[_defclause clause-name schema] (:children node)]
|
||||
(assoc x :node (-> (hooks/list-node
|
||||
(list
|
||||
(hooks/token-node 'do)
|
||||
(-> (hooks/keyword-node (keyword "metabase.legacy-mbql.schema" (name (hooks/sexpr clause-name))))
|
||||
(with-meta (meta clause-name))
|
||||
(hooks/reg-keyword! 'metabase.legacy-mbql.schemas.macros/defclause*))
|
||||
schema))
|
||||
(with-meta (meta node))))))
|
||||
[{:keys [node]}]
|
||||
(let [[_defclause clause-name-form & arg-specs] (:children node)]
|
||||
{:node (-> (hooks/list-node
|
||||
(list
|
||||
(hooks/token-node 'def)
|
||||
(unwrap-defclause-clause-name clause-name-form)
|
||||
(hooks/string-node "Docstring.")
|
||||
(collect-defclause-body-schemas arg-specs)))
|
||||
(with-meta (meta node)))}))
|
||||
|
||||
(comment
|
||||
(defn- expand-defclause [form]
|
||||
(defn- defclause* [form]
|
||||
(hooks/sexpr
|
||||
(:node
|
||||
(defclause
|
||||
@@ -73,35 +55,15 @@
|
||||
form)))}))))
|
||||
|
||||
(defn- x []
|
||||
(expand-defclause '(defclause var
|
||||
field-or-expression FieldOrExpressionDef)))
|
||||
(defclause* '(defclause [ag:var var]
|
||||
field-or-expression FieldOrExpressionDef)))
|
||||
;; =>
|
||||
(do :metabase.legacy-mbql.schema/var [FieldOrExpressionDef])
|
||||
(def ag:var "Docstring." [FieldOrExpressionDef])
|
||||
|
||||
(defn- y []
|
||||
(expand-defclause '(defclause var
|
||||
x (some-list-schema)
|
||||
y (optional FieldOrExpressionDef)
|
||||
z (rest (some-other-schema)))))
|
||||
(defclause* '(defclause var
|
||||
x (some-list-schema)
|
||||
y (optional FieldOrExpressionDef)
|
||||
z (rest (some-other-schema)))))
|
||||
;; =>
|
||||
(do :metabase.legacy-mbql.schema/var [(some-list-schema) FieldOrExpressionDef (some-other-schema)]))
|
||||
|
||||
(defn one-of [{:keys [node], :as x}]
|
||||
(let [[_one-of & schema-nodes] (:children node)
|
||||
node' (-> (hooks/vector-node
|
||||
(mapv (fn [schema-node]
|
||||
(if (and (hooks/token-node? schema-node)
|
||||
(simple-symbol? (hooks/sexpr schema-node)))
|
||||
(-> (hooks/keyword-node (keyword "mteabase.legacy-mbql.schema" (name (hooks/sexpr schema-node))))
|
||||
(with-meta (meta schema-node)))
|
||||
schema-node))
|
||||
schema-nodes))
|
||||
(with-meta (meta node)))]
|
||||
(assoc x :node node')))
|
||||
|
||||
(comment
|
||||
(defn- expand-one-of [form]
|
||||
(hooks/sexpr (:node (one-of {:node (hooks/parse-string (with-out-str (clojure.pprint/pprint form)))}))))
|
||||
|
||||
(defn- z []
|
||||
(expand-one-of '(one-of absolute-datetime relative-datetime time))))
|
||||
(def var "Docstring." [(some-list-schema) FieldOrExpressionDef (some-other-schema)]))
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
(ns hooks.metabase.query-processor.expressions-test
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[hooks.metabase.test.data :as data]))
|
||||
|
||||
(defn calculate-bird-scarcity
|
||||
[{{[_ formula filter-clause] :children} :node}]
|
||||
(let [formula* (data/replace-$id-special-tokens formula)
|
||||
filter-clause* (or (some-> filter-clause data/replace-$id-special-tokens)
|
||||
(hooks/token-node 'nil))
|
||||
node* (hooks/list-node
|
||||
(list
|
||||
(hooks/token-node 'do)
|
||||
formula*
|
||||
filter-clause*))]
|
||||
{:node node*}))
|
||||
@@ -0,0 +1,16 @@
|
||||
(ns hooks.metabase.query-processor-test.expressions-test
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[hooks.metabase.test.data :as data]))
|
||||
|
||||
(defn calculate-bird-scarcity
|
||||
[{{[_ formula filter-clause] :children} :node}]
|
||||
(let [formula* (data/replace-$id-special-tokens formula)
|
||||
filter-clause* (or (some-> filter-clause data/replace-$id-special-tokens)
|
||||
(hooks/token-node 'nil))
|
||||
node* (hooks/list-node
|
||||
(list
|
||||
(hooks/token-node 'do)
|
||||
formula*
|
||||
filter-clause*))]
|
||||
{:node node*}))
|
||||
@@ -97,6 +97,7 @@
|
||||
metabot-get-prompt-templates-url
|
||||
metabot-prompt-generator-token-limit
|
||||
multi-setting-read-only
|
||||
notebook-native-preview-shown
|
||||
notebook-native-preview-sidebar-width
|
||||
notification-link-base-url
|
||||
num-metabot-choices
|
||||
@@ -118,10 +119,10 @@
|
||||
reset-token-ttl-hours
|
||||
retired-setting
|
||||
retry-initial-interval
|
||||
retry-max-retries
|
||||
retry-max-attempts
|
||||
retry-max-interval-millis
|
||||
retry-multiplier
|
||||
retry-jitter-factor
|
||||
retry-randomization-factor
|
||||
saml-application-name
|
||||
saml-attribute-email
|
||||
saml-attribute-firstname
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
(ns hooks.metabase.test.util
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]))
|
||||
(:require [clj-kondo.hooks-api :as hooks]
|
||||
[hooks.common :as common]))
|
||||
|
||||
(defn- namespaced-symbol-node? [node]
|
||||
(when (hooks/token-node? node)
|
||||
|
||||
@@ -43,17 +43,6 @@
|
||||
'{:metabase/modules {api {:uses #{search}}
|
||||
search {:api #{metabase.search.core}}}}))))
|
||||
|
||||
(deftest ^:parallel module-checker-friends-test
|
||||
(is (= []
|
||||
(lint-modules
|
||||
'(ns metabase.search-rest.api
|
||||
(:require
|
||||
[metabase.search.config :as search.config]
|
||||
[metabase.search.core :as search]))
|
||||
'{:metabase/modules {search {:api #{metabase.search.core}
|
||||
:friends #{search-rest}}
|
||||
search-rest {:uses #{search}}}}))))
|
||||
|
||||
(deftest ^:parallel module-checker-api-namespaces-ignore-test
|
||||
(is (empty? (lint-modules
|
||||
'(ns metabase.api.search
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
(ns hooks.metabase.api.macros-test
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as api]
|
||||
[clj-kondo.impl.utils]
|
||||
[clojure.test :refer :all]
|
||||
[hooks.metabase.api.macros]))
|
||||
|
||||
@@ -70,41 +69,3 @@
|
||||
(respond :x)
|
||||
(catch Throwable e
|
||||
(raise e))))))))
|
||||
|
||||
(defn- defendpoint [form]
|
||||
(hooks.metabase.api.macros/defendpoint
|
||||
{:node (-> form pr-str api/parse-string)}))
|
||||
|
||||
(defn- defendpoint-warnings
|
||||
[form]
|
||||
(binding [clj-kondo.impl.utils/*ctx* {:config {:linters {:metabase/validate-defendpoint-route-uses-kebab-case {:level :warning}
|
||||
:metabase/validate-defendpoint-query-params-use-kebab-case {:level :warning}}}
|
||||
:ignores (atom nil)
|
||||
:findings (atom [])
|
||||
:namespaces (atom {})}]
|
||||
(defendpoint form)
|
||||
@(:findings clj-kondo.impl.utils/*ctx*)))
|
||||
|
||||
(deftest ^:parallel validate-route-kebab-case-test
|
||||
(is (=? [{:message "REST API routes and query params should use kebab-case, not snake_case"
|
||||
:type :metabase/validate-defendpoint-route-uses-kebab-case
|
||||
:row pos-int?
|
||||
:col pos-int?}]
|
||||
(defendpoint-warnings
|
||||
'(api.macros/defendpoint :get "/:id/syncable_schemas" ; snake_case route should trigger a warning
|
||||
"Returns a list of all syncable schemas found for the database `id`."
|
||||
[{:keys [id]}]
|
||||
id)))))
|
||||
|
||||
(deftest ^:parallel validate-query-params-kebab-case-test
|
||||
(is (=? [{:message "REST API routes and query params should use kebab-case, not snake_case",
|
||||
:type :metabase/validate-defendpoint-query-params-use-kebab-case
|
||||
:row pos-int?
|
||||
:col pos-int?}]
|
||||
(defendpoint-warnings
|
||||
'(api.macros/defendpoint :get "/:id/syncable-schemas"
|
||||
"Returns a list of all syncable schemas found for the database `id`."
|
||||
[{:keys [id]}
|
||||
{:keys [filter_by]} :- [:map
|
||||
[:filter_by {:optional true} :string]]]
|
||||
id)))))
|
||||
|
||||
629
.cljfmt.edn
629
.cljfmt.edn
@@ -38,322 +38,321 @@
|
||||
;; zero get indented 2 spaces if not on the first line regardless of number of elements on the first line.
|
||||
:extra-indents
|
||||
{;; clojure.core stuff
|
||||
fn* [[:inner 0]]
|
||||
let* [[:block 1]]
|
||||
with-meta [[:default]]
|
||||
fn* [[:inner 0]]
|
||||
let* [[:block 1]]
|
||||
with-meta [[:default]]
|
||||
;; library stuff. Libraries that specify their own `:style/indent` specs are included in the section after this
|
||||
;; since it is generated automatically.
|
||||
clojure.core.match/match [[:block 1]]
|
||||
clojure.test.check.properties/for-all [[:block 1]]
|
||||
clojure.test.check.properties/for-all* [[:block 1]]
|
||||
clojure.test.check/quick-check [[:block 1]]
|
||||
clojure.tools.macro/macrolet [[:block 1] [:inner 2 0]]
|
||||
clojurewerkz.quartzite.jobs/build [[:default]]
|
||||
clojurewerkz.quartzite.jobs/with-description [[:default]]
|
||||
clojurewerkz.quartzite.jobs/with-identity [[:default]]
|
||||
clojurewerkz.quartzite.schedule.cron/schedule [[:default]]
|
||||
clojurewerkz.quartzite.schedule.cron/with-misfire-handling-instruction-do-nothing [[:default]]
|
||||
clojurewerkz.quartzite.triggers/build [[:default]]
|
||||
clojurewerkz.quartzite.triggers/with-description [[:default]]
|
||||
clojurewerkz.quartzite.triggers/with-identity [[:default]]
|
||||
clojurewerkz.quartzite.triggers/with-priority [[:default]]
|
||||
clojurewerkz.quartzite.triggers/with-schedule [[:default]]
|
||||
def-map-type [[:block 2] [:inner 1]]
|
||||
defprotocol+ [[:block 1] [:inner 1]]
|
||||
defrecord+ [[:block 2] [:inner 1]]
|
||||
deftype+ [[:block 2] [:inner 1]]
|
||||
net.cgrand.macrovich/case [[:block 0]]
|
||||
potemkin.namespaces/import-vars [[:default]]
|
||||
potemkin.types/def-map-type [[:block 2] [:inner 1]]
|
||||
potemkin.types/defprotocol+ [[:block 1] [:inner 1]]
|
||||
potemkin.types/defrecord+ [[:block 2] [:inner 1]]
|
||||
potemkin.types/deftype+ [[:block 2] [:inner 1]]
|
||||
potemkin/def-map-type [[:block 2] [:inner 1]]
|
||||
potemkin/defprotocol+ [[:block 1] [:inner 1]]
|
||||
potemkin/defrecord+ [[:block 2] [:inner 1]]
|
||||
potemkin/deftype+ [[:block 2] [:inner 1]]
|
||||
potemkin/import-vars [[:default]]
|
||||
taoensso.nippy/extend-freeze [[:inner 0]]
|
||||
taoensso.nippy/extend-thaw [[:inner 0]]
|
||||
throttle.core/with-throttling [[:block 1]]
|
||||
clojure.core.logic/fresh [[:block 1]]
|
||||
clojure.core.logic/matcha [[:block 1]]
|
||||
clojure.core.logic/run [[:block 2]]
|
||||
clojure.core.match/match [[:block 1]]
|
||||
clojure.test.check.properties/for-all [[:block 1]]
|
||||
clojure.test.check.properties/for-all* [[:block 1]]
|
||||
clojure.test.check/quick-check [[:block 1]]
|
||||
clojure.tools.macro/macrolet [[:block 1] [:inner 2 0]]
|
||||
clojurewerkz.quartzite.jobs/build [[:default]]
|
||||
clojurewerkz.quartzite.jobs/with-description [[:default]]
|
||||
clojurewerkz.quartzite.jobs/with-identity [[:default]]
|
||||
clojurewerkz.quartzite.schedule.cron/schedule [[:default]]
|
||||
clojurewerkz.quartzite.schedule.cron/with-misfire-handling-instruction-do-nothing [[:default]]
|
||||
clojurewerkz.quartzite.triggers/build [[:default]]
|
||||
clojurewerkz.quartzite.triggers/with-description [[:default]]
|
||||
clojurewerkz.quartzite.triggers/with-identity [[:default]]
|
||||
clojurewerkz.quartzite.triggers/with-priority [[:default]]
|
||||
clojurewerkz.quartzite.triggers/with-schedule [[:default]]
|
||||
def-map-type [[:block 2] [:inner 1]]
|
||||
defprotocol+ [[:block 1] [:inner 1]]
|
||||
defrecord+ [[:block 2] [:inner 1]]
|
||||
deftype+ [[:block 2] [:inner 1]]
|
||||
net.cgrand.macrovich/case [[:block 0]]
|
||||
potemkin.namespaces/import-vars [[:default]]
|
||||
potemkin.types/def-map-type [[:block 2] [:inner 1]]
|
||||
potemkin.types/defprotocol+ [[:block 1] [:inner 1]]
|
||||
potemkin.types/defrecord+ [[:block 2] [:inner 1]]
|
||||
potemkin.types/deftype+ [[:block 2] [:inner 1]]
|
||||
potemkin/def-map-type [[:block 2] [:inner 1]]
|
||||
potemkin/defprotocol+ [[:block 1] [:inner 1]]
|
||||
potemkin/defrecord+ [[:block 2] [:inner 1]]
|
||||
potemkin/deftype+ [[:block 2] [:inner 1]]
|
||||
potemkin/import-vars [[:default]]
|
||||
taoensso.nippy/extend-freeze [[:inner 0]]
|
||||
taoensso.nippy/extend-thaw [[:inner 0]]
|
||||
throttle.core/with-throttling [[:block 1]]
|
||||
;; the rest of the stuff in this list is generated automatically based on `:style/indent` specs
|
||||
;; by running [[dev.generate-cljfmt-config/specs]]... use that code to update this list
|
||||
metabase-enterprise.cache.config-test/with-temp-persist-models [[:block 1]]
|
||||
metabase-enterprise.impersonation.util-test/with-impersonations! [[:inner 0]]
|
||||
metabase-enterprise.sandbox.test-util/with-gtaps! [[:inner 0]]
|
||||
metabase-enterprise.sandbox.test-util/with-gtaps-for-user! [[:inner 0]]
|
||||
metabase-enterprise.sandbox.test-util/with-user-attributes [[:block 2]]
|
||||
metabase-enterprise.serialization.test-util/with-dbs [[:inner 0]]
|
||||
metabase-enterprise.serialization.test-util/with-random-dump-dir [[:block 1]]
|
||||
metabase-enterprise.serialization.test-util/with-world [[:block 0]]
|
||||
metabase-enterprise.sso.integrations.jwt-test/with-users-with-email-deleted [[:block 1]]
|
||||
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]]
|
||||
metabase.actions.test-util/with-actions-enabled [[:block 0]]
|
||||
metabase.actions.test-util/with-actions-test-data [[:inner 0]]
|
||||
metabase.actions.test-util/with-actions-test-data-and-actions-enabled [[:block 0]]
|
||||
metabase.actions.test-util/with-actions-test-data-tables [[:block 1]]
|
||||
metabase.actions.test-util/with-empty-db [[:inner 0]]
|
||||
metabase.actions.test-util/with-temp-test-data [[:inner 0]]
|
||||
metabase.api.common/check [[:default]]
|
||||
metabase.api.common/define-routes [[:block 0]]
|
||||
metabase.api.common/let-404 [[:block 1]]
|
||||
metabase.app-db.core/with-application-db [[:inner 0]]
|
||||
metabase.app-db.liquibase/with-liquibase [[:block 1]]
|
||||
metabase.app-db.liquibase/with-scope-locked [[:block 1]]
|
||||
metabase.app-db.schema-migrations-test.impl/test-migrations [[:block 2]]
|
||||
metabase.cmd.copy/step [[:block 1]]
|
||||
metabase.cmd.copy/with-connection-rollback-only [[:block 1]]
|
||||
metabase.cmd.copy/with-disabled-db-constraints [[:block 2]]
|
||||
metabase.collections-rest.api-test/with-collection-hierarchy [[:block 1]]
|
||||
metabase.collections-rest.api-test/with-french-user-and-personal-collection [[:block 2]]
|
||||
metabase.collections-rest.api-test/with-some-children-of-collection [[:block 1]]
|
||||
metabase.collections.models.collection-test/with-collection-hierarchy [[:block 1]]
|
||||
metabase.collections.models.collection-test/with-current-user-perms-for-collections [[:block 1]]
|
||||
metabase.collections.models.collection-test/with-personal-and-impersonal-collections [[:block 1]]
|
||||
metabase.config/build-type-case [[:block 0]]
|
||||
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-in [[:inner 0]]
|
||||
metabase.driver.bigquery-cloud-sdk.query-processor/with-temporal-type [[:default]]
|
||||
metabase.driver.druid.client/DELETE [[:default]]
|
||||
metabase.driver.druid.client/GET [[:default]]
|
||||
metabase.driver.druid.client/POST [[:default]]
|
||||
metabase.driver.druid.query-processor-test/druid-query [[:block 0]]
|
||||
metabase.driver.druid.query-processor-test/druid-query-returning-rows [[:block 0]]
|
||||
metabase.driver.mongo.query-processor/mongo-let [[:block 1]]
|
||||
metabase.driver.sql-jdbc.actions/with-jdbc-transaction [[:block 1]]
|
||||
metabase.driver.sql-jdbc.connection/with-connection-spec-for-testing-connection [[:block 1]]
|
||||
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]]
|
||||
metabase.embedding-rest.api.embed-test/with-embedding-enabled-and-temp-dashcard-referencing [[:block 3]]
|
||||
metabase.embedding-rest.api.embed-test/with-new-secret-key [[:block 0]]
|
||||
metabase.embedding-rest.api.embed-test/with-temp-card [[:block 1]]
|
||||
metabase.embedding-rest.api.embed-test/with-temp-dashcard [[:block 1]]
|
||||
metabase.embedding-rest.api.preview-embed-test/with-embedding-enabled-and-new-secret-key [[:block 0]]
|
||||
metabase.embedding-rest.api.preview-embed-test/with-new-secret-key [[:block 0]]
|
||||
metabase.lib.binning/with-binning [[:default]]
|
||||
metabase.lib.core/with-binning [[:default]]
|
||||
metabase.lib.core/with-join-alias [[:default]]
|
||||
metabase.lib.core/with-join-conditions [[:default]]
|
||||
metabase.lib.join/with-join-alias [[:default]]
|
||||
metabase.lib.join/with-join-conditions [[:default]]
|
||||
metabase.lib.test-util.macros/$ids [[:inner 0]]
|
||||
metabase.lib.test-util.macros/mbql-5-query [[:inner 0]]
|
||||
metabase.lib.test-util.macros/mbql-query [[:inner 0]]
|
||||
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/replace [[: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]]
|
||||
metabase.parameters.chain-filter-test/with-clean-field-values-for-field [[:block 1]]
|
||||
metabase.parameters.chain-filter-test/with-fk-field-to-field-remapping! [[:block 0]]
|
||||
metabase.parameters.chain-filter-test/with-human-readable-values-remapping! [[:block 0]]
|
||||
metabase.permissions.core/allow-changing-all-users-group-members [[:block 0]]
|
||||
metabase.permissions.core/disable-perms-cache [[:block 0]]
|
||||
metabase.public-sharing-rest.api-test/with-sharing-enabled-and-temp-card-referencing [[:block 3]]
|
||||
metabase.public-sharing-rest.api-test/with-sharing-enabled-and-temp-dashcard-referencing [[:block 3]]
|
||||
metabase.public-sharing-rest.api-test/with-temp-public-card [[:block 1]]
|
||||
metabase.public-sharing-rest.api-test/with-temp-public-dashboard [[:block 1]]
|
||||
metabase.public-sharing-rest.api-test/with-temp-public-dashboard-and-card [[:block 1]]
|
||||
metabase.pulse.api.alert-test/with-alert-in-collection [[:block 1]]
|
||||
metabase.pulse.dashboard-subscription-test/with-link-card-fixture-for-dashboard [[:block 2]]
|
||||
metabase.pulse.models.pulse-test/with-dashboard-subscription-in-collection [[:block 1]]
|
||||
metabase.pulse.models.pulse-test/with-pulse-in-collection [[:inner 0]]
|
||||
metabase.queries-rest.api.card-test/with-cards-in-readable-collection [[:block 1]]
|
||||
metabase.queries-rest.api.card-test/with-cards-in-writeable-collection [[:block 1]]
|
||||
metabase.queries-rest.api.card-test/with-ordered-items [[:inner 0]]
|
||||
metabase.queries-rest.api.card-test/with-temp-native-card [[:block 1]]
|
||||
metabase.queries-rest.api.card-test/with-temp-native-card-with-params [[:block 1]]
|
||||
metabase.query-processor.debug/debug> [[:block 0]]
|
||||
metabase.query-processor.error-type/deferror [[:block 1]]
|
||||
metabase.query-processor.middleware.cache-backend.interface/with-cached-results [[:block 4]]
|
||||
metabase.query-processor.middleware.process-userland-query-test/with-query-execution [[:block 1]]
|
||||
metabase.query-processor.pipeline-queries-test/pmbql-query [[:block 1]]
|
||||
metabase.query-processor.pipeline-queries-test/run-pmbql-query [[:block 1]]
|
||||
metabase.query-processor.store/cached [[:block 1]]
|
||||
metabase.query-processor.store/with-metadata-provider [[:inner 0]]
|
||||
metabase.query-processor.streaming/streaming-response [[:block 1]]
|
||||
metabase.query-processor.test-util/format-rows-by [[:default]]
|
||||
metabase.query-processor.timeseries-test.util/test-timeseries-drivers [[:block 0]]
|
||||
metabase.query-processor.timeseries-test/run-mbql-query [[:inner 0]]
|
||||
metabase.request.core/as-admin [[:block 0]]
|
||||
metabase.request.core/with-current-request [[:inner 0]]
|
||||
metabase.request.core/with-current-user [[:inner 0]]
|
||||
metabase.request.core/with-limit-and-offset [[:inner 0]]
|
||||
metabase.request.session/as-admin [[:block 0]]
|
||||
metabase.request.session/with-current-user [[:inner 0]]
|
||||
metabase.sample-data.impl-test/with-temp-sample-database-db [[:block 1]]
|
||||
metabase.search.api-test/do-test-users [[:block 1]]
|
||||
metabase.server.middleware.session/as-admin [[:block 0]]
|
||||
metabase.server.middleware.session/with-current-user [[:inner 0]]
|
||||
metabase.server.streaming-response-test/with-streaming-response-thread-pool [[:block 0]]
|
||||
metabase.server.streaming-response-test/with-test-driver-db! [[:block 0]]
|
||||
metabase.server.streaming-response/streaming-response [[:block 2]]
|
||||
metabase.session.api/http-401-on-error [[:block 0]]
|
||||
metabase.settings.core/define-multi-setting [[:inner 0]]
|
||||
metabase.settings.core/defsetting [[:block 1]]
|
||||
metabase.settings.models.setting.multi-setting/define-multi-setting [[:inner 0]]
|
||||
metabase.settings.models.setting/defsetting [[:block 1]]
|
||||
metabase.sync.fetch-metadata/log-if-error [[:block 1]]
|
||||
metabase.sync.sync-metadata.fields-test/with-test-db [[:block 0]]
|
||||
metabase.sync.util/do-with-start-and-finish-debug-logging [[:default]]
|
||||
metabase.sync.util/sum-for [[:block 1]]
|
||||
metabase.sync.util/sync-operation [[:block 3]]
|
||||
metabase.sync.util/with-db-logging-disabled [[:default]]
|
||||
metabase.sync.util/with-duplicate-ops-prevented [[:default]]
|
||||
metabase.sync.util/with-emoji-progress-bar [[:block 1]]
|
||||
metabase.sync.util/with-error-handling [[:block 1]]
|
||||
metabase.sync.util/with-start-and-finish-logging [[:default]]
|
||||
metabase.sync.util/with-start-and-finish-logging* [[:default]]
|
||||
metabase.sync.util/with-sync-events [[:default]]
|
||||
metabase.task-history.models.task-history/with-task-history [[:block 1]]
|
||||
metabase.task.core/rerun-on-error [[:block 1]]
|
||||
metabase.task.impl/rerun-on-error [[:block 1]]
|
||||
metabase.test.data.datasets/test-driver [[:inner 0]]
|
||||
metabase.test.data.datasets/test-drivers [[:inner 0]]
|
||||
metabase.test.data.one-off-dbs/with-blank-db [[:block 0]]
|
||||
metabase.test.data.one-off-dbs/with-blueberries-db [[:block 0]]
|
||||
metabase.test.data.users/with-test-user [[:inner 0]]
|
||||
metabase.test.data/$ids [[:inner 0]]
|
||||
metabase.test.data/dataset [[:inner 0]]
|
||||
metabase.test.data/mbql-query [[:inner 0]]
|
||||
metabase.test.data/query [[:block 1]]
|
||||
metabase.test.data/run-mbql-query [[:inner 0]]
|
||||
metabase.test.data/with-empty-h2-app-db! [[:block 0]]
|
||||
metabase.test.data/with-temp-copy-of-db [[:block 0]]
|
||||
metabase.test.util.public-settings/with-additional-premium-features [[:block 1]]
|
||||
metabase.test.util.public-settings/with-premium-features [[:block 1]]
|
||||
metabase.test.util.thread-local/test-helpers-set-global-values! [[:block 0]]
|
||||
metabase.test.util/discard-setting-changes [[:block 1]]
|
||||
metabase.test.util/let-url [[:block 1]]
|
||||
metabase.test.util/throw-if-called! [[:block 1]]
|
||||
metabase.test.util/with-temp-scheduler [[:block 0]]
|
||||
metabase.test.util/with-temp-vals-in-db [[:block 3]]
|
||||
metabase.test.util/with-user-in-groups [[:inner 0]]
|
||||
metabase.test/$ids [[:inner 0]]
|
||||
metabase.test/as-admin [[:inner 0]]
|
||||
metabase.test/dataset [[:inner 0]]
|
||||
metabase.test/discard-setting-changes [[:block 1]]
|
||||
metabase.test/format-rows-by [[:default]]
|
||||
metabase.test/let-url [[:block 1]]
|
||||
metabase.test/mbql-query [[:inner 0]]
|
||||
metabase.test/query [[:block 1]]
|
||||
metabase.test/run-mbql-query [[:inner 0]]
|
||||
metabase.test/test-driver [[:inner 0]]
|
||||
metabase.test/test-drivers [[:inner 0]]
|
||||
metabase.test/test-helpers-set-global-values! [[:block 0]]
|
||||
metabase.test/throw-if-called! [[:block 1]]
|
||||
metabase.test/with-actions [[:block 1]]
|
||||
metabase.test/with-actions-disabled [[:block 0]]
|
||||
metabase.test/with-actions-enabled [[:block 0]]
|
||||
metabase.test/with-actions-test-data [[:inner 0]]
|
||||
metabase.test/with-actions-test-data-and-actions-enabled [[:block 0]]
|
||||
metabase.test/with-actions-test-data-tables [[:block 1]]
|
||||
metabase.test/with-additional-premium-features [[:block 1]]
|
||||
metabase.test/with-current-user [[:inner 0]]
|
||||
metabase.test/with-driver [[:block 1]]
|
||||
metabase.test/with-empty-db [[:inner 0]]
|
||||
metabase.test/with-empty-h2-app-db! [[:block 0]]
|
||||
metabase.test/with-metadata-provider [[:inner 0]]
|
||||
metabase.test/with-premium-features [[:block 1]]
|
||||
metabase.test/with-temp [[:inner 0]]
|
||||
metabase.test/with-temp-copy-of-db [[:block 0]]
|
||||
metabase.test/with-temp-scheduler [[:block 0]]
|
||||
metabase.test/with-temp-test-data [[:inner 0]]
|
||||
metabase.test/with-temp-vals-in-db [[:block 3]]
|
||||
metabase.test/with-test-user [[:inner 0]]
|
||||
metabase.test/with-user-in-groups [[:inner 0]]
|
||||
metabase.upload.impl-test/with-upload-table! [[:inner 0]]
|
||||
metabase.upload.types/with-currency [[:default]]
|
||||
metabase.upload.types/with-parens [[:default]]
|
||||
metabase.util.async/cancelable-thread [[:block 0]]
|
||||
metabase.util.date-2/with-time-zone-same-instant [[:default]]
|
||||
metabase.util.encryption-test/with-secret-key [[:block 1]]
|
||||
metabase.util.honey-sql-2/with-database-type-info [[:default]]
|
||||
metabase.util.i18n/deferred-trs [[:default]]
|
||||
metabase.util.i18n/deferred-tru [[:default]]
|
||||
metabase.util.i18n/trs [[:default]]
|
||||
metabase.util.i18n/trsn [[:default]]
|
||||
metabase.util.i18n/tru [[:default]]
|
||||
metabase.util.i18n/trun [[:default]]
|
||||
metabase.util.jvm/auto-retry [[:block 1]]
|
||||
metabase.util.jvm/varargs [[:block 1]]
|
||||
metabase.util.jvm/with-us-locale [[:block 0]]
|
||||
metabase.util.malli/disable-enforcement [[:block 0]]
|
||||
metabase.util.malli/with [[:default]]
|
||||
metabase.util.malli/with-api-error-message [[:default]]
|
||||
metabase.util.namespaces/import-fns [[:default]]
|
||||
metabase.util.queue-test/await-test-while [[:block 1]]
|
||||
metabase.util/auto-retry [[:block 1]]
|
||||
metabase.util/case-enum [[:block 1]]
|
||||
metabase.util/for-map [[:block 1]]
|
||||
metabase.util/ignore-exceptions [[:block 0]]
|
||||
metabase.util/or-with [[:block 1]]
|
||||
metabase.util/profile [[:block 1]]
|
||||
metabase.util/prog1 [[:inner 0]]
|
||||
metabase.util/varargs [[:inner 0]]
|
||||
metabase.util/with-us-locale [[:block 0]]
|
||||
metabuild-common.core/exit-when-finished-nonzero-on-exception [[:block 0]]
|
||||
metabuild-common.core/step [[:block 1]]
|
||||
metabuild-common.entrypoint/exit-when-finished-nonzero-on-exception [[:block 0]]
|
||||
metabuild-common.steps/step [[:block 1]]
|
||||
methodical.core/defmethod [[:inner 0]]
|
||||
methodical.core/defmulti [[:inner 0]]
|
||||
methodical.core/with-dispatcher [[:default]]
|
||||
methodical.core/with-method-table [[:default]]
|
||||
methodical.core/with-prefers [[:default]]
|
||||
methodical.interface/with-dispatcher [[:default]]
|
||||
methodical.interface/with-method-table [[:default]]
|
||||
methodical.interface/with-prefers [[:default]]
|
||||
methodical.macros/defmethod [[:inner 0]]
|
||||
methodical.macros/defmulti [[:inner 0]]
|
||||
saml20-clj.sp.response/validate-confirmation-datas [[:block 1]]
|
||||
toucan2.connection/with-transaction [[:block 1]]
|
||||
toucan2.core/build [[:block 0]]
|
||||
toucan2.core/compile [[:block 0]]
|
||||
toucan2.core/debug [[:block 0]]
|
||||
toucan2.core/define-after-insert [[:inner 0]]
|
||||
toucan2.core/define-after-select [[:inner 0]]
|
||||
toucan2.core/define-after-update [[:inner 0]]
|
||||
toucan2.core/define-before-delete [[:inner 0]]
|
||||
toucan2.core/define-before-insert [[:inner 0]]
|
||||
toucan2.core/define-before-select [[:inner 0]]
|
||||
toucan2.core/define-default-fields [[:inner 0]]
|
||||
toucan2.core/define-named-query [[:block 1]]
|
||||
toucan2.core/deftransforms [[:block 1]]
|
||||
toucan2.core/with-transaction [[:block 1]]
|
||||
toucan2.tools.after-insert/define-after-insert [[:inner 0]]
|
||||
toucan2.tools.after-select/define-after-select [[:inner 0]]
|
||||
toucan2.tools.after-update/define-after-update [[:inner 0]]
|
||||
toucan2.tools.after/define-after-impl [[:default]]
|
||||
toucan2.tools.before-delete/define-before-delete [[:inner 0]]
|
||||
toucan2.tools.before-insert/define-before-insert [[:inner 0]]
|
||||
toucan2.tools.before-select/define-before-select [[:inner 0]]
|
||||
toucan2.tools.compile/build [[:block 0]]
|
||||
toucan2.tools.compile/compile [[:block 0]]
|
||||
toucan2.tools.compile/resolved [[:block 0]]
|
||||
toucan2.tools.debug/debug [[:block 0]]
|
||||
toucan2.tools.default-fields/define-default-fields [[:inner 0]]
|
||||
toucan2.tools.named-query/define-named-query [[:block 1]]
|
||||
toucan2.tools.simple-out-transform/define-out-transform [[:inner 0]]
|
||||
toucan2.tools.transformed/deftransforms [[:block 1]]
|
||||
toucan2.tools.with-temp/with-temp [[:inner 0]]
|
||||
toucan2.util/try-with-error-context [[:inner 0]]}}
|
||||
metabase-enterprise.cache.config-test/with-temp-persist-models [[:block 1]]
|
||||
metabase-enterprise.impersonation.util-test/with-impersonations! [[:inner 0]]
|
||||
metabase-enterprise.sandbox.test-util/with-gtaps! [[:inner 0]]
|
||||
metabase-enterprise.sandbox.test-util/with-gtaps-for-user! [[:inner 0]]
|
||||
metabase-enterprise.sandbox.test-util/with-user-attributes [[:block 2]]
|
||||
metabase-enterprise.serialization.test-util/with-dbs [[:inner 0]]
|
||||
metabase-enterprise.serialization.test-util/with-random-dump-dir [[:block 1]]
|
||||
metabase-enterprise.serialization.test-util/with-world [[:block 0]]
|
||||
metabase-enterprise.sso.integrations.jwt-test/with-users-with-email-deleted [[:block 1]]
|
||||
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.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]]
|
||||
metabase.actions.test-util/with-actions-enabled [[:block 0]]
|
||||
metabase.actions.test-util/with-actions-test-data [[:inner 0]]
|
||||
metabase.actions.test-util/with-actions-test-data-and-actions-enabled [[:block 0]]
|
||||
metabase.actions.test-util/with-actions-test-data-tables [[:block 1]]
|
||||
metabase.actions.test-util/with-empty-db [[:inner 0]]
|
||||
metabase.actions.test-util/with-temp-test-data [[:inner 0]]
|
||||
metabase.api.common/check [[:default]]
|
||||
metabase.api.common/define-routes [[:block 0]]
|
||||
metabase.api.common/let-404 [[:block 1]]
|
||||
metabase.app-db.core/with-application-db [[:inner 0]]
|
||||
metabase.app-db.liquibase/with-liquibase [[:block 1]]
|
||||
metabase.app-db.liquibase/with-scope-locked [[:block 1]]
|
||||
metabase.app-db.schema-migrations-test.impl/test-migrations [[:block 2]]
|
||||
metabase.cmd.copy/step [[:block 1]]
|
||||
metabase.cmd.copy/with-connection-rollback-only [[:block 1]]
|
||||
metabase.cmd.copy/with-disabled-db-constraints [[:block 2]]
|
||||
metabase.collections.api-test/with-collection-hierarchy [[:block 1]]
|
||||
metabase.collections.api-test/with-french-user-and-personal-collection [[:block 2]]
|
||||
metabase.collections.api-test/with-some-children-of-collection [[:block 1]]
|
||||
metabase.collections.models.collection-test/with-collection-hierarchy [[:block 1]]
|
||||
metabase.collections.models.collection-test/with-current-user-perms-for-collections [[:block 1]]
|
||||
metabase.collections.models.collection-test/with-personal-and-impersonal-collections [[:block 1]]
|
||||
metabase.config/build-type-case [[:block 0]]
|
||||
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/replace [[: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]]
|
||||
metabase.driver.druid.client/GET [[:default]]
|
||||
metabase.driver.druid.client/POST [[:default]]
|
||||
metabase.driver.druid.query-processor-test/druid-query [[:block 0]]
|
||||
metabase.driver.druid.query-processor-test/druid-query-returning-rows [[:block 0]]
|
||||
metabase.driver.mongo.query-processor/mongo-let [[:block 1]]
|
||||
metabase.driver.sql-jdbc.actions/with-jdbc-transaction [[:block 1]]
|
||||
metabase.driver.sql-jdbc.connection/with-connection-spec-for-testing-connection [[:block 1]]
|
||||
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.embedding.api.embed-test/do-response-formats [[:block 1]]
|
||||
metabase.embedding.api.embed-test/with-embedding-enabled-and-new-secret-key [[:block 0]]
|
||||
metabase.embedding.api.embed-test/with-embedding-enabled-and-temp-card-referencing [[:block 3]]
|
||||
metabase.embedding.api.embed-test/with-embedding-enabled-and-temp-dashcard-referencing [[:block 3]]
|
||||
metabase.embedding.api.embed-test/with-new-secret-key [[:block 0]]
|
||||
metabase.embedding.api.embed-test/with-temp-card [[:block 1]]
|
||||
metabase.embedding.api.embed-test/with-temp-dashcard [[:block 1]]
|
||||
metabase.embedding.api.preview-embed-test/with-embedding-enabled-and-new-secret-key [[:block 0]]
|
||||
metabase.embedding.api.preview-embed-test/with-new-secret-key [[:block 0]]
|
||||
metabase.lib.binning/with-binning [[:default]]
|
||||
metabase.lib.core/with-binning [[:default]]
|
||||
metabase.lib.core/with-join-alias [[:default]]
|
||||
metabase.lib.core/with-join-conditions [[:default]]
|
||||
metabase.lib.join/with-join-alias [[:default]]
|
||||
metabase.lib.join/with-join-conditions [[:default]]
|
||||
metabase.lib.test-util.macros/$ids [[:inner 0]]
|
||||
metabase.lib.test-util.macros/mbql-5-query [[:inner 0]]
|
||||
metabase.lib.test-util.macros/mbql-query [[:inner 0]]
|
||||
metabase.lib.util.match/match [[:inner 0]]
|
||||
metabase.lib.util.match/match-one [[:inner 0]]
|
||||
metabase.lib.util.match/match-lite [[:inner 0]]
|
||||
metabase.lib.util.match/match-lite-recursive [[:inner 0]]
|
||||
metabase.lib.util.match/replace [[:inner 0]]
|
||||
metabase.lib.util.match/replace-in [[:inner 0]]
|
||||
metabase.models.interface/define-batched-hydration-method [[:inner 0]]
|
||||
metabase.models.interface/define-simple-hydration-method [[:inner 0]]
|
||||
metabase.parameters.chain-filter-test/with-clean-field-values-for-field [[:block 1]]
|
||||
metabase.parameters.chain-filter-test/with-fk-field-to-field-remapping [[:block 0]]
|
||||
metabase.parameters.chain-filter-test/with-human-readable-values-remapping [[:block 0]]
|
||||
metabase.permissions.core/allow-changing-all-users-group-members [[:block 0]]
|
||||
metabase.permissions.core/disable-perms-cache [[:block 0]]
|
||||
metabase.public-sharing.api-test/with-sharing-enabled-and-temp-card-referencing [[:block 3]]
|
||||
metabase.public-sharing.api-test/with-sharing-enabled-and-temp-dashcard-referencing [[:block 3]]
|
||||
metabase.public-sharing.api-test/with-temp-public-card [[:block 1]]
|
||||
metabase.public-sharing.api-test/with-temp-public-dashboard [[:block 1]]
|
||||
metabase.public-sharing.api-test/with-temp-public-dashboard-and-card [[:block 1]]
|
||||
metabase.pulse.api.alert-test/with-alert-in-collection [[:block 1]]
|
||||
metabase.pulse.dashboard-subscription-test/with-link-card-fixture-for-dashboard [[:block 2]]
|
||||
metabase.pulse.models.pulse-test/with-dashboard-subscription-in-collection [[:block 1]]
|
||||
metabase.pulse.models.pulse-test/with-pulse-in-collection [[:inner 0]]
|
||||
metabase.queries.api.card-test/with-cards-in-readable-collection [[:block 1]]
|
||||
metabase.queries.api.card-test/with-cards-in-writeable-collection [[:block 1]]
|
||||
metabase.queries.api.card-test/with-ordered-items [[:inner 0]]
|
||||
metabase.queries.api.card-test/with-temp-native-card! [[:block 1]]
|
||||
metabase.queries.api.card-test/with-temp-native-card-with-params! [[:block 1]]
|
||||
metabase.query-processor-test.pipeline-queries-test/pmbql-query [[:block 1]]
|
||||
metabase.query-processor-test.pipeline-queries-test/run-pmbql-query [[:block 1]]
|
||||
metabase.query-processor.debug/debug> [[:block 0]]
|
||||
metabase.query-processor.error-type/deferror [[:block 1]]
|
||||
metabase.query-processor.middleware.cache-backend.interface/with-cached-results [[:block 4]]
|
||||
metabase.query-processor.middleware.process-userland-query-test/with-query-execution [[:block 1]]
|
||||
metabase.query-processor.store/cached [[:block 1]]
|
||||
metabase.query-processor.store/with-metadata-provider [[:inner 0]]
|
||||
metabase.query-processor.streaming/streaming-response [[:block 1]]
|
||||
metabase.query-processor.test-util/format-rows-by [[:default]]
|
||||
metabase.request.core/as-admin [[:block 0]]
|
||||
metabase.request.core/with-current-request [[:inner 0]]
|
||||
metabase.request.core/with-current-user [[:inner 0]]
|
||||
metabase.request.core/with-limit-and-offset [[:inner 0]]
|
||||
metabase.request.session/as-admin [[:block 0]]
|
||||
metabase.request.session/with-current-user [[:inner 0]]
|
||||
metabase.sample-data.impl-test/with-temp-sample-database-db [[:block 1]]
|
||||
metabase.search.api-test/do-test-users [[:block 1]]
|
||||
metabase.server.middleware.session/as-admin [[:block 0]]
|
||||
metabase.server.middleware.session/with-current-user [[:inner 0]]
|
||||
metabase.server.streaming-response-test/with-streaming-response-thread-pool [[:block 0]]
|
||||
metabase.server.streaming-response-test/with-test-driver-db! [[:block 0]]
|
||||
metabase.server.streaming-response/streaming-response [[:block 2]]
|
||||
metabase.session.api/http-401-on-error [[:block 0]]
|
||||
metabase.settings.core/define-multi-setting [[:inner 0]]
|
||||
metabase.settings.core/defsetting [[:block 1]]
|
||||
metabase.settings.models.setting.multi-setting/define-multi-setting [[:inner 0]]
|
||||
metabase.settings.models.setting/defsetting [[:block 1]]
|
||||
metabase.sync.fetch-metadata/log-if-error [[:block 1]]
|
||||
metabase.sync.sync-metadata.fields-test/with-test-db [[:block 0]]
|
||||
metabase.sync.util/do-with-start-and-finish-debug-logging [[:default]]
|
||||
metabase.sync.util/sum-for [[:block 1]]
|
||||
metabase.sync.util/sync-operation [[:block 3]]
|
||||
metabase.sync.util/with-db-logging-disabled [[:default]]
|
||||
metabase.sync.util/with-duplicate-ops-prevented [[:default]]
|
||||
metabase.sync.util/with-emoji-progress-bar [[:block 1]]
|
||||
metabase.sync.util/with-error-handling [[:block 1]]
|
||||
metabase.sync.util/with-start-and-finish-logging [[:default]]
|
||||
metabase.sync.util/with-start-and-finish-logging* [[:default]]
|
||||
metabase.sync.util/with-sync-events [[:default]]
|
||||
metabase.task-history.models.task-history/with-task-history [[:block 1]]
|
||||
metabase.task.core/rerun-on-error [[:block 1]]
|
||||
metabase.task.impl/rerun-on-error [[:block 1]]
|
||||
metabase.test.data.datasets/test-driver [[:inner 0]]
|
||||
metabase.test.data.datasets/test-drivers [[:inner 0]]
|
||||
metabase.test.data.one-off-dbs/with-blank-db [[:block 0]]
|
||||
metabase.test.data.one-off-dbs/with-blueberries-db [[:block 0]]
|
||||
metabase.test.data.users/with-test-user [[:inner 0]]
|
||||
metabase.test.data/$ids [[:inner 0]]
|
||||
metabase.test.data/dataset [[:inner 0]]
|
||||
metabase.test.data/mbql-query [[:inner 0]]
|
||||
metabase.test.data/query [[:block 1]]
|
||||
metabase.test.data/run-mbql-query [[:inner 0]]
|
||||
metabase.test.data/with-empty-h2-app-db! [[:block 0]]
|
||||
metabase.test.data/with-temp-copy-of-db [[:block 0]]
|
||||
metabase.test.util.public-settings/with-additional-premium-features [[:block 1]]
|
||||
metabase.test.util.public-settings/with-premium-features [[:block 1]]
|
||||
metabase.test.util.thread-local/test-helpers-set-global-values! [[:block 0]]
|
||||
metabase.test.util/discard-setting-changes [[:block 1]]
|
||||
metabase.test.util/let-url [[:block 1]]
|
||||
metabase.test.util/throw-if-called! [[:block 1]]
|
||||
metabase.test.util/with-temp-scheduler [[:block 0]]
|
||||
metabase.test.util/with-temp-vals-in-db [[:block 3]]
|
||||
metabase.test.util/with-user-in-groups [[:inner 0]]
|
||||
metabase.test/$ids [[:inner 0]]
|
||||
metabase.test/as-admin [[:inner 0]]
|
||||
metabase.test/dataset [[:inner 0]]
|
||||
metabase.test/discard-setting-changes [[:block 1]]
|
||||
metabase.test/format-rows-by [[:default]]
|
||||
metabase.test/let-url [[:block 1]]
|
||||
metabase.test/mbql-query [[:inner 0]]
|
||||
metabase.test/query [[:block 1]]
|
||||
metabase.test/run-mbql-query [[:inner 0]]
|
||||
metabase.test/test-driver [[:inner 0]]
|
||||
metabase.test/test-drivers [[:inner 0]]
|
||||
metabase.test/test-helpers-set-global-values! [[:block 0]]
|
||||
metabase.test/throw-if-called! [[:block 1]]
|
||||
metabase.test/with-actions [[:block 1]]
|
||||
metabase.test/with-actions-disabled [[:block 0]]
|
||||
metabase.test/with-actions-enabled [[:block 0]]
|
||||
metabase.test/with-actions-test-data [[:inner 0]]
|
||||
metabase.test/with-actions-test-data-and-actions-enabled [[:block 0]]
|
||||
metabase.test/with-actions-test-data-tables [[:block 1]]
|
||||
metabase.test/with-additional-premium-features [[:block 1]]
|
||||
metabase.test/with-current-user [[:inner 0]]
|
||||
metabase.test/with-driver [[:block 1]]
|
||||
metabase.test/with-empty-db [[:inner 0]]
|
||||
metabase.test/with-empty-h2-app-db! [[:block 0]]
|
||||
metabase.test/with-metadata-provider [[:inner 0]]
|
||||
metabase.test/with-premium-features [[:block 1]]
|
||||
metabase.test/with-temp [[:inner 0]]
|
||||
metabase.test/with-temp-copy-of-db [[:block 0]]
|
||||
metabase.test/with-temp-scheduler [[:block 0]]
|
||||
metabase.test/with-temp-test-data [[:inner 0]]
|
||||
metabase.test/with-temp-vals-in-db [[:block 3]]
|
||||
metabase.test/with-test-user [[:inner 0]]
|
||||
metabase.test/with-user-in-groups [[:inner 0]]
|
||||
metabase.timeseries-query-processor-test.util/test-timeseries-drivers [[:block 0]]
|
||||
metabase.timeseries-query-processor-test/run-mbql-query [[:inner 0]]
|
||||
metabase.upload.impl-test/with-upload-table! [[:inner 0]]
|
||||
metabase.upload.types/with-currency [[:default]]
|
||||
metabase.upload.types/with-parens [[:default]]
|
||||
metabase.util/for-map [[:block 1]]
|
||||
metabase.util.async/cancelable-thread [[:block 0]]
|
||||
metabase.util.date-2/with-time-zone-same-instant [[:default]]
|
||||
metabase.util.encryption-test/with-secret-key [[:block 1]]
|
||||
metabase.util.honey-sql-2/with-database-type-info [[:default]]
|
||||
metabase.util.i18n/deferred-trs [[:default]]
|
||||
metabase.util.i18n/deferred-tru [[:default]]
|
||||
metabase.util.i18n/trs [[:default]]
|
||||
metabase.util.i18n/trsn [[:default]]
|
||||
metabase.util.i18n/tru [[:default]]
|
||||
metabase.util.i18n/trun [[:default]]
|
||||
metabase.util.jvm/auto-retry [[:block 1]]
|
||||
metabase.util.jvm/varargs [[:block 1]]
|
||||
metabase.util.jvm/with-us-locale [[:block 0]]
|
||||
metabase.util.malli/disable-enforcement [[:block 0]]
|
||||
metabase.util.malli/with [[:default]]
|
||||
metabase.util.malli/with-api-error-message [[:default]]
|
||||
metabase.util.namespaces/import-fns [[:default]]
|
||||
metabase.util.queue-test/await-test-while [[:block 1]]
|
||||
metabase.util/auto-retry [[:block 1]]
|
||||
metabase.util/case-enum [[:block 1]]
|
||||
metabase.util/ignore-exceptions [[:block 0]]
|
||||
metabase.util/or-with [[:block 1]]
|
||||
metabase.util/profile [[:block 1]]
|
||||
metabase.util/prog1 [[:inner 0]]
|
||||
metabase.util/varargs [[:inner 0]]
|
||||
metabase.util/with-us-locale [[:block 0]]
|
||||
metabuild-common.core/exit-when-finished-nonzero-on-exception [[:block 0]]
|
||||
metabuild-common.core/step [[:block 1]]
|
||||
metabuild-common.entrypoint/exit-when-finished-nonzero-on-exception [[:block 0]]
|
||||
metabuild-common.steps/step [[:block 1]]
|
||||
methodical.core/defmethod [[:inner 0]]
|
||||
methodical.core/defmulti [[:inner 0]]
|
||||
methodical.core/with-dispatcher [[:default]]
|
||||
methodical.core/with-method-table [[:default]]
|
||||
methodical.core/with-prefers [[:default]]
|
||||
methodical.interface/with-dispatcher [[:default]]
|
||||
methodical.interface/with-method-table [[:default]]
|
||||
methodical.interface/with-prefers [[:default]]
|
||||
methodical.macros/defmethod [[:inner 0]]
|
||||
methodical.macros/defmulti [[:inner 0]]
|
||||
saml20-clj.sp.response/validate-confirmation-datas [[:block 1]]
|
||||
toucan2.connection/with-transaction [[:block 1]]
|
||||
toucan2.core/build [[:block 0]]
|
||||
toucan2.core/compile [[:block 0]]
|
||||
toucan2.core/debug [[:block 0]]
|
||||
toucan2.core/define-after-insert [[:inner 0]]
|
||||
toucan2.core/define-after-select [[:inner 0]]
|
||||
toucan2.core/define-after-update [[:inner 0]]
|
||||
toucan2.core/define-before-delete [[:inner 0]]
|
||||
toucan2.core/define-before-insert [[:inner 0]]
|
||||
toucan2.core/define-before-select [[:inner 0]]
|
||||
toucan2.core/define-default-fields [[:inner 0]]
|
||||
toucan2.core/define-named-query [[:block 1]]
|
||||
toucan2.core/deftransforms [[:block 1]]
|
||||
toucan2.core/with-transaction [[:block 1]]
|
||||
toucan2.tools.after-insert/define-after-insert [[:inner 0]]
|
||||
toucan2.tools.after-select/define-after-select [[:inner 0]]
|
||||
toucan2.tools.after-update/define-after-update [[:inner 0]]
|
||||
toucan2.tools.after/define-after-impl [[:default]]
|
||||
toucan2.tools.before-delete/define-before-delete [[:inner 0]]
|
||||
toucan2.tools.before-insert/define-before-insert [[:inner 0]]
|
||||
toucan2.tools.before-select/define-before-select [[:inner 0]]
|
||||
toucan2.tools.compile/build [[:block 0]]
|
||||
toucan2.tools.compile/compile [[:block 0]]
|
||||
toucan2.tools.compile/resolved [[:block 0]]
|
||||
toucan2.tools.debug/debug [[:block 0]]
|
||||
toucan2.tools.default-fields/define-default-fields [[:inner 0]]
|
||||
toucan2.tools.named-query/define-named-query [[:block 1]]
|
||||
toucan2.tools.simple-out-transform/define-out-transform [[:inner 0]]
|
||||
toucan2.tools.transformed/deftransforms [[:block 1]]
|
||||
toucan2.tools.with-temp/with-temp [[:inner 0]]
|
||||
toucan2.util/try-with-error-context [[:inner 0]]}}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
afterAll
|
||||
ags
|
||||
altho
|
||||
Brite
|
||||
cancelation
|
||||
cancelations
|
||||
caracteres
|
||||
copyin
|
||||
datas
|
||||
edn
|
||||
eduction
|
||||
hasTable
|
||||
hasTables
|
||||
horizontalX
|
||||
horizontalY
|
||||
juxt
|
||||
methodd
|
||||
nd
|
||||
notIn
|
||||
optionA
|
||||
ot
|
||||
pass-thru
|
||||
requestor
|
||||
re-use
|
||||
re-used
|
||||
ser
|
||||
serieses
|
||||
sortings
|
||||
titel
|
||||
trs
|
||||
trsn
|
||||
tru
|
||||
trun
|
||||
unparseable
|
||||
withs
|
||||
24
.codespellrc
24
.codespellrc
@@ -1,24 +0,0 @@
|
||||
[codespell]
|
||||
skip =
|
||||
*.csv,
|
||||
*.edn,
|
||||
*.geojson,
|
||||
*.json,
|
||||
*.tsv,
|
||||
*.unit.spec.*,
|
||||
*.xml,
|
||||
docs/developers-guide/drivers/driver-tests.md,
|
||||
docs/developers-guide/e2e-tests.md,
|
||||
docs/questions/query-builder/expressions-list.md,
|
||||
docs/questions/visualizations/country-codes.md,
|
||||
e2e/test/scenarios/admin/i18n/content-translation/constants.ts,
|
||||
frontend/src/cljs,
|
||||
frontend/src/metabase/querying/expressions/tokenizer/lezer.js,
|
||||
frontend/src/metabase/query_builder/components/NativeQueryEditor/CodeMirrorEditor/language.ts,
|
||||
frontend/src/metabase/static-viz/constants/char-sizes.ts,
|
||||
frontend/src/metabase/visualizations/lib/mapping_codes.ts,
|
||||
frontend/*/mocks/*,
|
||||
modules/drivers/mongo/src/metabase/driver/mongo/operators.clj,
|
||||
src/metabase/analyze/classifiers/name.clj,
|
||||
src/metabase/util/currency.cljc,
|
||||
ignore-words = .codespellignore
|
||||
@@ -1,11 +1,11 @@
|
||||
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-key adv --refresh-keys --keyserver keyserver.ubuntu.com \
|
||||
&& 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'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.babel_cache
|
||||
docs/*
|
||||
target/*
|
||||
.circleci
|
||||
.cpcache
|
||||
|
||||
163
.env.example
163
.env.example
@@ -1,163 +0,0 @@
|
||||
# ==============================================================================
|
||||
# 0. METABASE DEVELOPMENT ENVIRONMENT
|
||||
# ==============================================================================
|
||||
#
|
||||
# Central configuration for local Metabase development.
|
||||
# Copy to .env and uncomment/modify values as needed.
|
||||
#
|
||||
# Usage:
|
||||
# op inject --in-file .env.example --out-file .env (populate using CLI)
|
||||
# cp .env.example .env (populate manually)
|
||||
#
|
||||
# Hint: Auto-load environment variables with mise or direnv:
|
||||
# mise - mise.toml: [settings] env_file = ".env" (https://mise.jdx.dev)
|
||||
# direnv - Create .envrc with: dotenv .env (https://direnv.net)
|
||||
#
|
||||
# Full Metabase environment configuration reference:
|
||||
# https://www.metabase.com/docs/latest/configuring-metabase/environment-variables
|
||||
#
|
||||
# ⚠️ Make sure the same var is not exported multiple times in different contexts.
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 1. GLOBAL CONTEXT
|
||||
# ==============================================================================
|
||||
#
|
||||
# Shared credentials and tokens used across multiple dev workflows.
|
||||
|
||||
# GitHub - For gh CLI, API access, and CI integrations
|
||||
# https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens
|
||||
# GITHUB_TOKEN=your-PAT
|
||||
|
||||
# AWS - For S3, Athena, Redshift, and other AWS services
|
||||
# https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html
|
||||
# AWS_ACCESS_KEY_ID=your-access-key
|
||||
# AWS_SECRET_ACCESS_KEY=your-secret-key
|
||||
# AWS_REGION=us-east-1
|
||||
|
||||
# Docker Hub - Avoid rate limits when pulling images
|
||||
# https://docs.docker.com/security/for-developers/access-tokens/
|
||||
# DOCKER_USERNAME=your-username
|
||||
# DOCKER_PASSWORD=your-access-token
|
||||
|
||||
# ==============================================================================
|
||||
# 2. LOCAL METABASE DEVELOPMENT
|
||||
# ==============================================================================
|
||||
#
|
||||
# Settings for running Metabase locally. Most have sensible defaults—only
|
||||
# configure what you need.
|
||||
|
||||
# 2.1 Application Database
|
||||
# ------------------------------------------------------------------------------
|
||||
# Where Metabase stores its own data (dashboards, users, settings, etc.).
|
||||
# https://www.metabase.com/docs/latest/installation-and-operation/configuring-application-database
|
||||
|
||||
# Option A: Individual connection parameters
|
||||
# MB_DB_TYPE="postgres"
|
||||
# MB_DB_HOST="localhost"
|
||||
# MB_DB_PORT=5432
|
||||
# MB_DB_DBNAME="metabase"
|
||||
# MB_DB_USER="<username>"
|
||||
# MB_DB_PASS="<password>"
|
||||
|
||||
# Option B: Single connection URI (overrides individual params)
|
||||
# MB_DB_CONNECTION_URI="jdbc:postgresql://localhost:5432/metabase?user=<username>&password=<password>"
|
||||
|
||||
# 2.2 Server & Integrations
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://www.metabase.com/docs/latest/configuring-metabase/customizing-jetty-webserver
|
||||
|
||||
# Jetty server (defaults: localhost:3000)
|
||||
# MB_JETTY_HOST="localhost"
|
||||
# MB_JETTY_PORT=3000
|
||||
|
||||
# SMTP for testing email notifications (use with maildev/maildev container)
|
||||
MB_EMAIL_SMTP_HOST="localhost"
|
||||
MB_EMAIL_SMTP_PORT=1025
|
||||
MB_EMAIL_SMTP_USERNAME="admin"
|
||||
MB_EMAIL_SMTP_PASSWORD="admin"
|
||||
|
||||
# 2.3 Feature Flags & Tokens
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Use staging server for license validation (avoids hitting production)
|
||||
METASTORE_DEV_SERVER_URL="https://token-check.staging.metabase.com"
|
||||
|
||||
# Metabase license token
|
||||
# Uncomment this line only when running local dev and NEVER when running Cypress
|
||||
# Tests need to start from zero, and set different tokens programmatically (on-demand)
|
||||
# MB_PREMIUM_EMBEDDING_TOKEN="op://Shared/Metabase License Dev Tokens/Licenses/MB_ALL_FEATURES_TOKEN"
|
||||
|
||||
# Skip embedding SDK build for faster frontend compilation
|
||||
# SKIP_EMBEDDING_SDK="true"
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 3. E2E / CYPRESS TESTING
|
||||
# ==============================================================================
|
||||
#
|
||||
# Configuration for running Cypress end-to-end tests locally.
|
||||
#
|
||||
# The whole environment is already orchestrated via deps.edn using :e2e alias.
|
||||
# Treat it as a source of truth for JVM flags and ENVs whether you decide to run
|
||||
# backend on your own, or using `bun run test-cypress` (local test runner).
|
||||
#
|
||||
# You don't _have to_ export any of these vars (except for the tokens below).
|
||||
# Things will still work.
|
||||
|
||||
# 3.1. E2E Application Database (H2 required)
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Whether to run the enterprise or the open-source Metabase version
|
||||
MB_EDITION="ee"
|
||||
|
||||
# Which file to use for the ephemeral testing application database?
|
||||
# MB_DB_FILE="./e2e/tmp/cypress-test.db"
|
||||
|
||||
# Where does Cypress go to look for the application? (defaults: localhost:4000)
|
||||
# MB_JETTY_HOST="localhost"
|
||||
# MB_JETTY_PORT=4000
|
||||
|
||||
# Frontend dev server port (for hot reload). Must match the port used by `yarn build-hot`.
|
||||
# MB_FRONTEND_DEV_PORT=8080
|
||||
|
||||
# 3.2 E2E Local Runner Setup
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Whether to run true e2e test suite, or SDK component tests (e2e | component)
|
||||
CYPRESS_TESTING_TYPE="e2e"
|
||||
|
||||
# Whether to start Cypress in an interactive (open) mode with full GUI
|
||||
CYPRESS_GUI="true"
|
||||
|
||||
# Whether to generate application database dumps (colloquially "snapshots")
|
||||
# Keep this enabled unless you're experimenting, or know what you're doing.
|
||||
GENERATE_SNAPSHOTS="true"
|
||||
|
||||
# E2E tests run the backend from source by default.
|
||||
# To run tests against a JAR instead, set the JAR_PATH variable.
|
||||
#
|
||||
# Hint: build the JAR with ./bin/build.sh
|
||||
# JAR_PATH="target/uberjar/metabase.jar"
|
||||
|
||||
# 3.3 E2E Tokens
|
||||
# ------------------------------------------------------------------------------
|
||||
#
|
||||
# These make sense for Metabase core devs only
|
||||
# op inject --in-file .env.example --out-file .env
|
||||
|
||||
CYPRESS_MB_ALL_FEATURES_TOKEN="op://Shared/Metabase License Dev Tokens/Licenses/MB_ALL_FEATURES_TOKEN"
|
||||
CYPRESS_MB_STARTER_CLOUD_TOKEN="op://Shared/Metabase License Dev Tokens/Licenses/MB_STARTER_CLOUD_TOKEN"
|
||||
CYPRESS_MB_PRO_CLOUD_TOKEN="op://Shared/Metabase License Dev Tokens/Licenses/MB_PRO_CLOUD_TOKEN"
|
||||
CYPRESS_MB_PRO_SELF_HOSTED_TOKEN="op://Shared/Metabase License Dev Tokens/Licenses/MB_PRO_SELF_HOSTED_TOKEN"
|
||||
|
||||
# 3.4 E2E Misc.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# To make Node.js match the instance tz
|
||||
TZ="US/Pacific"
|
||||
|
||||
# Export this environment variable when running tests on Apple Silicon.
|
||||
# DO NOT USE Docker Desktop. Use Orbstack instead. Metabase will fail to connect
|
||||
# to the dockerized Mongo QA database otherwise.
|
||||
EXPERIMENTAL_DOCKER_DESKTOP_FORCE_QEMU=1
|
||||
4
.eslintignore
Normal file
4
.eslintignore
Normal file
@@ -0,0 +1,4 @@
|
||||
frontend/src/cljs
|
||||
frontend/src/cljs_release
|
||||
e2e/support/cypress_sample_database.js
|
||||
e2e/support/cypress_sample_instance_data.js
|
||||
351
.eslintrc.js
Normal file
351
.eslintrc.js
Normal file
@@ -0,0 +1,351 @@
|
||||
/* 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",
|
||||
{ additionalHooks: "(useSafeAsyncFunction)" },
|
||||
],
|
||||
"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",
|
||||
"describeWithSnowplow",
|
||||
],
|
||||
},
|
||||
],
|
||||
complexity: ["error", { max: 55 }],
|
||||
...(shouldLintCssModules
|
||||
? {
|
||||
"postcss-modules/no-undef-class": "error",
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
globals: {
|
||||
before: true,
|
||||
cy: true,
|
||||
Cypress: true,
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
commonjs: true,
|
||||
jest: true,
|
||||
"jest/globals": true,
|
||||
},
|
||||
parser: "babel-eslint",
|
||||
plugins,
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react/jsx-runtime",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings",
|
||||
"plugin:import/typescript",
|
||||
"plugin:depend/recommended",
|
||||
"plugin:storybook/recommended",
|
||||
"plugin:i18next/recommended",
|
||||
],
|
||||
settings: {
|
||||
"import/internal-regex": "^metabase/|^metabase-lib/",
|
||||
"import/resolver": {
|
||||
webpack: {
|
||||
config: path.resolve(__dirname, "./rspack.main.config.js"),
|
||||
typescript: true,
|
||||
},
|
||||
},
|
||||
"import/ignore": ["\\.css$"],
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
"postcss-modules": {
|
||||
baseDir: "./frontend/src",
|
||||
},
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
legacyDecorators: true,
|
||||
},
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.js", "*.jsx", "*.ts", "*.tsx"],
|
||||
rules: {
|
||||
"jtag-missing-key": "error",
|
||||
"no-unconditional-metabase-links-render": "error",
|
||||
"no-color-literals": "error",
|
||||
"no-literal-metabase-strings": "error",
|
||||
"depend/ban-dependencies": [
|
||||
"error",
|
||||
{
|
||||
allowed: [
|
||||
"underscore",
|
||||
"moment",
|
||||
"lodash.orderby",
|
||||
"lodash.debounce",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
"*.unit.spec.*",
|
||||
"frontend/src/metabase/admin/**/*",
|
||||
"frontend/src/metabase/setup/**/*",
|
||||
"enterprise/frontend/src/metabase-enterprise/whitelabel/**/*",
|
||||
"frontend/lint/**/*",
|
||||
"*.stories.*",
|
||||
"**/.storybook/*",
|
||||
"stories-data.*",
|
||||
"e2e/**/*",
|
||||
"**/tests/*",
|
||||
"release/**/*",
|
||||
"rspack.config.js",
|
||||
"rspack.main.config.js",
|
||||
"rspack.embedding-sdk-package.config.js",
|
||||
"rspack.embedding-sdk-bundle.config.js",
|
||||
],
|
||||
rules: {
|
||||
"no-color-literals": "off",
|
||||
"no-unconditional-metabase-links-render": "off",
|
||||
"no-literal-metabase-strings": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
"*.unit.spec.*",
|
||||
"frontend/lint/**/*",
|
||||
"*.stories.*",
|
||||
"stories-data.*",
|
||||
"e2e/**/*",
|
||||
"**/tests/*",
|
||||
"release/**/*",
|
||||
"rspack.main.config.js",
|
||||
],
|
||||
rules: {
|
||||
"i18next/no-literal-string": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
extends: ["plugin:@typescript-eslint/recommended"],
|
||||
files: ["*.ts", "*.tsx"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint"],
|
||||
rules: {
|
||||
"prefer-rest-params": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-inferrable-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-this-alias": "off",
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"error",
|
||||
{
|
||||
fixStyle: "inline-type-imports",
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-import-type-side-effects": "error",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
argsIgnorePattern: "^_.+$",
|
||||
varsIgnorePattern: "^_.+$",
|
||||
ignoreRestSiblings: true,
|
||||
destructuredArrayIgnorePattern: "^_.+$",
|
||||
},
|
||||
],
|
||||
// This was introduced in 6.0.0
|
||||
"@typescript-eslint/no-unsafe-declaration-merging": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
extends: [
|
||||
"plugin:jest/recommended",
|
||||
"plugin:jest-dom/recommended",
|
||||
"plugin:testing-library/react",
|
||||
"plugin:jest-formatting/recommended",
|
||||
],
|
||||
plugins: ["jest", "jest-dom", "testing-library", "jest-formatting"],
|
||||
files: [
|
||||
"*.unit.spec.ts",
|
||||
"*.unit.spec.tsx",
|
||||
"*.unit.spec.js",
|
||||
"*.unit.spec.jsx",
|
||||
],
|
||||
rules: {
|
||||
"jest/valid-title": ["error", { ignoreTypeOfDescribeName: true }],
|
||||
"jest/expect-expect": [
|
||||
"error",
|
||||
{
|
||||
assertFunctionNames: ["expect*", "assert*"],
|
||||
additionalTestBlockFunctions: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Enable jest formatting for cypress tests too, the plugin logic just works
|
||||
extends: ["plugin:jest-formatting/recommended"],
|
||||
files: ["*.cy.spec.ts", "*.cy.spec.js"],
|
||||
},
|
||||
{
|
||||
files: ["frontend/src/**/*"],
|
||||
rules: {
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
selector: "Literal[value=/mb-base-color-/]",
|
||||
message:
|
||||
"You may not use base colors in the application, use semantic colors instead. (see colors.module.css)",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["frontend/src/metabase/**/*"],
|
||||
rules: {
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
selector: "Literal[value=/mb-base-color-/]",
|
||||
message:
|
||||
"You may not use base colors in the application, use semantic colors instead. (see colors.module.css)",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["frontend/src/metabase/query_builder/**/*"],
|
||||
rules: {
|
||||
"import/no-cycle": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["docs/**/snippets/**/*.{ts,tsx,js,jsx}"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"import/no-commonjs": "off",
|
||||
"import/no-default-export": "off",
|
||||
"import/order": "off",
|
||||
"import/no-unresolved": "off",
|
||||
"no-color-literals": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["frontend/build/**/*.js"],
|
||||
rules: {
|
||||
"import/no-commonjs": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
7
.gitattributes
vendored
7
.gitattributes
vendored
@@ -1,7 +0,0 @@
|
||||
# Custom merge driver for YAML migration files
|
||||
# Treats each changeset as an atomic unit to avoid conflicts within changesets
|
||||
# Execute this in your shell to enable custom merge driver:
|
||||
# echo '\n[include]\npath=../.gitconfig' >> .git/config
|
||||
#
|
||||
# TODO (Bryan 12/12/25) -- reenable this when we can completely verify correctness
|
||||
# resources/migrations/*.yaml merge=yaml-migrations
|
||||
@@ -1,5 +0,0 @@
|
||||
# execute this in your shell to enable custom merge driver:
|
||||
# echo '\n[include]\npath=../.gitconfig' >> .git/config
|
||||
[merge "yaml-migrations"]
|
||||
name = YAML migration changeset-aware merge
|
||||
driver = bin/mage -merge-yaml-migrations %O %A %B %L %P
|
||||
23
.github/ISSUE_TEMPLATE/epic.md
vendored
Normal file
23
.github/ISSUE_TEMPLATE/epic.md
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: Epic (Internal Use)
|
||||
about: This issue is used to track a feature/project that may span several days
|
||||
to weeks to implement. This is used by the core Metabase team to project plan
|
||||
and manage work.
|
||||
title: "[Epic] Title"
|
||||
labels: ".Epic"
|
||||
---
|
||||
|
||||
### Links
|
||||
|
||||
- [product doc](LINK_TO_PRODUCT_DOC)
|
||||
- [technical design doc](LINK_TO_TECHNICAL_DESIGN_DOC)
|
||||
- [testing plan](LINK_TO_TESTING_PLAN)
|
||||
- feature branch: `BRANCH_NAME`
|
||||
- issue links: _related issues, if any_
|
||||
|
||||
### Implementation Plan
|
||||
|
||||
```[tasklist]
|
||||
#### Milestone 1
|
||||
- [ ] _Add tasks here_
|
||||
```
|
||||
13
.github/ISSUE_TEMPLATE/feature_request.md
vendored
13
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -7,13 +7,16 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**What problem will this feature request solve?**
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like.**
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**How does this feature request impact you?**
|
||||
For example, is it a minor inconvenience, or does it block important work?
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional information**
|
||||
**How important is this feature to you?**
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
14
.github/ISSUE_TEMPLATE/task.md
vendored
Normal file
14
.github/ISSUE_TEMPLATE/task.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: Task (Internal Use)
|
||||
about: This issue is used to track a smaller feature/task that does not require
|
||||
being broken down to milestones. This is used by the core Metabase team to
|
||||
project plan and manage work.
|
||||
labels: ".Task"
|
||||
|
||||
---
|
||||
|
||||
**Context**
|
||||
|
||||
|
||||
- issue links: _related issues if any_
|
||||
|
||||
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -25,4 +25,3 @@ _Upload a demo video or before/after screenshots if sensible or remove the secti
|
||||
### Checklist
|
||||
|
||||
- [ ] Tests have been added/updated to cover changes in this PR
|
||||
- [ ] If adding new Loki tests: they pass [stress testing](https://github.com/metabase/metabase/actions/workflows/loki-stress-test-flake-fix.yml)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,6 +17,8 @@ runs:
|
||||
|
||||
- name: Prepare backend environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
with:
|
||||
m2-cache-key: cross-branch-migrations
|
||||
|
||||
- name: Run backend on base branch
|
||||
run: |
|
||||
|
||||
20
.github/actions/fetch-artifact/action.yml
vendored
20
.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,20 +25,15 @@ runs:
|
||||
script: | # js
|
||||
const fs = require('fs');
|
||||
|
||||
const isRelease = "${{ inputs.release }}" === "true";
|
||||
const prefix = isRelease ? "metabase-release" : "metabase";
|
||||
const artifactName = `${prefix}-${{ inputs.edition }}-${{ inputs.commit }}-uberjar`;
|
||||
console.log(`Looking for artifact: ${artifactName}`);
|
||||
|
||||
const artifacts = await github.rest.actions.listArtifactsForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: artifactName,
|
||||
name: "${{ inputs.name }}",
|
||||
per_page: 1,
|
||||
});
|
||||
|
||||
if (!artifacts.data?.artifacts?.[0]?.id) {
|
||||
throw new Error(`No artifacts found for ${artifactName}`);
|
||||
throw new Error(`No artifacts found for ${{ inputs.name }}`);
|
||||
}
|
||||
|
||||
const artifact_id = artifacts.data.artifacts[0].id;
|
||||
|
||||
37
.github/actions/get-cache-keys/action.yml
vendored
37
.github/actions/get-cache-keys/action.yml
vendored
@@ -1,37 +0,0 @@
|
||||
name: Get cache keys
|
||||
description: |
|
||||
Get deterministic cache keys for Maven and Node.js caches based on the centrally controlled cache prefix,
|
||||
and the suffix based on the runner OS and the current week of the year.
|
||||
|
||||
outputs:
|
||||
m2-cache-key:
|
||||
description: "The cache key for the Maven cache."
|
||||
value: ${{ steps.keys-factory.outputs.m2-cache-key }}
|
||||
node-cache-key:
|
||||
description: "The cache key for the Node.js cache."
|
||||
value: ${{ steps.keys-factory.outputs.node-cache-key }}
|
||||
eslint-cache-key:
|
||||
description: "The cache key for Eslint cache"
|
||||
value: ${{ steps.keys-factory.outputs.eslint-cache-key }}
|
||||
cypress-cache-key:
|
||||
description: "The cache key for Cypress cache"
|
||||
value: ${{ steps.keys-factory.outputs.cypress-cache-key }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Generate and output cache keys
|
||||
id: keys-factory
|
||||
run: |
|
||||
M2_CACHE_PREFIX='M2'
|
||||
NODE_CACHE_PREFIX='node-modules'
|
||||
ESLINT_CACHE_PREFIX='eslint'
|
||||
CYPRESS_CACHE_PREFIX='cypress'
|
||||
WEEK_OF_YEAR=$(/bin/date "+%G-%V")
|
||||
CACHE_SUFFIX="${{ runner.os }}-$WEEK_OF_YEAR"
|
||||
|
||||
echo "m2-cache-key=$M2_CACHE_PREFIX-$CACHE_SUFFIX" >> $GITHUB_OUTPUT
|
||||
echo "node-cache-key=$NODE_CACHE_PREFIX-$CACHE_SUFFIX" >> $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
|
||||
19
.github/actions/maximize-disk-space/action.yml
vendored
19
.github/actions/maximize-disk-space/action.yml
vendored
@@ -1,19 +0,0 @@
|
||||
name: Maximize disk space
|
||||
description: Removes unused stuff from GH runner
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Maximize build space
|
||||
shell: bash
|
||||
run: |
|
||||
echo "-- Dotnet"
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
echo "-- Android"
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
echo "-- Haskell"
|
||||
sudo rm -rf /opt/ghc
|
||||
echo "-- CodeQL"
|
||||
sudo rm -rf /opt/hostedtoolcache/CodeQL
|
||||
|
||||
df -h
|
||||
42
.github/actions/prepare-backend/action.yml
vendored
42
.github/actions/prepare-backend/action.yml
vendored
@@ -1,13 +1,18 @@
|
||||
name: Prepare back-end environment
|
||||
description: |
|
||||
Prepare the backend. Installs jdk, Clojure, and fetches the cache for m2.
|
||||
Prepare the backend. Installs jdk, Clojure, and fetches the cache for m2. The CI config fetchs `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'
|
||||
m2-cache-key:
|
||||
description: 'Key to cache M2 packages from Maven Central'
|
||||
required: true
|
||||
default: 'm2'
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
@@ -16,29 +21,28 @@ 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: |
|
||||
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
|
||||
- name: Fetch CI config
|
||||
shell: bash
|
||||
run: |
|
||||
curl -o ci-test-config.json https://raw.githubusercontent.com/metabase/metabase/refs/heads/master/ci-test-config.json
|
||||
- name: Check to see if dependencies should be cached
|
||||
if: ${{ contains(github.event.head_commit.message, '[ci nocache]') }}
|
||||
run: echo "Commit message includes [ci nocache]; dependencies will NOT be cached"
|
||||
shell: bash
|
||||
- name: Get M2 cache
|
||||
uses: actions/cache@v4
|
||||
if: ${{ !contains(github.event.head_commit.message, '[ci nocache]') }}
|
||||
with:
|
||||
path: |
|
||||
~/.m2
|
||||
~/.gitlibs
|
||||
key: ${{ steps.get-cache-keys.outputs.m2-cache-key }}
|
||||
key: ${{ runner.os }}-${{ inputs.m2-cache-key }}-${{ hashFiles('**/deps.edn') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ inputs.m2-cache-key }}-
|
||||
|
||||
31
.github/actions/prepare-cypress/action.yml
vendored
31
.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,27 +15,28 @@ 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
|
||||
uses: ./.github/actions/get-cache-keys
|
||||
id: get-cache-keys
|
||||
|
||||
- name: Check to see if dependencies should be cached
|
||||
if: ${{ contains(github.event.head_commit.message, '[ci nocache]') }}
|
||||
run: echo "Commit message includes [ci nocache]; dependencies will NOT be cached"
|
||||
shell: bash
|
||||
- name: Get Cypress cache
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache@v4
|
||||
if: ${{ !contains(github.event.head_commit.message, '[ci nocache]') }}
|
||||
with:
|
||||
path: ~/.cache/Cypress
|
||||
key: ${{ steps.get-cache-keys.outputs.cypress-cache-key }}
|
||||
|
||||
key: ${{ runner.os }}-Cypress-${{ hashFiles('**/yarn.lock') }}
|
||||
- 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 }}
|
||||
|
||||
63
.github/actions/prepare-frontend/action.yml
vendored
63
.github/actions/prepare-frontend/action.yml
vendored
@@ -1,13 +1,8 @@
|
||||
name: Prepare front-end environment
|
||||
description: "Prepare the front-end environment for the CI build"
|
||||
inputs:
|
||||
node-version:
|
||||
description: "The version of Node.js to use, overriding .nvmrc"
|
||||
required: false
|
||||
apply-patches:
|
||||
description: "Whether to patch frontend dependencies with patch-package."
|
||||
required: true
|
||||
default: "true"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
@@ -17,48 +12,26 @@ runs:
|
||||
node-version: ${{ inputs.node-version }} # this overrides, if present
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: Install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: "1.3.7"
|
||||
token: ${{ github.token }}
|
||||
|
||||
- name: Get cache keys
|
||||
uses: ./.github/actions/get-cache-keys
|
||||
id: get-cache-keys
|
||||
|
||||
- name: Restore M2 cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
~/.m2
|
||||
~/.gitlibs
|
||||
key: ${{ steps.get-cache-keys.outputs.m2-cache-key }}
|
||||
|
||||
- name: Get bun cache directory
|
||||
id: bun-cache
|
||||
- name: Check to see if dependencies should be cached
|
||||
if: ${{ contains(github.event.head_commit.message, '[ci nocache]') }}
|
||||
run: |
|
||||
BUN_CACHE_DIR=$(bun pm cache)
|
||||
echo "Bun cache directory: $BUN_CACHE_DIR"
|
||||
echo "dir=$BUN_CACHE_DIR" >> $GITHUB_OUTPUT
|
||||
echo "Commit message includes [ci nocache]; dependencies will NOT be cached"
|
||||
yarn cache clean
|
||||
shell: bash
|
||||
|
||||
- name: Restore node_modules cache
|
||||
uses: actions/cache/restore@v4
|
||||
id: node-modules-cache
|
||||
- name: Get M2 cache
|
||||
uses: actions/cache@v4
|
||||
if: ${{ !contains(github.event.head_commit.message, '[ci nocache]') }}
|
||||
with:
|
||||
path: |
|
||||
${{ steps.bun-cache.outputs.dir }}
|
||||
node_modules
|
||||
key: ${{ steps.get-cache-keys.outputs.node-cache-key }}
|
||||
path: ~/.m2
|
||||
key: ${{ runner.os }}-cljs-${{ hashFiles('**/shadow-cljs.edn') }}
|
||||
|
||||
# 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
|
||||
shell: bash
|
||||
- name: Patch frontend dependencies
|
||||
if: inputs.apply-patches == 'true'
|
||||
run: bun run patch-package
|
||||
- name: Get node_modules cache
|
||||
uses: actions/cache@v4
|
||||
id: node-modules-cache
|
||||
if: ${{ !contains(github.event.head_commit.message, '[ci nocache]') }}
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-node-modules-${{ hashFiles('**/yarn.lock', '**/patches/*.patch') }}
|
||||
|
||||
- run: yarn install --frozen-lockfile --prefer-offline
|
||||
shell: bash
|
||||
|
||||
@@ -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
|
||||
|
||||
33
.github/actions/run-embedding-sdk-storybook/action.yml
vendored
Normal file
33
.github/actions/run-embedding-sdk-storybook/action.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Run Embedding SDK Storybook
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Prepare frontend environment
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
with:
|
||||
m2-cache-key: "cljs"
|
||||
|
||||
- name: Compile CLJS
|
||||
run: yarn build-pure:cljs
|
||||
shell: bash
|
||||
|
||||
- name: Build Storybook
|
||||
run: yarn build-storybook-embedding-sdk
|
||||
shell: bash
|
||||
|
||||
- name: Serve Storybook
|
||||
run: yarn http-server storybook-static -p 6006 &
|
||||
shell: bash
|
||||
|
||||
- name: Wait for Storybook to become available
|
||||
run: |
|
||||
echo "Waiting for Storybook to be served..."
|
||||
while ! curl --silent --fail http://localhost:6006; do
|
||||
sleep 5
|
||||
echo "Retrying..."
|
||||
done
|
||||
echo "Storybook is being served!"
|
||||
shell: bash
|
||||
71
.github/actions/sdk-bundle-size-check/action.yml
vendored
71
.github/actions/sdk-bundle-size-check/action.yml
vendored
@@ -1,71 +0,0 @@
|
||||
name: Embedding SDK Bundle Size Check
|
||||
description: |
|
||||
Downloads the current branch's uberjar and the base ref's uberjar,
|
||||
extracts embedding-sdk.js from both, 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: Download current uberjar
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: metabase-${{ inputs.edition }}-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
|
||||
path: temp/current-uberjar
|
||||
- name: Extract embedding-sdk.js from current uberjar
|
||||
shell: bash
|
||||
run: unzip -j temp/current-uberjar/target/uberjar/metabase.jar frontend_client/app/embedding-sdk.js -d temp/current-bundle
|
||||
- name: Download base-ref uberjar
|
||||
uses: ./.github/actions/fetch-artifact
|
||||
with:
|
||||
commit: ${{ inputs.base-ref-commit }}
|
||||
edition: ${{ inputs.edition }}
|
||||
extract-path: temp/base-ref-uberjar
|
||||
- name: Extract embedding-sdk.js from base-ref uberjar
|
||||
shell: bash
|
||||
run: unzip -j temp/base-ref-uberjar/metabase.jar frontend_client/app/embedding-sdk.js -d temp/base-ref-bundle
|
||||
- name: Compare sizes and set output
|
||||
id: compare
|
||||
shell: bash
|
||||
run: |
|
||||
current_size=$(wc -c < temp/current-bundle/embedding-sdk.js) || { echo "::error::Failed to read current bundle"; exit 1; }
|
||||
base_ref_size=$(wc -c < temp/base-ref-bundle/embedding-sdk.js) || { echo "::error::Failed to read base-ref bundle"; exit 1; }
|
||||
[ "$current_size" -gt 0 ] && [ "$base_ref_size" -gt 0 ] || { echo "::error::Bundle file is empty"; exit 1; }
|
||||
|
||||
diff=$((current_size - base_ref_size))
|
||||
percent=$(( diff * 100 / base_ref_size ))
|
||||
|
||||
echo "Current size: $current_size bytes"
|
||||
echo "Base ref size: $base_ref_size bytes"
|
||||
echo "Difference: $diff bytes ($percent%)"
|
||||
|
||||
threshold="${{ inputs.threshold-percent }}"
|
||||
if [ "$percent" -gt "$threshold" ]; then
|
||||
status="increased"
|
||||
elif [ "$percent" -lt "$(( -threshold ))" ]; then
|
||||
status="decreased"
|
||||
else
|
||||
status="stable"
|
||||
fi
|
||||
|
||||
echo "status=$status" >> $GITHUB_OUTPUT
|
||||
echo "size_change_percent=$percent" >> $GITHUB_OUTPUT
|
||||
17
.github/actions/setup-python-runner/action.yml
vendored
17
.github/actions/setup-python-runner/action.yml
vendored
@@ -4,11 +4,24 @@ inputs:
|
||||
metabase-automation-token:
|
||||
description: Token for accessing the python-runner repository
|
||||
required: true
|
||||
python-runner-version:
|
||||
description: Git tag or commit of the python-runner repository to checkout
|
||||
required: false
|
||||
default: 'v0.0.1'
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Setup python-runner
|
||||
run: ./.github/scripts/setup-python-runner.sh
|
||||
- name: Clone and setup python-runner
|
||||
run: |
|
||||
echo "Cloning python-runner repository..."
|
||||
git clone --depth 1 --branch ${{ inputs.python-runner-version }} \
|
||||
https://${{ inputs.metabase-automation-token }}@github.com/metabase/python-runner-container.git python-runner
|
||||
|
||||
echo "Starting python-runner..."
|
||||
cd python-runner
|
||||
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 ..
|
||||
shell: bash
|
||||
env:
|
||||
METABASE_AUTOMATION_USER_TOKEN: ${{ inputs.metabase-automation-token }}
|
||||
|
||||
38
.github/actions/test-driver/action.yml
vendored
38
.github/actions/test-driver/action.yml
vendored
@@ -9,47 +9,29 @@ inputs:
|
||||
build-static-viz:
|
||||
required: true
|
||||
default: false
|
||||
logs-artifact-suffix:
|
||||
required: false
|
||||
description: Suffix to append to the test logs artifact name to avoid conflicts in matrix jobs
|
||||
edition:
|
||||
required: false
|
||||
default: 'ee'
|
||||
description: Edition to test (oss or ee)
|
||||
|
||||
outputs:
|
||||
test-outcome:
|
||||
description: "The outcome of the test run (success, failure, cancelled, skipped)"
|
||||
value: ${{ steps.run-test.outcome }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
with:
|
||||
m2-cache-key: driver
|
||||
- name: Prepare front-end environment
|
||||
if: ${{ inputs.build-static-viz == 'true' }}
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
- name: Build static viz frontend
|
||||
if: ${{ inputs.build-static-viz == 'true' }}
|
||||
run: bun run build-static-viz
|
||||
- name: Build static viz frontend
|
||||
run: yarn build-static-viz
|
||||
shell: bash
|
||||
if: ${{ inputs.build-static-viz == 'true' }}
|
||||
env:
|
||||
MB_EDITION: ee
|
||||
- name: Test database driver
|
||||
run: clojure -X:dev:ci:${{ inputs.edition }}:${{ inputs.edition }}-dev:drivers:drivers-dev:test ${{ inputs.test-args }}
|
||||
run: clojure -X:dev:ci:ee:ee-dev:drivers:drivers-dev:test ${{ inputs.test-args }}
|
||||
shell: bash
|
||||
id: run-test
|
||||
# Trunk Quarantine controls the final state of the run in the CI!
|
||||
# Even if this step fails we let it pass, and then `trunk-io/analytics-uploader` determines
|
||||
# whether to fail or to pass the whole job, based on the quarantine list.
|
||||
continue-on-error: true
|
||||
- name: Publish Test Report (JUnit)
|
||||
uses: dorny/test-reporter@v1
|
||||
# As per the documentation: https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#steps-context
|
||||
# test.outcome gives us access to the result before `continue-on-error` overrides it and sets it to 'success'.
|
||||
# We want test reports even if the test step gets cancelled (e.g. times out), hence we match anything that is not a 'success'.
|
||||
if: ${{ steps.run-test.outcome != 'success' }}
|
||||
if: failure()
|
||||
with:
|
||||
path: "target/junit/**/*_test.xml"
|
||||
name: JUnit Test Report ${{ inputs.junit-name }}
|
||||
@@ -58,11 +40,9 @@ runs:
|
||||
list-tests: failed
|
||||
fail-on-error: false
|
||||
- name: Upload Logs on Failure
|
||||
if: always() && steps.run-test.outcome != 'success'
|
||||
if: always() && !success()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ format('test-logs-{0}{1}', github.job, inputs.logs-artifact-suffix && format('-{0}', inputs.logs-artifact-suffix) || '') }}
|
||||
name: test-logs-${{ github.job }}
|
||||
path: logs/test-log.json
|
||||
retention-days: 1
|
||||
# Failing to upload the artifact should never affect the outcome of the whole test run.
|
||||
continue-on-error: true
|
||||
|
||||
41
.github/actions/update-cache/action.yml
vendored
41
.github/actions/update-cache/action.yml
vendored
@@ -1,41 +0,0 @@
|
||||
name: Update cache
|
||||
description: |
|
||||
Update a GitHub Actions cache based on a provided path and key.
|
||||
If a cache entry already exists for the given key, it is first removed.
|
||||
This ensures immutability of cache entries, and prevents runtime errors.
|
||||
|
||||
inputs:
|
||||
path:
|
||||
description: "The path(s) to cache, separated by newlines."
|
||||
required: true
|
||||
key:
|
||||
description: "The cache key to use."
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Check if the cache entry exists
|
||||
id: cache-lookup
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
key: ${{ inputs.key }}
|
||||
lookup-only: true # Important bit: the action will not fetch anything, just check for the cache existence.
|
||||
- name: Delete the cache entry if it exists
|
||||
if: ${{ steps.cache-lookup.outputs.cache-hit == 'true' }}
|
||||
run: |
|
||||
echo "Cache entry found for key '${{ inputs.key }}'. Deleting it to ensure immutability."
|
||||
gh api \
|
||||
--method DELETE \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
/repos/${{ github.repository }}/actions/caches?key=${{ inputs.key }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
shell: bash
|
||||
- name: Save the new cache entry
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
key: ${{ inputs.key }}
|
||||
10
.github/actions/upload-test-results/action.yml
vendored
10
.github/actions/upload-test-results/action.yml
vendored
@@ -20,9 +20,7 @@ inputs:
|
||||
required: true
|
||||
variant:
|
||||
required: false
|
||||
default: ""
|
||||
previous-step-outcome:
|
||||
required: true
|
||||
default: ''
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
@@ -50,13 +48,11 @@ runs:
|
||||
aws s3 cp ${OUTPUT_FILE}.zip s3://$BUCKET/$DATE/$GITHUB_RUN_ID/$GITHUB_RUN_ATTEMPT/
|
||||
|
||||
- name: Upload results to Trunk
|
||||
if: always() # Important! This step must always run because it controls job's exit code.
|
||||
if: ${{ always() && !(github.event_name == 'pull_request' && github.event.pull_request.draft == true) }}
|
||||
uses: trunk-io/analytics-uploader@main
|
||||
with:
|
||||
junit-paths: ${{ inputs.input-path }}
|
||||
variant: ${{ inputs.variant || inputs.output-name }}
|
||||
org-slug: metabase
|
||||
dry-run: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == true }} # quarantine, but do not upload results
|
||||
token: ${{ inputs.trunk-api-token }}
|
||||
use-cache: true
|
||||
previous-step-outcome: ${{ inputs.previous-step-outcome }}
|
||||
continue-on-error: true
|
||||
|
||||
2
.github/codeql/codeql-config.yml
vendored
2
.github/codeql/codeql-config.yml
vendored
@@ -1,5 +1,3 @@
|
||||
version: 1
|
||||
|
||||
name: "Metabase CodeQL config"
|
||||
|
||||
paths-ignore:
|
||||
|
||||
75
.github/copilot-instructions.md
vendored
75
.github/copilot-instructions.md
vendored
@@ -1,55 +1,52 @@
|
||||
# Metabase Developer Assistant Instructions
|
||||
|
||||
For detailed coding standards and conventions, see `CLAUDE.md` and the skills in `.claude/skills/`.
|
||||
## Frontend Development
|
||||
|
||||
## Code Review Standards
|
||||
### Repository Structure
|
||||
|
||||
Review focus areas: Security → Performance → Testing → Documentation (in priority order)
|
||||
```
|
||||
frontend/src/
|
||||
├── metabase/ # Main application components and pages
|
||||
├── metabase-lib/ # Query building and data modeling utilities
|
||||
├── metabase-types/ # TypeScript type definitions
|
||||
└── metabase/ui/ # Design system components
|
||||
|
||||
When reviewing code, use these emoji prefixes to categorize feedback:
|
||||
- 🔒 Security concerns
|
||||
- ⚡ Performance opportunities
|
||||
- 🧹 Cleanup needs
|
||||
- 📚 Documentation gaps
|
||||
- 🚨 Blocking issues
|
||||
- 💭 Clarification questions
|
||||
e2e/ # Cypress end-to-end tests
|
||||
frontend/test/ # Jest unit tests
|
||||
```
|
||||
|
||||
### Security Critical Issues
|
||||
### Technology Stack
|
||||
|
||||
- Check for hardcoded secrets, API keys, or credentials
|
||||
- Look for SQL injection and XSS vulnerabilities
|
||||
- Verify proper input validation and sanitization
|
||||
- Review authentication and authorization logic
|
||||
- Ensure enterprise features use the plugin system (no enterprise code in OSS)
|
||||
- **Framework**: React 18 with TypeScript
|
||||
- **State**: Redux Toolkit
|
||||
- **Styling**: CSS Modules (preferred) > Emotion styled-components
|
||||
- **UI**: `metabase/ui` components built with Mantine v8
|
||||
- **Build**: Rspack (primary), Webpack (legacy)
|
||||
- **Testing**: Jest + React Testing Library, Cypress
|
||||
|
||||
### Performance Red Flags
|
||||
### Coding Standards
|
||||
|
||||
- Identify N+1 database query problems
|
||||
- Spot inefficient loops and algorithmic issues
|
||||
- Check for memory leaks and resource cleanup
|
||||
- Review caching opportunities for expensive operations
|
||||
#### Component Preferences
|
||||
|
||||
### Code Quality Essentials
|
||||
- Prefer `metabase/ui`components over `metabase/common/components`
|
||||
- Use `.tsx` for components, `.ts` for utilities
|
||||
|
||||
- Functions should be focused and appropriately sized
|
||||
- Use clear, descriptive naming conventions
|
||||
- Ensure proper error handling throughout
|
||||
- Remove dead code and unused imports
|
||||
#### Styling
|
||||
|
||||
### Review Style
|
||||
ALWAYS prefer Mantine style props,then CSS modules. DO NOT suggest styled components, they are deprecated.
|
||||
|
||||
- Only flag actual issues worth mentioning
|
||||
- Do not post "looks good" comments or congratulate following conventions
|
||||
- Be specific and actionable in feedback
|
||||
- Explain the "why" behind recommendations
|
||||
- Ask clarifying questions when code intent is unclear
|
||||
#### TypeScript Migration
|
||||
|
||||
### Review Format
|
||||
When heavily editing `.js`/`.jsx` files, create a separate PR to convert to TypeScript first, then implement changes.
|
||||
|
||||
- Condense feedback into as few comments as possible
|
||||
- Prefer a single summary comment over dozens of inline comments
|
||||
- Only use inline comments for critical issues that require immediate attention
|
||||
- For non-critical issues, link to the specific line from the summary instead
|
||||
- Delete all stale comments and summaries on each new push or CI run
|
||||
#### Enterprise Features
|
||||
|
||||
Language-specific rules are in `.github/instructions/*.instructions.md`.
|
||||
Enterprise functionality MUST use the plugin system. It is very important to not expose enterprise code in the OSS version.
|
||||
|
||||
#### Testing Requirements
|
||||
|
||||
All PRs should include tests. Prefer Unit tests over E2E tests.
|
||||
|
||||
#### Localization
|
||||
|
||||
All user-facing strings MUST be localized using the ttag library. Localized strings should be complete phrases, do not concatenate a few separately localized strings. You should add context to strings where the meaning of the string might not be obvious in isolation: e.g. "Home" might have different words in some languages depending on whether you're talking about a dwelling or the landing page for a website.
|
||||
|
||||
57
.github/file-paths.yaml
vendored
57
.github/file-paths.yaml
vendored
@@ -11,22 +11,18 @@ shared_specs: &shared_specs
|
||||
- "test/**/*.cljc"
|
||||
- "test/**/*.cljs"
|
||||
|
||||
shared_ci: &shared_ci
|
||||
frontend_ci: &frontend_ci
|
||||
- ".github/actions/prepare-frontend/**"
|
||||
- ".github/actions/prepare-backend/**"
|
||||
- ".github/workflows/run-tests.yml"
|
||||
|
||||
shared_e2e: &shared_e2e
|
||||
- "e2e/runner/**"
|
||||
- "e2e/snapshot-creators/**"
|
||||
- "e2e/support/**"
|
||||
|
||||
frontend_ci: &frontend_ci
|
||||
- *shared_ci
|
||||
- ".github/workflows/frontend.yml"
|
||||
|
||||
frontend_embedding_sdk_ci: &frontend_embedding_sdk_ci
|
||||
- ".github/actions/prepare-frontend/**"
|
||||
- ".github/actions/prepare-backend/**"
|
||||
- ".github/workflows/*embedding-sdk*.yml"
|
||||
|
||||
frontend_configs: &frontend_configs
|
||||
- "bun.lock"
|
||||
- "yarn.lock"
|
||||
- "**/tsconfig*.json"
|
||||
- "package.json"
|
||||
- "babel.config.json"
|
||||
@@ -45,12 +41,14 @@ frontend_sources: &frontend_sources
|
||||
- "enterprise/frontend/!(test)**/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
|
||||
- "frontend/!(test)**/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
|
||||
|
||||
# Used only in .github/workflows/conventional-commit-pr-title-reminder.yml
|
||||
frontend_embedding_sources:
|
||||
- "enterprise/frontend/src/{embedding,embedding-*}/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
|
||||
- "enterprise/frontend/src/metabase-enterprise/{embedding,embedding-*}**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
|
||||
|
||||
# Used only in .github/workflows/embedding-sdk-labels-reminder.yml
|
||||
frontend_embedding_sdk_sources:
|
||||
- *frontend_configs
|
||||
- "enterprise/frontend/!(test)**/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
|
||||
|
||||
frontend_embedding_sdk_package_sources:
|
||||
- "enterprise/frontend/src/embedding-sdk-package/**/!(*.spec).{js,jsx,ts,tsx,css}"
|
||||
|
||||
@@ -78,16 +76,15 @@ frontend_loki_ci: &frontend_loki_ci
|
||||
- ".storybook/**"
|
||||
|
||||
backend_ci: &backend_ci
|
||||
- *shared_ci
|
||||
- ".github/actions/prepare-frontend/**"
|
||||
- ".github/actions/prepare-backend/**"
|
||||
- ".github/actions/test-driver/**"
|
||||
- ".github/workflows/backend.yml"
|
||||
- ".github/workflows/drivers.yml"
|
||||
|
||||
backend_presto_kerberos:
|
||||
- "**/presto_jdbc/**"
|
||||
- "**/presto-jdbc/**" # the driver module has a hyphen. underscores are classpaths
|
||||
- "**/presto_jdbc.clj"
|
||||
- "**/presto_jdbc_test.clj"
|
||||
- ".github/workflows/presto-kerberos-integration-test.yml"
|
||||
|
||||
backend_sources: &backend_sources
|
||||
@@ -120,9 +117,9 @@ sources: &sources
|
||||
- *backend_sources
|
||||
|
||||
e2e_ci: &e2e_ci
|
||||
- *shared_ci
|
||||
- ".github/actions/prepare-frontend/**"
|
||||
- ".github/actions/prepare-backend/**"
|
||||
- ".github/actions/prepare-uberjar-artifact/**"
|
||||
- ".github/actions/fetch-artifact/action.yml"
|
||||
- ".github/actions/e2e-prepare-containers/**"
|
||||
- ".github/actions/prepare-cypress/**"
|
||||
- ".github/workflows/e2e-*"
|
||||
@@ -130,27 +127,22 @@ e2e_ci: &e2e_ci
|
||||
e2e_specs: &e2e_specs
|
||||
- e2e/!(test-host-app)**/*.cy.*.{js,ts}
|
||||
|
||||
embedding_sdk_ci: &embedding_sdk_ci
|
||||
- *shared_ci
|
||||
- ".github/actions/prepare-uberjar-artifact/**"
|
||||
- ".github/actions/fetch-artifact/action.yml"
|
||||
- ".github/actions/prepare-cypress/**"
|
||||
- ".github/workflows/embedding-sdk*.yml"
|
||||
e2e_cross_version: &e2e_cross_version
|
||||
- ".github/workflows/e2e-cross-version.yml"
|
||||
- "e2e/test/scenarios/cross-version/**"
|
||||
|
||||
embedding_sdk_components:
|
||||
- *default
|
||||
- *embedding_sdk_ci
|
||||
e2e_embedding_sdk:
|
||||
- *frontend_embedding_sdk_ci
|
||||
- *sources
|
||||
- *shared_e2e
|
||||
- "e2e/test-component/**/*.cy.*.{js,jsx,ts,tsx}"
|
||||
- "e2e/component/**/*.cy.*.{js,ts}"
|
||||
|
||||
embedding_sdk_host_sample_apps:
|
||||
e2e_embedding_sdk_compatibility:
|
||||
- *default
|
||||
- *embedding_sdk_ci
|
||||
- *frontend_embedding_sdk_ci
|
||||
- *frontend_sources
|
||||
- *backend_sources
|
||||
- *shared_e2e
|
||||
- "e2e/embedding-sdk-host-apps/**"
|
||||
- "e2e/runner/embedding-sdk/**"
|
||||
- "e2e/test-host-app/**/*.cy.*.{js,ts}"
|
||||
|
||||
snowplow: &snowplow
|
||||
@@ -178,7 +170,6 @@ yaml:
|
||||
- "**/*.yaml"
|
||||
|
||||
codeql:
|
||||
- ".github/workflows/codeql.yml"
|
||||
- "frontend/src/**"
|
||||
- "enterprise/frontend/src/**"
|
||||
|
||||
|
||||
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`
|
||||
1
.github/scripts/average-e2e-timings.js
vendored
1
.github/scripts/average-e2e-timings.js
vendored
@@ -21,6 +21,7 @@ const collectAndAverageTimings = async ({
|
||||
|
||||
const timingData = await downloadAndExtractArtifacts(
|
||||
artifacts,
|
||||
token,
|
||||
github,
|
||||
context,
|
||||
);
|
||||
|
||||
7
.github/scripts/build-e2e-matrix.js
vendored
7
.github/scripts/build-e2e-matrix.js
vendored
@@ -3,8 +3,14 @@
|
||||
// grouping some specific tests together, other tests are split into chunks
|
||||
|
||||
const DEFAULT_SPEC_PATTERN = "./e2e/test/scenarios/**/*.cy.spec.*";
|
||||
const EMBEDDING_SDK_SPEC_PATTERN =
|
||||
"./e2e/test/scenarios/embedding-sdk/**.cy.spec.*";
|
||||
|
||||
const specialTestConfigs = [
|
||||
{
|
||||
name: "embedding-sdk",
|
||||
specs: EMBEDDING_SDK_SPEC_PATTERN,
|
||||
},
|
||||
{
|
||||
name: "oss-subset",
|
||||
edition: "oss",
|
||||
@@ -12,7 +18,6 @@ const specialTestConfigs = [
|
||||
specs: DEFAULT_SPEC_PATTERN,
|
||||
},
|
||||
{ name: "mongo", tags: "@mongo", specs: DEFAULT_SPEC_PATTERN },
|
||||
{ name: "python", tags: "@python", specs: DEFAULT_SPEC_PATTERN },
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
18
.github/scripts/build-e2e-matrix.unit.spec.js
vendored
18
.github/scripts/build-e2e-matrix.unit.spec.js
vendored
@@ -21,10 +21,10 @@ describe("buildMatrix", () => {
|
||||
});
|
||||
|
||||
it("should apply default specs path when empty entry is provided", () => {
|
||||
const result = testBuildMatrix("", 6);
|
||||
const result = testBuildMatrix("", 5);
|
||||
|
||||
expect(result.regularChunks).toBe(3);
|
||||
expect(result.config.length).toBe(6);
|
||||
expect(result.regularChunks).toBe(2);
|
||||
expect(result.config.length).toBe(5);
|
||||
});
|
||||
|
||||
it("should name regular test groups sequentially", () => {
|
||||
@@ -43,9 +43,9 @@ describe("buildMatrix", () => {
|
||||
|
||||
expect(result.regularChunks).toBe(0);
|
||||
expect(result.config.length).toBe(3);
|
||||
expect(result.config[0].name).toBe("oss-subset");
|
||||
expect(result.config[1].name).toBe("mongo");
|
||||
expect(result.config[2].name).toBe("python");
|
||||
expect(result.config[0].name).toBe("embedding-sdk");
|
||||
expect(result.config[1].name).toBe("oss-subset");
|
||||
expect(result.config[2].name).toBe("mongo");
|
||||
});
|
||||
|
||||
it("should correctly calculate regularChunks for non-default spec pattern", () => {
|
||||
@@ -83,9 +83,9 @@ describe("buildMatrix", () => {
|
||||
it("should return special test configs when default spec pattern is provided", () => {
|
||||
const result = testBuildMatrix("./e2e/test/scenarios/**/*.cy.spec.*", 50);
|
||||
|
||||
expect(result.config[47].name).toBe("oss-subset");
|
||||
expect(result.config[48].name).toBe("mongo");
|
||||
expect(result.config[49].name).toBe("python");
|
||||
expect(result.config[47].name).toBe("embedding-sdk");
|
||||
expect(result.config[48].name).toBe("oss-subset");
|
||||
expect(result.config[49].name).toBe("mongo");
|
||||
});
|
||||
|
||||
it("should not return special test configs when non-default spec pattern is provided", () => {
|
||||
|
||||
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 };
|
||||
40
.github/scripts/find-linear-issue.js
vendored
40
.github/scripts/find-linear-issue.js
vendored
@@ -1,40 +0,0 @@
|
||||
async function findLinearIssue({ githubIssueUrl, linearApiKey }) {
|
||||
const response = await fetch("https://api.linear.app/graphql", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: linearApiKey,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
query {
|
||||
attachmentsForURL(url: "${githubIssueUrl}") {
|
||||
nodes {
|
||||
issue { identifier }
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
return { success: false, message: `Linear API error: ${response.status}` };
|
||||
}
|
||||
|
||||
if (!data.data?.attachmentsForURL?.nodes?.length) {
|
||||
return {
|
||||
success: false,
|
||||
message: `No Linear issue linked to ${githubIssueUrl}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
issue: data.data.attachmentsForURL.nodes[0].issue.identifier,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { findLinearIssue };
|
||||
73
.github/team.json
vendored
73
.github/team.json
vendored
@@ -10,7 +10,14 @@
|
||||
"metamben",
|
||||
"wzimrin",
|
||||
"ericnormand",
|
||||
"galdre",
|
||||
"galdre"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Drivers",
|
||||
"label": ".Team/Drivers",
|
||||
"members": [
|
||||
"snoe",
|
||||
"rileythomp"
|
||||
]
|
||||
},
|
||||
@@ -25,6 +32,20 @@
|
||||
"lorem--ipsum"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Workflows",
|
||||
"label": ".Team/Workflows",
|
||||
"members": [
|
||||
"qnkhuat",
|
||||
"piranha",
|
||||
"crisptrutski",
|
||||
"spalt08",
|
||||
"stasgavrylov",
|
||||
"uladzimirdev",
|
||||
"kulyk",
|
||||
"kamilmielnik"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Admin Webapp",
|
||||
"label": ".Team/AdminWebapp",
|
||||
@@ -37,13 +58,10 @@
|
||||
"label": ".Team/DevEx",
|
||||
"members": [
|
||||
"camsaul",
|
||||
"dpsutton",
|
||||
"escherize",
|
||||
"fraserdrops",
|
||||
"nemanjaglumac"
|
||||
"escherize"
|
||||
]
|
||||
},
|
||||
{
|
||||
{
|
||||
"name": "Metabot",
|
||||
"label": ".Team/Metabot",
|
||||
"members": [
|
||||
@@ -52,11 +70,19 @@
|
||||
"retro",
|
||||
"sloansparger",
|
||||
"somtom",
|
||||
"wotbrew",
|
||||
"tsplude",
|
||||
"lbrdnk",
|
||||
"piranha",
|
||||
"crisptrutski",
|
||||
"undrfined"
|
||||
"bronsa"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Product Growth",
|
||||
"label": ".Team/ProductGrowth",
|
||||
"members": [
|
||||
"nemanjaglumac",
|
||||
"chodorowicz",
|
||||
"andsalves"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -80,37 +106,12 @@
|
||||
"eliotmetabase",
|
||||
"npfitz",
|
||||
"johnswanson",
|
||||
"iethree",
|
||||
"nvoxland",
|
||||
"edpaget",
|
||||
"deniskaber",
|
||||
"alxnddr",
|
||||
"andsalves",
|
||||
"kevinkelleher12"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Gadget",
|
||||
"label": ".Team/Gadget",
|
||||
"members": [
|
||||
"nmezzopera",
|
||||
"bronsa",
|
||||
"wotbrew",
|
||||
"qnkhuat",
|
||||
"chodorowicz",
|
||||
"kamilmielnik",
|
||||
"stasgavrylov",
|
||||
"uladzimirdev",
|
||||
"egorgrushin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Growth Eng",
|
||||
"label": ".Team/GrowthEng",
|
||||
"members": [
|
||||
"iethree",
|
||||
"bpander",
|
||||
"gantoreno",
|
||||
"losrebellos"
|
||||
"bpander"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
49
.github/workflows/app-db.yml
vendored
49
.github/workflows/app-db.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
be-tests-mariadb:
|
||||
if: ${{ !inputs.skip }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 40
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -68,8 +68,8 @@ jobs:
|
||||
(
|
||||
github.event_name == 'push' &&
|
||||
(
|
||||
github.ref_name == 'master' ||
|
||||
startsWith(github.ref_name, 'release-')
|
||||
github.ref == 'master' ||
|
||||
startsWith(github.ref, 'release-')
|
||||
) &&
|
||||
''
|
||||
) ||
|
||||
@@ -105,7 +105,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test ${{ matrix.version.name }}
|
||||
id: test-driver
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
build-static-viz: ${{ matrix.job.build-static-viz }}
|
||||
@@ -124,12 +123,11 @@ jobs:
|
||||
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
|
||||
|
||||
be-tests-mysql:
|
||||
if: ${{ !inputs.skip }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 40
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -139,13 +137,11 @@ jobs:
|
||||
image: cimg/mysql:8.0
|
||||
env:
|
||||
enable-ssl-tests: 'false'
|
||||
enable-aws-iam-tests: 'false'
|
||||
- name: MySQL Latest
|
||||
junit-name: be-tests-mysql-latest-ee
|
||||
image: mysql:latest
|
||||
env:
|
||||
enable-ssl-tests: 'true'
|
||||
enable-aws-iam-tests: 'true'
|
||||
job:
|
||||
- name: Enterprise Tests
|
||||
build-static-viz: false
|
||||
@@ -184,8 +180,8 @@ jobs:
|
||||
(
|
||||
github.event_name == 'push' &&
|
||||
(
|
||||
github.ref_name == 'master' ||
|
||||
startsWith(github.ref_name, 'release-')
|
||||
github.ref == 'master' ||
|
||||
startsWith(github.ref, 'release-')
|
||||
) &&
|
||||
''
|
||||
) ||
|
||||
@@ -215,21 +211,12 @@ jobs:
|
||||
MB_MYSQL_SSL_TEST_SSL_CERT: ${{ secrets.MB_MYSQL_SSL_TEST_SSL_CERT }}
|
||||
MB_MYSQL_SSL_TEST_USER: metabase
|
||||
MB_MYSQL_SSL_TEST_PASSWORD: ${{ secrets.MYSQL_RDS_SSL_INSTANCE_PASSWORD }}
|
||||
MB_MYSQL_AWS_IAM_TEST: ${{ matrix.version.env.enable-aws-iam-tests }}
|
||||
MB_MYSQL_AWS_IAM_TEST_HOST: ${{ secrets.MYSQL_AURORA_IAM_INSTANCE_HOST }}
|
||||
MB_MYSQL_AWS_IAM_TEST_PORT: 3306
|
||||
MB_MYSQL_AWS_IAM_TEST_USER: my_iam_user
|
||||
MB_MYSQL_AWS_IAM_TEST_DBNAME: test_db
|
||||
MB_MYSQL_AWS_IAM_TEST_SSL_CERT: trust
|
||||
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-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,12 +235,11 @@ jobs:
|
||||
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
|
||||
|
||||
be-tests-postgres:
|
||||
if: ${{ !inputs.skip }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 40
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -263,14 +249,18 @@ jobs:
|
||||
docker-image: postgres:12-alpine
|
||||
env:
|
||||
enable-ssl-tests: 'false'
|
||||
enable-aws-iam-tests: 'false'
|
||||
- name: Postgres Latest
|
||||
junit-name: postgres-latest-ee
|
||||
docker-image: postgres:latest
|
||||
env:
|
||||
enable-ssl-tests: 'true'
|
||||
enable-aws-iam-tests: 'true'
|
||||
job:
|
||||
- name: Driver Tests
|
||||
build-static-viz: false
|
||||
test-args: >-
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
exclude-tag: ':mb/transforms-python-test'
|
||||
- name: Enterprise Tests
|
||||
build-static-viz: false
|
||||
test-args: >-
|
||||
@@ -301,8 +291,8 @@ jobs:
|
||||
(
|
||||
github.event_name == 'push' &&
|
||||
(
|
||||
github.ref_name == 'master' ||
|
||||
startsWith(github.ref_name, 'release-')
|
||||
github.ref == 'master' ||
|
||||
startsWith(github.ref, 'release-')
|
||||
) &&
|
||||
''
|
||||
) ||
|
||||
@@ -321,13 +311,6 @@ jobs:
|
||||
MB_POSTGRES_SSL_TEST_SSL: ${{ matrix.version.env.enable-ssl-tests }}
|
||||
MB_POSTGRES_SSL_TEST_SSL_MODE: verify-full
|
||||
MB_POSTGRES_SSL_TEST_SSL_ROOT_CERT_PATH: 'test-resources/certificates/us-east-2-bundle.pem'
|
||||
MB_POSTGRES_AWS_IAM_TEST: ${{ matrix.version.env.enable-aws-iam-tests }}
|
||||
MB_POSTGRES_AWS_IAM_TEST_HOST: ${{ secrets.POSTGRES_AURORA_IAM_INSTANCE_HOST }}
|
||||
MB_POSTGRES_AWS_IAM_TEST_PORT: 5432
|
||||
MB_POSTGRES_AWS_IAM_TEST_USER: my_iam_user
|
||||
MB_POSTGRES_AWS_IAM_TEST_DBNAME: test_db
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_AURORA_IAM_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_AURORA_IAM_SECRET_ACCESS_KEY }}
|
||||
services:
|
||||
postgres:
|
||||
image: ${{ matrix.version.docker-image }}
|
||||
@@ -340,7 +323,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test ${{ matrix.version.name }} (${{ matrix.job.name }})
|
||||
id: test-driver
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
build-static-viz: ${{ matrix.job.build-static-viz }}
|
||||
@@ -359,7 +341,6 @@ jobs:
|
||||
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
|
||||
|
||||
app-db-tests-result:
|
||||
needs:
|
||||
|
||||
9
.github/workflows/auto-assign.yml
vendored
9
.github/workflows/auto-assign.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: | #js
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const teamConfig = JSON.parse(fs.readFileSync('.github/team.json', 'utf-8'));
|
||||
const prAuthor = context.payload.pull_request.user.login;
|
||||
@@ -65,3 +65,10 @@ jobs:
|
||||
issue_number: prNumber,
|
||||
labels: [team.label]
|
||||
});
|
||||
|
||||
return team.projectUrl || null;
|
||||
- uses: actions/add-to-project@v0.5.0
|
||||
if: ${{ steps.auto-assign.outputs.result != 'null' }}
|
||||
with:
|
||||
project-url: ${{ steps.auto-assign.outputs.result }}
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
91
.github/workflows/backend-cloverage.yml
vendored
91
.github/workflows/backend-cloverage.yml
vendored
@@ -1,109 +1,30 @@
|
||||
name: Backend-Cloverage
|
||||
|
||||
# Module lists are generated dynamically by listing source directories.
|
||||
# The setup-oss and setup-ee jobs list folders in src/metabase/ and
|
||||
# enterprise/backend/src/metabase_enterprise/ respectively, convert
|
||||
# underscores to dashes (for Clojure namespace convention), and output
|
||||
# as JSON arrays for the matrix jobs to consume.
|
||||
#
|
||||
# jq flags: -R (raw string input), -s (slurp all lines into one string),
|
||||
# -c (compact single-line output for GitHub Actions).
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 10 * * 1-5' # every weekday at 5am US eastern time
|
||||
|
||||
jobs:
|
||||
setup-oss:
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
modules: ${{ steps.get-modules.outputs.modules }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- id: get-modules
|
||||
run: |
|
||||
modules=$(ls -1d src/metabase/*/ | xargs -n1 basename | tr '_' '-' | jq -R -s -c 'split("\n") | map(select(length > 0))')
|
||||
echo "modules=$modules" >> $GITHUB_OUTPUT
|
||||
|
||||
be-linter-cloverage:
|
||||
needs: setup-oss
|
||||
# don't run this workflow on a cron for forks
|
||||
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
modules: ${{ fromJson(needs.setup-oss.outputs.modules) }}
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Prepare front-end environment
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
|
||||
with:
|
||||
m2-cache-key: "cloverage"
|
||||
- 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
|
||||
run: |
|
||||
clojure -X:dev:ci:ee:ee-dev:test:cloverage \
|
||||
:ns-regex '["^metabase\\.${{ matrix.modules }}\\..*"]' \
|
||||
:test-ns-path '["test/metabase/${{ matrix.modules }}"]'
|
||||
continue-on-error: true
|
||||
|
||||
run: clojure -X:dev:ci:ee:ee-dev:test:cloverage
|
||||
- name: Upload coverage to codecov.io
|
||||
if: always()
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./target/coverage/codecov.json
|
||||
flags: back-end
|
||||
|
||||
setup-ee:
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
modules: ${{ steps.get-modules.outputs.modules }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- id: get-modules
|
||||
run: |
|
||||
modules=$(ls -1d enterprise/backend/src/metabase_enterprise/*/ | xargs -n1 basename | tr '_' '-' | jq -R -s -c 'split("\n") | map(select(length > 0))')
|
||||
echo "modules=$modules" >> $GITHUB_OUTPUT
|
||||
|
||||
be-linter-cloverage-enterprise:
|
||||
needs: setup-ee
|
||||
# don't run this workflow on a cron for forks
|
||||
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
modules: ${{ fromJson(needs.setup-ee.outputs.modules) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Prepare front-end environment
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
|
||||
- name: Build static viz frontend
|
||||
run: bun run build-static-viz
|
||||
env:
|
||||
MB_EDITION: ee
|
||||
- name: Collect the test coverage
|
||||
run: |
|
||||
clojure -X:dev:ci:ee:ee-dev:test:cloverage \
|
||||
:ns-regex '["^metabase-enterprise\\.${{ matrix.modules }}\\..*"]' \
|
||||
:test-ns-path '["enterprise/backend/test/metabase/${{ matrix.modules }}"]'
|
||||
continue-on-error: true
|
||||
|
||||
- name: Upload coverage to codecov.io
|
||||
if: always()
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./target/coverage/codecov.json
|
||||
|
||||
44
.github/workflows/backend.yml
vendored
44
.github/workflows/backend.yml
vendored
@@ -21,6 +21,8 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
with:
|
||||
m2-cache-key: 'kondo'
|
||||
- name: Run clj-kondo
|
||||
run: ./bin/mage kondo
|
||||
|
||||
@@ -33,13 +35,15 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
with:
|
||||
m2-cache-key: "eastwood"
|
||||
- run: clojure -X:dev:ee:ee-dev:drivers:drivers-dev:test:eastwood
|
||||
name: Run Eastwood linter
|
||||
|
||||
be-tests:
|
||||
if: ${{ !inputs.skip }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 40
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -47,33 +51,33 @@ jobs:
|
||||
job:
|
||||
- name: Enterprise Tests
|
||||
edition: ee
|
||||
test-group-name: ee
|
||||
build-static-viz: 'false'
|
||||
test-args: >-
|
||||
:only '"enterprise/backend/test"'
|
||||
- name: EE App DB Tests (Part 1)
|
||||
edition: ee
|
||||
test-group-name: ee-app-db-1
|
||||
build-static-viz: 'true'
|
||||
test-args: >-
|
||||
:only '["test" ".clj-kondo/test"]'
|
||||
:partition/total 2
|
||||
:partition/index 0
|
||||
- name: EE App DB Tests (Part 2)
|
||||
edition: ee
|
||||
test-group-name: ee-app-db-2
|
||||
build-static-viz: 'true'
|
||||
test-args: >-
|
||||
:only '["test" ".clj-kondo/test"]'
|
||||
:partition/total 2
|
||||
:partition/index 1
|
||||
- name: OSS App DB Tests (Part 1)
|
||||
edition: oss
|
||||
test-group-name: oss-app-db-1
|
||||
build-static-viz: 'true'
|
||||
test-args: >-
|
||||
:only '["test" ".clj-kondo/test"]'
|
||||
:partition/total 2
|
||||
:partition/index 0
|
||||
- name: OSS App DB Tests (Part 2)
|
||||
edition: oss
|
||||
test-group-name: oss-app-db-2
|
||||
build-static-viz: 'true'
|
||||
test-args: >-
|
||||
:only '["test" ".clj-kondo/test"]'
|
||||
:partition/total 2
|
||||
@@ -88,8 +92,8 @@ jobs:
|
||||
(
|
||||
github.event_name == 'push' &&
|
||||
(
|
||||
github.ref_name == 'master' ||
|
||||
startsWith(github.ref_name, 'release-')
|
||||
github.ref == 'master' ||
|
||||
startsWith(github.ref, 'release-')
|
||||
) &&
|
||||
''
|
||||
) ||
|
||||
@@ -98,15 +102,26 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Prepare front-end environment
|
||||
if: ${{ matrix.job.build-static-viz == 'true' }}
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
with:
|
||||
m2-cache-key: be-tests
|
||||
java-version: "${{ matrix.java-version }}"
|
||||
# Depending on which files changed, we might either have to build static-viz
|
||||
# from scratch or download the previously built artifact
|
||||
- name: Build static-viz frontend
|
||||
run: bun run build-static-viz
|
||||
if: ${{ matrix.job.build-static-viz == 'true' && !inputs.skip }}
|
||||
run: yarn build-static-viz
|
||||
env:
|
||||
MB_EDITION: ${{ matrix.job.edition }}
|
||||
- name: Download Static Viz Bundle Artifact
|
||||
if: ${{ matrix.job.build-static-viz == 'true' && !inputs.skip }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: static-viz-bundle-${{ github.sha }}
|
||||
path: resources/frontend_client/app/dist
|
||||
|
||||
- name: "Test Java ${{ matrix.java-version }} ${{ matrix.job.name }}"
|
||||
id: run-java-tests
|
||||
@@ -114,10 +129,6 @@ jobs:
|
||||
clojure -X:dev:ci:test:${{ matrix.job.edition }}:${{ matrix.job.edition }}-dev
|
||||
:exclude-tags '[:mb/driver-tests ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
|
||||
${{ matrix.job.test-args }}
|
||||
# Trunk Quarantine controls the final state of the run in the CI!
|
||||
# Even if this step fails we let it pass, and then `trunk-io/analytics-uploader` determines
|
||||
# whether to fail or to pass the whole job, based on the quarantine list.
|
||||
continue-on-error: true
|
||||
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
@@ -130,7 +141,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
|
||||
@@ -147,7 +157,7 @@ jobs:
|
||||
if: always() && !success()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-logs-${{ matrix.java-version }}-${{ matrix.job.edition }}-${{ github.job }}-${{ matrix.job.test-group-name }}
|
||||
name: test-logs-${{ matrix.java-version }}-${{ matrix.job.edition }}-${{ github.job }}
|
||||
path: logs/test-log.json
|
||||
retention-days: 1
|
||||
|
||||
@@ -165,6 +175,8 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Prepare backend
|
||||
uses: ./.github/actions/prepare-backend
|
||||
with:
|
||||
m2-cache-key: be-check
|
||||
# TODO -- we should probably also check WITHOUT ee -- do a check with only OSS namespaces on the classpath
|
||||
- name: Check namespaces
|
||||
run: clojure -M:ee:drivers:check
|
||||
@@ -178,6 +190,8 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
with:
|
||||
m2-cache-key: 'cljfmt'
|
||||
- name: Run cljfmt
|
||||
run: ./bin/mage cljfmt-all --force-check
|
||||
|
||||
|
||||
52
.github/workflows/bugs-triage-slack-notification.yml
vendored
Normal file
52
.github/workflows/bugs-triage-slack-notification.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: Bugs Slack Notification
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- labeled
|
||||
jobs:
|
||||
on-issue-labeled:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 5
|
||||
if: |
|
||||
github.event.label.name == '.Needs Triage' && contains(github.event.issue.labels.*.name, 'Type:Bug')
|
||||
steps:
|
||||
- name: Setting title
|
||||
uses: actions/github-script@v7
|
||||
id: vars
|
||||
with:
|
||||
script: |
|
||||
core.setOutput('issue_title', ${{ toJson(github.event.issue.title) }}.replaceAll(/"/g, '\\"'));
|
||||
- name: Check for labels containing team name
|
||||
id: check_labels
|
||||
run: |
|
||||
labels_json=$(sed 's/&/\$/g' <<< '${{ toJson(github.event.issue.labels) }}')
|
||||
echo "has_team_label=false" >> "$GITHUB_OUTPUT"
|
||||
for label in "$labels_json"
|
||||
do
|
||||
if [[ $label == *".Team"* ]]; then
|
||||
echo "Label contains the team name."
|
||||
echo "has_team_label=true" >> "$GITHUB_OUTPUT"
|
||||
break
|
||||
fi
|
||||
done
|
||||
- name: Send the issue ${{ github.event.issue.number }} to Slack
|
||||
id: slack
|
||||
if: steps.check_labels.outputs.has_team_label == 'false'
|
||||
uses: slackapi/slack-github-action@v1.26.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "${{ steps.vars.outputs.issue_title }}\n${{ github.event.issue.html_url }}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_BUG_TRIAGE_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
35
.github/workflows/build-for-release.yml
vendored
35
.github/workflows/build-for-release.yml
vendored
@@ -19,11 +19,9 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
release
|
||||
.github
|
||||
- name: Prepare release scripts
|
||||
uses: ./.github/actions/prepare-release-scripts
|
||||
sparse-checkout: release
|
||||
- name: Prepare build scripts
|
||||
run: cd ${{ github.workspace }}/release && yarn && yarn build
|
||||
- name: Send build start message
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
@@ -81,11 +79,9 @@ jobs:
|
||||
exit 1
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
release
|
||||
.github
|
||||
- name: Prepare release scripts
|
||||
uses: ./.github/actions/prepare-release-scripts
|
||||
sparse-checkout: release
|
||||
- name: Prepare build scripts
|
||||
run: cd ${{ github.workspace }}/release && yarn && yarn build
|
||||
- name: Get Release Version
|
||||
uses: actions/github-script@v7
|
||||
id: canonical_version
|
||||
@@ -125,11 +121,9 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # IMPORTANT! to get all the branches
|
||||
sparse-checkout: |
|
||||
release
|
||||
.github
|
||||
- name: Prepare release scripts
|
||||
uses: ./.github/actions/prepare-release-scripts
|
||||
sparse-checkout: release
|
||||
- name: Prepare build scripts
|
||||
run: cd ${{ github.workspace }}/release && yarn && yarn build
|
||||
- name: Get Release Branch
|
||||
uses: actions/github-script@v7
|
||||
id: release_branch
|
||||
@@ -141,13 +135,12 @@ jobs:
|
||||
const releaseBranch = getReleaseBranch(version);
|
||||
return releaseBranch;
|
||||
- name: Ensure that the specified commit exists in the ${{ steps.release_branch.outputs.result }} release branch
|
||||
run: | # bash
|
||||
run: |
|
||||
RELEASE_BRANCH=${{ steps.release_branch.outputs.result }}
|
||||
git checkout $RELEASE_BRANCH
|
||||
git branch --contains ${{ inputs.commit }} | grep -q $RELEASE_BRANCH \
|
||||
&& echo "Commit found in correct release branch" \
|
||||
|| (echo "Commit not found in correct release branch" && exit 1)
|
||||
git checkout -
|
||||
|
||||
get-build-requirements:
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -159,11 +152,9 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
release
|
||||
.github
|
||||
- name: Prepare release scripts
|
||||
uses: ./.github/actions/prepare-release-scripts
|
||||
sparse-checkout: release
|
||||
- name: Prepare build scripts
|
||||
run: cd ${{ github.workspace }}/release && yarn && yarn build
|
||||
- name: Get build dependencies
|
||||
uses: actions/github-script@v7
|
||||
id: dependencies
|
||||
|
||||
2
.github/workflows/build-scripts.yml
vendored
2
.github/workflows/build-scripts.yml
vendored
@@ -16,6 +16,8 @@ jobs:
|
||||
- name: Prepare back-end environment
|
||||
timeout-minutes: 5
|
||||
uses: ./.github/actions/prepare-backend
|
||||
with:
|
||||
m2-cache-key: 'build-scripts'
|
||||
|
||||
- name: Run build and release script tests
|
||||
run: clojure -X:dev:drivers:build:build-dev:build-test:ci
|
||||
|
||||
140
.github/workflows/cache-generator.yml
vendored
140
.github/workflows/cache-generator.yml
vendored
@@ -1,140 +0,0 @@
|
||||
# This workflow is the single source of truth for generating and updating various caches used by our CI pipeline.
|
||||
# It sets the fresh cache each Monday but provides a manual trigger to update it on-demand.
|
||||
#
|
||||
# All other jobs only restore from this cache; *they never update it*!
|
||||
# They should always use `actions/cache/restore@v4` because `actions/cache@v4` would attempt to save the cache.
|
||||
#
|
||||
# Our agreed-upon strategy is to use a deterministic cache key based on the week of the year.
|
||||
# Jobs can utilize the prior week's cache if the current week's cache is not found.
|
||||
#
|
||||
#
|
||||
# PRINCIPLES:
|
||||
# - The cache is never wrong!
|
||||
# - Caches are immutable once created.
|
||||
# - Caches are for speeding up builds, not for correctness.
|
||||
# - It is faster to download a large cache than to re-resolve missing dependencies.
|
||||
#
|
||||
#
|
||||
# DESIGN:
|
||||
#
|
||||
# 1. Prepare
|
||||
# - A job first prepares the local environment (e.g., installs JDK, Clojure, Node.js, etc.)
|
||||
# - This fetches the dependencies from the cache (if it exists) and warms up the local cache for this workflow.
|
||||
# 2. Resolve
|
||||
# - The next step pre-resolves all dependencies, ensuring that the local cache is fully populated and up-to-date.
|
||||
# 3. Update
|
||||
# - The final step saves the local cache back to GitHub Actions cache with a deterministic key.
|
||||
# - If a cache entry already exists for that key, it is deleted first to ensure immutability.
|
||||
#
|
||||
name: Cache Generator
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 3 * * 1" # Mondays at 03:00 UTC
|
||||
workflow_dispatch: # manually update the cache
|
||||
|
||||
concurrency:
|
||||
group: cache-generator
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
get-cache-keys:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get cache keys
|
||||
uses: ./.github/actions/get-cache-keys
|
||||
id: get-cache-keys
|
||||
outputs:
|
||||
m2-cache-key: ${{ steps.get-cache-keys.outputs.m2-cache-key }}
|
||||
node-cache-key: ${{ steps.get-cache-keys.outputs.node-cache-key }}
|
||||
eslint-cache-key: ${{ steps.get-cache-keys.outputs.eslint-cache-key }}
|
||||
cypress-cache-key: ${{ steps.get-cache-keys.outputs.cypress-cache-key }}
|
||||
|
||||
generate-caches:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
needs: get-cache-keys
|
||||
env:
|
||||
M2_CACHE_KEY: ${{ needs.get-cache-keys.outputs.m2-cache-key }}
|
||||
NODE_CACHE_KEY: ${{ needs.get-cache-keys.outputs.node-cache-key }}
|
||||
CYPRESS_CACHE_KEY: ${{ needs.get-cache-keys.outputs.cypress-cache-key }}
|
||||
ESLINT_CACHE_KEY: ${{ needs.get-cache-keys.outputs.eslint-cache-key }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Backend preparation and cache
|
||||
- name: Prepare backend
|
||||
uses: ./.github/actions/prepare-backend
|
||||
|
||||
# Add or remove aliases to affect the cache size and its contents.
|
||||
# Adjust to taste, then redeploy this workflow.
|
||||
- name: Pre-resolve Clojure dependencies
|
||||
run: clojure -A:build:dev:ee:ee-dev:drivers:drivers-dev:cljs -P
|
||||
|
||||
- name: Update M2 cache
|
||||
uses: ./.github/actions/update-cache
|
||||
with:
|
||||
path: |
|
||||
~/.m2
|
||||
~/.gitlibs
|
||||
key: ${{ env.M2_CACHE_KEY }}
|
||||
|
||||
# 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.
|
||||
# IMPORTANT: Caching node_modules after patches are already applied will break any PR that attempts to either:
|
||||
# - upgrade a patched library
|
||||
# - change a patch
|
||||
# Hence why we explicitly exclude patches from the cache (controlled by the `apply-patches` action input).
|
||||
- name: Install front-end dependencies
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
with:
|
||||
apply-patches: "false"
|
||||
|
||||
- name: Get bun cache directory
|
||||
id: bun-cache
|
||||
run: |
|
||||
BUN_CACHE_DIR=$(bun pm cache)
|
||||
echo "Bun cache directory: $BUN_CACHE_DIR"
|
||||
echo "dir=$BUN_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
|
||||
shell: bash
|
||||
|
||||
# Run Prettier to populate its cache (stored in node_modules/.cache/prettier)
|
||||
# so it gets saved as part of the node_modules cache below
|
||||
- name: Run Prettier to populate cache
|
||||
run: bun run lint-prettier-pure
|
||||
|
||||
- name: Update node_modules cache
|
||||
uses: ./.github/actions/update-cache
|
||||
with:
|
||||
path: |
|
||||
${{ steps.bun-cache.outputs.dir }}
|
||||
node_modules
|
||||
key: ${{ env.NODE_CACHE_KEY }}
|
||||
|
||||
- name: Update Cypress cache
|
||||
uses: ./.github/actions/update-cache
|
||||
with:
|
||||
path: ~/.cache/Cypress
|
||||
key: ${{ env.CYPRESS_CACHE_KEY }}
|
||||
|
||||
# ESLint cache (requires both backend and frontend)
|
||||
- name: Compile CLJS
|
||||
run: bun run build-pure:cljs
|
||||
|
||||
- name: Run ESLint to populate cache
|
||||
run: bun run lint-eslint-pure
|
||||
|
||||
- name: Update eslint cache
|
||||
uses: ./.github/actions/update-cache
|
||||
with:
|
||||
path: ${{ github.workspace}}/.eslintcache
|
||||
key: ${{ env.ESLINT_CACHE_KEY }}
|
||||
8
.github/workflows/check-milestone.yml
vendored
8
.github/workflows/check-milestone.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/chromatic.yml
vendored
4
.github/workflows/chromatic.yml
vendored
@@ -40,8 +40,10 @@ jobs:
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
with:
|
||||
m2-cache-key: "cljs"
|
||||
- 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:
|
||||
|
||||
59
.github/workflows/claude-code-bug-fix.yml
vendored
59
.github/workflows/claude-code-bug-fix.yml
vendored
@@ -70,31 +70,6 @@ jobs:
|
||||
core.setFailed(error.message);
|
||||
}
|
||||
|
||||
- name: Find linked Linear issue
|
||||
id: find_linear
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
|
||||
ISSUE_NUMBER: ${{ steps.get_issue.outputs.issue_number }}
|
||||
with:
|
||||
script: |
|
||||
const { findLinearIssue } = require('${{ github.workspace }}/.github/scripts/find-linear-issue.js');
|
||||
|
||||
const githubIssueUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/issues/${process.env.ISSUE_NUMBER}`;
|
||||
|
||||
const result = await findLinearIssue({
|
||||
githubIssueUrl,
|
||||
linearApiKey: process.env.LINEAR_API_KEY
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log(`Found Linear issue: ${result.issue}`);
|
||||
core.setOutput('linear_issue', result.issue);
|
||||
} else {
|
||||
console.log(`Could not find Linear issue: ${result.message}`);
|
||||
}
|
||||
|
||||
- name: Set up git configuration
|
||||
run: |
|
||||
git config --global user.name "Claude Code[bot]"
|
||||
@@ -184,15 +159,15 @@ jobs:
|
||||
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
|
||||
3. **Implement a fix** - Follow existing code patterns and conventions
|
||||
4. **Test the fix** - Run appropriate tests using the commands in CLAUDE.md:
|
||||
- JavaScript/TypeScript: `yarn test-unit path/to/file.unit.spec.js`
|
||||
- Clojure: `clojure -X:dev:test :only namespace/test-name`
|
||||
- Lint: `yarn lint-eslint-pure` for JS/TS, `./bin/mage kondo <file>` for Clojure
|
||||
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
|
||||
- Follow the development patterns in CLAUDE.md
|
||||
- Write failing tests first, then fix them
|
||||
- Work in small, testable increments
|
||||
- Don't commit changes - leave that for the workflow
|
||||
@@ -230,12 +205,6 @@ jobs:
|
||||
mkdir -p .claude-scratchpad
|
||||
echo "Scratchpad directory created for Claude's temporary verification files"
|
||||
|
||||
- name: Prepare front-end environment
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
|
||||
- name: Generate GitHub App Token for Claude
|
||||
uses: actions/create-github-app-token@v1
|
||||
id: claude-app-token
|
||||
@@ -244,14 +213,15 @@ jobs:
|
||||
private-key: ${{ secrets.METABASE_BOT_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Generate Bug Fix with Claude Code
|
||||
uses: anthropics/claude-code-action@v1
|
||||
uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
mode: agent
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ steps.claude-app-token.outputs.token }}
|
||||
prompt: ${{ env.BUG_FIX_PROMPT }}
|
||||
claude_args: |
|
||||
--allowedTools "Bash(bun run test-unit*),Bash(bun run lint-eslint-pure),Bash(./bin/mage*),Bash(clojure*),Bash(find*),Bash(ls*),Bash(grep*),Write,Read,Glob,Grep,Edit,MultiEdit"
|
||||
--disallowedTools "Bash(git commit*),Bash(git push*),Bash(gh*)"
|
||||
direct_prompt: ${{ env.BUG_FIX_PROMPT }}
|
||||
timeout_minutes: "25"
|
||||
allowed_tools: "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"
|
||||
disallowed_tools: "Bash(git commit*),Bash(git push*),Bash(gh*)"
|
||||
|
||||
- name: Clean up scratchpad directory
|
||||
run: |
|
||||
@@ -290,7 +260,6 @@ jobs:
|
||||
core.setOutput('reviewers', issueCreator);
|
||||
|
||||
- name: Create Pull Request
|
||||
id: create_pr
|
||||
if: steps.check_changes.outputs.has_changes == 'true'
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
@@ -311,8 +280,6 @@ jobs:
|
||||
body: |
|
||||
This PR automatically fixes the bug reported in #${{ env.ISSUE_NUMBER }}.
|
||||
|
||||
${{ steps.find_linear.outputs.linear_issue && format('Closes {0}', steps.find_linear.outputs.linear_issue) || '' }}
|
||||
|
||||
## Bug Report
|
||||
**Original Issue:** #${{ env.ISSUE_NUMBER }}
|
||||
**Reporter:** @${{ env.ISSUE_AUTHOR }}
|
||||
@@ -333,6 +300,7 @@ jobs:
|
||||
- If issues are found, either fix them or close this PR
|
||||
|
||||
---
|
||||
Closes #${{ env.ISSUE_NUMBER }}
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
|
||||
@@ -342,7 +310,6 @@ jobs:
|
||||
automated-fix
|
||||
ci skip
|
||||
reviewers: ${{ steps.prepare_reviewers.outputs.reviewers }}
|
||||
assignees: ${{ github.actor }}
|
||||
|
||||
- name: Log existing branch update
|
||||
if: steps.check_changes.outputs.has_changes == 'true' && env.BRANCH_EXISTS == 'true'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user