Compare commits
175 Commits
openapi
...
load-stats
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0815d3471 | ||
|
|
6b27c064dc | ||
|
|
1c5ba041d6 | ||
|
|
86c23eaf5e | ||
|
|
1447d9cf97 | ||
|
|
90688e49a7 | ||
|
|
60f2668137 | ||
|
|
503c3bf068 | ||
|
|
a6de42f467 | ||
|
|
c998859487 | ||
|
|
a491bd11be | ||
|
|
0a30b63176 | ||
|
|
f1ce332568 | ||
|
|
1690028d0f | ||
|
|
0c76874681 | ||
|
|
a6be073fb2 | ||
|
|
cb4541b965 | ||
|
|
463fb9355b | ||
|
|
791b649cc0 | ||
|
|
085418edb8 | ||
|
|
1118c3e897 | ||
|
|
6e74e41eaf | ||
|
|
38404fd9df | ||
|
|
8a7cd79f47 | ||
|
|
f3685652d3 | ||
|
|
08497ded2f | ||
|
|
bfe0b19738 | ||
|
|
a155c1f4ee | ||
|
|
9f48230b57 | ||
|
|
503349c6ae | ||
|
|
4d60c4bcf5 | ||
|
|
d9f905b7da | ||
|
|
36f273d523 | ||
|
|
38cda45f69 | ||
|
|
fc18a66b6c | ||
|
|
433561ad5b | ||
|
|
3c23f921e2 | ||
|
|
7f4883cc49 | ||
|
|
667d038d46 | ||
|
|
19df422bbd | ||
|
|
9fff12013e | ||
|
|
4bc6ccb00c | ||
|
|
81049d6069 | ||
|
|
85ce065045 | ||
|
|
dcb85ae924 | ||
|
|
3403f82c0a | ||
|
|
95306c12fc | ||
|
|
5d1e1ad8f1 | ||
|
|
6465693b78 | ||
|
|
4e4cbac853 | ||
|
|
9762d0e164 | ||
|
|
13e00d55b9 | ||
|
|
ad2d269251 | ||
|
|
a22958cee3 | ||
|
|
e92dfab50e | ||
|
|
7c4159eedd | ||
|
|
6dc53c3773 | ||
|
|
86d1bf3ec7 | ||
|
|
fe4e2beffa | ||
|
|
e096a361d8 | ||
|
|
8b58b48375 | ||
|
|
ecde907aff | ||
|
|
d1934fb897 | ||
|
|
03a39f47c8 | ||
|
|
ad22a8730c | ||
|
|
3d145ccb73 | ||
|
|
1b93876984 | ||
|
|
283b194a38 | ||
|
|
28b2ad9eb6 | ||
|
|
5b705231d7 | ||
|
|
10f53bdcec | ||
|
|
2e1a0c1a2c | ||
|
|
e49b98186b | ||
|
|
f74d32c39a | ||
|
|
6431bc412b | ||
|
|
40a15bc485 | ||
|
|
b0c8103cdb | ||
|
|
4b88b2ae79 | ||
|
|
d3c7576ccb | ||
|
|
7dbf078a40 | ||
|
|
c0949b1d38 | ||
|
|
ebdcd6c743 | ||
|
|
66038ba6d0 | ||
|
|
97d871c78a | ||
|
|
6bb98bce1d | ||
|
|
c908146299 | ||
|
|
2d80c0e360 | ||
|
|
7f589be657 | ||
|
|
12e5f16918 | ||
|
|
979b344073 | ||
|
|
4558ce654d | ||
|
|
e584e95d48 | ||
|
|
6ed9837a03 | ||
|
|
e4cc4a61ff | ||
|
|
c1805286bb | ||
|
|
22b2577799 | ||
|
|
2a35f6390b | ||
|
|
f508214657 | ||
|
|
a24ac528b3 | ||
|
|
e0c40554da | ||
|
|
c9c2552964 | ||
|
|
b57475ab38 | ||
|
|
758aade121 | ||
|
|
5db23f4182 | ||
|
|
0efc4c2161 | ||
|
|
f4b04b6966 | ||
|
|
83ed9b6c3d | ||
|
|
0cd9134160 | ||
|
|
d273dc56b6 | ||
|
|
2d6db97c9d | ||
|
|
3fb5cbcb74 | ||
|
|
f52a94984a | ||
|
|
8170a2ecdd | ||
|
|
ca2c341522 | ||
|
|
3390d69763 | ||
|
|
adf3bd7254 | ||
|
|
2357c1b843 | ||
|
|
e85fa9c446 | ||
|
|
250868257e | ||
|
|
27752c76e0 | ||
|
|
15106f17fd | ||
|
|
4b2cfdc623 | ||
|
|
3439faff23 | ||
|
|
18f216de52 | ||
|
|
e354134858 | ||
|
|
34da51c654 | ||
|
|
55f3c0f268 | ||
|
|
4cb99413ed | ||
|
|
beb22cd1b5 | ||
|
|
eb7bcaa06f | ||
|
|
7e11715582 | ||
|
|
c7a5060ca8 | ||
|
|
b7500371ca | ||
|
|
bf0186730c | ||
|
|
6ca683c042 | ||
|
|
c16a8abfa6 | ||
|
|
074e967be7 | ||
|
|
47bc44f775 | ||
|
|
06d46ec0c2 | ||
|
|
28a72bb30c | ||
|
|
b57dc3f98e | ||
|
|
ada3520e32 | ||
|
|
33ebc8e671 | ||
|
|
f1ea6b4ad5 | ||
|
|
0de35e5dd3 | ||
|
|
0ed863ba11 | ||
|
|
da793ea976 | ||
|
|
7e6c62a105 | ||
|
|
3ba6cb1479 | ||
|
|
3c07580bcc | ||
|
|
38fcd8e057 | ||
|
|
c573e659f6 | ||
|
|
f6ea5b6092 | ||
|
|
69256a104a | ||
|
|
ff7b417ce6 | ||
|
|
0e7e969925 | ||
|
|
1dfc2d1c90 | ||
|
|
78d3c10b00 | ||
|
|
bbef4a31a2 | ||
|
|
ace7aa5dd0 | ||
|
|
f531a39d14 | ||
|
|
dbc843ce23 | ||
|
|
e6216478c6 | ||
|
|
42a8e9f77d | ||
|
|
6fdf223404 | ||
|
|
70a3d8cb4a | ||
|
|
8e3b90894e | ||
|
|
a8db179087 | ||
|
|
ee9cd6a54e | ||
|
|
96c905f643 | ||
|
|
2a92db58c0 | ||
|
|
cbfbeb3ce1 | ||
|
|
234568bf20 | ||
|
|
4be1fd71c9 | ||
|
|
374e182461 |
@@ -149,8 +149,7 @@ 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/OpaqueJSONObject ;; JSON object whose keys are not ours to declare; string-keyed
|
||||
ms/PositiveNum ;; Positive number
|
||||
ms/IntGreaterThanOrEqualToZero ;; 0 or positive
|
||||
```
|
||||
|
||||
@@ -31,6 +31,11 @@ Phases 1, 2, 5 and 6 are shared. The middle two differ in **order**: update mode
|
||||
- **The component's intended API** — variants, sizes, and states the design system supports for this component. This defines the axes of your showcase (Phase 3).
|
||||
3. Decide the mode (see **Two modes** above): search for an existing component — in `metabase/ui` or as a legacy component elsewhere. Found → **update**; nothing equivalent anywhere → **create**. Confirm with the user when unsure.
|
||||
4. **Verify a Figma desktop MCP is connected before doing the work** — this skill depends on it for exact tokens in Phase 4 (and styling can't proceed accurately without it). Do a quick read against the issue's node (e.g. `get_metadata`/`get_variable_defs`); if it errors with "nothing selected" or the server isn't connected, prompt the user to enable it (Switch to Dev Mode → MCP section in the right sidebar) and select the relevant layer before continuing.
|
||||
5. **Figma Desktop MCP environments only:** open the issue's exact page with [`figma-open`](./figma-open) before MCP reads. Do not run this helper when Desktop MCP is unavailable.
|
||||
```bash
|
||||
.claude/skills/metabase-ui-component-from-figma/figma-open "<figma-url-with-node-id>"
|
||||
```
|
||||
A URL with `node-id` needs no token. Bare page-name and `--list` lookups require an already-configured `FIGMA_TOKEN`.
|
||||
|
||||
## Phase 2 — Understand the component
|
||||
|
||||
@@ -240,5 +245,6 @@ Only after sign-off:
|
||||
- [ ] Storybook showcase matrix built from the component's full state space (theme excluded — global toggle drives light/dark), single panel, hover/pressed forced via `storybook-addon-pseudo-states`, controls scoped per story; lint + type-check pass. (Update mode: built before styling, **component styles untouched**. Create mode: built after the component is implemented in Phase 4.)
|
||||
- [ ] Figma desktop MCP available; exact tokens extracted and mapped to semantic `--mb-color-*` (NO `color-mix`, NO primitives) and to existing scale vars for radius/spacing/elevation/type — any color *or* dimensional value with no matching token/scale step flagged to the user, not baked in as a literal.
|
||||
- [ ] Component styled; stylelint + eslint(+CSS modules) + type-check pass.
|
||||
- [ ] Loki coverage registered: `storiesFilter` matches exact story display names; targeted `loki update` generated references; reference PNGs and Playwright screenshots visually inspected; targeted `loki test` passes; `.loki/reference/*.png` included in the commit.
|
||||
- [ ] Iterated with the user until satisfied.
|
||||
- [ ] Committed (when asked); call sites migrated/verified (update) or export wired (create); findings doc handed back.
|
||||
|
||||
68
.claude/skills/metabase-ui-component-from-figma/figma-open
Executable file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
# figma-open — open a Figma file (or a specific page) in the Figma desktop app
|
||||
#
|
||||
# Usage:
|
||||
# figma-open <url-or-file-key> open the file
|
||||
# figma-open <url-or-file-key> <node-id> open at a node/page id (e.g. 12-3 or 12:3)
|
||||
# figma-open <url-or-file-key> --list list pages with their ids
|
||||
# figma-open <url-or-file-key> "Page name" open a page by name
|
||||
#
|
||||
# Page lookup by name/--list needs a Figma personal access token:
|
||||
# export FIGMA_TOKEN=figd_... or put it in ~/.config/figma/token
|
||||
set -euo pipefail
|
||||
|
||||
arg="${1:-}"
|
||||
[[ -n "$arg" ]] || { awk 'NR==1 && /^#!/ {next} /^#/ {sub(/^# ?/,""); print; next} {exit}' "$0"; exit 1; }
|
||||
|
||||
token="${FIGMA_TOKEN:-}"
|
||||
[[ -f "$HOME/.config/figma/token" ]] && token="$(<"$HOME/.config/figma/token")"
|
||||
|
||||
# Extract the file key from any Figma URL form, or use the arg as a bare key.
|
||||
key="$arg"
|
||||
case "$arg" in
|
||||
*figma.com*|figma://*)
|
||||
key="$(printf '%s' "$arg" | sed -E 's#https?://([a-z]+\.)?figma\.com##; s#^([a-z0-9-]+\.)?figma\.com##; s#^figma://##; s#^/?(file|design|proto|board)/##; s#^/##; s#/.*##; s#\?.*##')"
|
||||
;;
|
||||
esac
|
||||
|
||||
req="${2:-}"
|
||||
|
||||
# If no page arg was given but the URL carries a node-id, use it.
|
||||
url_node="$(printf '%s' "$arg" | sed -nE 's/.*[?&]node-id=([0-9]+([-:][0-9]+)*).*/\1/p')"
|
||||
|
||||
open_fig() { # $1 = optional node id
|
||||
if [[ -n "${1:-}" ]]; then
|
||||
open "figma://file/${key}?node-id=${1//:/-}"
|
||||
else
|
||||
open "figma://file/${key}"
|
||||
fi
|
||||
}
|
||||
|
||||
# No page requested — open the file (or the node the URL points at).
|
||||
[[ -z "$req" ]] && { open_fig "${url_node:-}"; exit 0; }
|
||||
|
||||
# Explicit node id (page ids work: pages are top-level nodes).
|
||||
if [[ "$req" =~ ^[0-9]+[:-][0-9]+$ ]]; then
|
||||
open_fig "$req"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Everything else needs the REST API.
|
||||
[[ -n "$token" ]] || { echo "error: page lookup needs FIGMA_TOKEN (env or ~/.config/figma/token)" >&2; exit 1; }
|
||||
|
||||
pages_json="$(curl -sf -H "X-Figma-Token: ${token}" "https://api.figma.com/v1/files/${key}?depth=1")" \
|
||||
|| { echo "error: could not fetch file ${key} (bad key, no access, or bad token)" >&2; exit 1; }
|
||||
|
||||
if [[ "$req" == "--list" || "$req" == "-l" ]]; then
|
||||
printf '%s\n' "$pages_json" | jq -r '.document.children[] | "\(.id // .guid)\t\(.name)"' | column -t -s $'\t'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Page by name (case-insensitive, exact match first, then substring).
|
||||
node_id="$(printf '%s' "$pages_json" | jq -r --arg n "$req" \
|
||||
'first(.document.children[] | select((.name // "") | ascii_downcase == ($n | ascii_downcase)) | .id // .guid) //
|
||||
first(.document.children[] | select((.name // "") | ascii_downcase | contains($n | ascii_downcase)) | .id // .guid) //
|
||||
empty')"
|
||||
|
||||
[[ -n "$node_id" ]] || { echo "error: no page matching \"$req\" in file ${key}" >&2; exit 1; }
|
||||
open_fig "$node_id"
|
||||
@@ -17,10 +17,10 @@ Adherence to `typescript-write` is the **highest-priority** review dimension: ra
|
||||
|
||||
Review in this priority order:
|
||||
|
||||
1. **Violations of [`typescript-write`](../typescript-write/SKILL.md) provisions** — no-`any`, type tightening, type modeling, null/undefined handling, naming, structure, comments. Highest priority; block on the no-`any` rule.
|
||||
1. **Violations of [`typescript-write`](../typescript-write/SKILL.md) provisions** — no-`any`, type tightening, type modeling, function signatures, null/undefined handling, naming, structure, comments. Highest priority; block on the no-`any` rule. Apply conditional guidance in context: explain the unsupported type guarantee or concrete readability problem, rather than treating every preference as a blanket ban.
|
||||
2. Compliance with `frontend/CLAUDE.md`.
|
||||
3. Readability and maintainability.
|
||||
4. Appropriate test coverage.
|
||||
4. Appropriate test coverage. For internal typed callers, avoid requesting tests solely for inputs the type system excludes. External API data, deserialised values, storage and JavaScript callers can violate annotations: test runtime validation and nontrivial assumptions at those boundaries. Types do not replace behavioural, security or data-integrity tests.
|
||||
|
||||
## Blind spots — act as the missing reviewer
|
||||
|
||||
@@ -31,3 +31,4 @@ These rarely surface in team reviews, so this skill should raise them. They are
|
||||
- **Security.** Evaluate potential security issues in new code.
|
||||
- **Bundle size.** Flag new large dependencies, default imports from icon or util libs, and heavy modules imported at route-load time.
|
||||
- **Analytics.** User-facing flows should emit tracking events. If a PR adds a new flow (button, modal, navigation) without a tracking event, ask whether one is expected.
|
||||
- **Public API surface** (embedding SDK). Consumers should be able to use public signatures and name types they need to import. Export those types deliberately and document public behaviour, including `@deprecated` for deprecated APIs; a referenced structural type does not automatically need its own named export.
|
||||
|
||||
@@ -20,20 +20,27 @@ description: Write TypeScript and JavaScript code following Metabase coding stan
|
||||
- **Avoid type casts and loose `unknown`** — fix the signature instead.
|
||||
- **If a function only needs one field of a wide object, accept that field** — not the wide object. The cast often disappears once the signature is right.
|
||||
- **Reach for `Partial<T>`, `Pick<T, K>`, `Record<K, V>`, and generics** before reaching for a cast.
|
||||
- **Match dictionary keys to the data.** Use a finite key union when the keys are known. Open dictionaries can use an index signature, `Record<string, T>`, or `Map`; index signatures still constrain values, and changing their spelling to `Record<string, T>` does not make missing-key access safe.
|
||||
- **Prefer making props/components generic** (`<T>`) when a value flows through unchanged and the caller knows the type.
|
||||
- **Prefer `unknown` over loose typing** and narrow before use — an `unknown` value forces a guard at the point of use.
|
||||
- **`satisfies` for object literals** that must conform without widening (config objects, lookup maps, discriminated literals) — better than `: T` (widens) or `as T` (unsafe).
|
||||
- **Avoid non-null assertions (`!`)**. Prefer a guard, early return, or `?.`. Use `!` only when non-nullness is provably true and localized, with a comment.
|
||||
- **Guard indexed lookups that may miss.** Arrays and dictionaries can return `undefined` even when the inferred type omits it. Use an iteration form that preserves the key/value relationship, and only assert `keyof` when the runtime keys are known to belong to the declared type.
|
||||
- **No redundant runtime coercion** — don't wrap already-typed values in `Number()` / `String()` / `Boolean()`.
|
||||
- **Type guards belong in `frontend/src/metabase-types/guards/`**. Do not redefine them locally.
|
||||
- **A cast you can't avoid needs a real justification comment.** The `metabase/no-unjustified-type-casts` rule accepts any preceding comment — state the actual reason the cast is safe. NEVER write the legacy `// Unjustified type cast. FIXME` placeholder; it exists only on casts that predated the rule, and copying it sneaks an unjustified cast past the linter. If you can't articulate why the cast is correct, the cast is wrong — fix the types.
|
||||
- **Keep unavoidable assertions local.** Isolate a repeated or complex assertion behind a helper when that makes its invariant easier to enforce. Test nontrivial runtime assumptions that justify it; a trivial assertion does not automatically need a new helper or test. Do not weaken a public signature just to silence implementation errors.
|
||||
|
||||
## Type modeling
|
||||
|
||||
- **Reuse existing types; don't re-declare them.** Use canonical IDs and domain entity types from `metabase-types/api` (`FieldId`, `TableId`, `ConcreteTableId`, `SchemaName`, …) and key data structures by them (`new Map<ConcreteTableId, …>()`). Don't duplicate generated/API types — compose or derive (`Pick`, `Omit`, indexed access `SomeType["field"]`, `ReturnType`).
|
||||
- **Use generics to allow TypeScript to infer correct types** when creating functions and components that need to be reusable and type-safe. Don't hesitate to introduce complex generics if they allow to derive types automatically instead of manual narrowing.
|
||||
- **Generics must make promises the implementation can keep.** A caller-selected `get<T>(): T` must not disguise an unchecked assertion about external data. Return `unknown` and validate, or accept a validator that establishes `T`. A factory such as `function empty<T>(): T[] { return []; }` is valid; judge the implementation, not how often `T` appears in the signature.
|
||||
- **Prefer an honest type over false precision.** Use generics when they express a real relationship. If a complex type cannot model the behaviour accurately, choose a simpler type or `unknown` with narrowing instead of asserting an unsupported guarantee.
|
||||
- **Model the actual data contract; keep types narrow.** Optional `field?: T` for a key that may be absent, `field: T | undefined` only when the key is always present but the value may be undefined, `| null` for explicit API nulls. Prefer domain unions over broad `string` / `number` / loose `Record`.
|
||||
- **Refer to API implementation** when defining or refining types to ensure they match the actual data structure. When considering a type cast, first consider if the type should be refined to match the actual data structure.
|
||||
- **Optionality must reflect absence.** Keep required fields required and optional fields optional. Normalise input when the application has a meaningful default, not merely to remove a type error. Preserve the actual wire shape in API types.
|
||||
- **Represent related absence together when modelling internal state.** If several fields exist or disappear together, consider a nullable containing object or a discriminated union. Do not reshape a raw API declaration unless the data actually has that shape.
|
||||
- **Prefer explicit special states when designing a format.** Use nullability or named union variants when sentinel values such as `-1` hide meaning. Preserve established protocol sentinel values unless the behaviour is deliberately changed, or normalise them at an explicit boundary.
|
||||
- **Discriminated unions for variant state, with exhaustive checks.** Model "one of N shapes" as a union with a literal discriminant rather than a bag of optional fields, and exhaust it with ts-pattern's `.exhaustive()` so adding a variant becomes a compile error:
|
||||
```ts
|
||||
import { match } from "ts-pattern";
|
||||
@@ -45,20 +52,30 @@ description: Write TypeScript and JavaScript code following Metabase coding stan
|
||||
.exhaustive(); // Compile-time guarantee all cases handled
|
||||
```
|
||||
- **Derive union types from constants** (`as const` + `typeof`/`keyof`) so the type and the values can't drift.
|
||||
- **`readonly` / immutability where mutation isn't intended** — component props, shared constants, exported config. Prefer `readonly T[]` / `ReadonlyArray<T>` for inputs you don't mutate.
|
||||
- **`readonly` / immutability where mutation isn't intended** — component props, shared constants, exported config, and unmutated parameters. Prefer `readonly T[]` / `ReadonlyArray<T>` for inputs you don't mutate. Functions that mutate caller-owned data should make that behaviour explicit.
|
||||
- **Construct complete, well-typed objects where practical.** Prefer an object expression when it avoids partially initialised state or assertions. Incremental construction is fine when it is clearer and maintains the type's invariants.
|
||||
- **Treat `metabase-lib` opaque types as black boxes.** Use `metabase-lib` functions to work with types such as `Lib.Query`, rather than casting into their internal representation.
|
||||
- **Type async and error states explicitly** (a discriminated union or the data-layer's typed result) — never leave loading/error/empty implicit.
|
||||
|
||||
## Function signatures
|
||||
|
||||
- **Make public contracts explicit where it improves stability and clarity.** Annotate parameters and return types at shared boundaries when useful; let local values infer. Use `satisfies` or an annotation when a declaration needs an explicit shape check.
|
||||
- **Accept the inputs the operation supports and return the most precise honest result.** Narrow avoidable uncertainty inside the function, but preserve meaningful nullability and union variants in its return type.
|
||||
- **Use named options when positional arguments are easy to confuse.** Consecutive arguments with the same type are a useful warning sign, not an automatic requirement to rewrite a clear API.
|
||||
- **Use `async`/`await` when it clarifies control flow or error handling.** Returning an existing promise directly is also valid; a Promise return type alone does not require adding `async`.
|
||||
|
||||
## Null and undefined
|
||||
|
||||
- **Narrow at the source**. If a value is optional only in a corner case, don't thread `undefined` through every layer — guard at the producer.
|
||||
- **Sensible defaults for optional values**. Use `?.` and `??` at the consumer.
|
||||
- **Lists should be filtered** before being used in a map or other iteration.
|
||||
- **Narrow nullable list elements when the operation requires present values.** Filter with a type guard when missing entries should be discarded; preserve them when their absence or position carries meaning.
|
||||
- **Avoid non-strict null comparisons** (`X != null`) when `X` can never be `null` — use a strict check or narrow the type. Use `checkNotNull` where necessary.
|
||||
- **Check actual nullability against API implementation**. Find the API endpoint implementation and check if the field can actually be null.
|
||||
|
||||
## Naming
|
||||
|
||||
- **Names describe the entity, not the mechanism**. A name must reflect what the value holds.
|
||||
- **Use domain vocabulary and include units where useful** (`timeoutMs`, `widthPx`, `temperatureC`). Prefer a more specific name when `Info`, `Data`, or `Entity` obscures the meaning, while keeping established terminology when it is clear.
|
||||
- **Align sibling concepts**: keep verb conventions consistent across a related API.
|
||||
- **No names that encode implementation history** rather than current meaning. Suffixes like `Base`, `New`, `Old`, `Initial` need a real semantic distinction, otherwise drop them.
|
||||
- **Avoid cryptic identifiers** (`v`, `n`, `$n`) for domain values; short names are fine only in tiny conventional contexts (loop index `i`, coordinates `x`/`y`, generic params `T`/`K`/`V`).
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
:metabase/namespace-name {:level :warning}
|
||||
:metabase/no-jsqlparser-imports {:level :warning}
|
||||
:metabase/prefer-with-dynamic-fn-redefs {:level :warning}
|
||||
:metabase/t2-query-namespace {:level :warning}
|
||||
:metabase/toucan-model-ns {:level :warning}
|
||||
:metabase/require-shape-checker {:level :warning}
|
||||
:metabase/tests-must-live-in-known-modules {:level :warning}
|
||||
@@ -469,6 +470,7 @@
|
||||
metabase-enterprise.serialization.metadata-file-import.wire-format-test/with-tiny-fixture! 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.tenants.pulse-api-test/with-tenant-pulse-fixture! clojure.core/fn
|
||||
metabase-enterprise.workspaces.test-util/with-resources! clojure.core/let
|
||||
metabase-enterprise.workspaces.test-util/with-workspaces! clojure.core/let
|
||||
metabase.actions.test-util/with-actions clojure.core/let
|
||||
@@ -632,6 +634,45 @@
|
||||
metabase.lib.schema.mbql-clause/define-tuple-mbql-clause hooks.metabase.lib.schema.mbql-clause/define-mbql-clause
|
||||
methodical.core/defmethod hooks.metabase.toucan.table-name/lint-defmethod
|
||||
methodical.macros/defmethod hooks.metabase.toucan.table-name/lint-defmethod
|
||||
metabase.app-db.core/query hooks.metabase.toucan.db-ns/lint-query-call
|
||||
metabase.app-db.core/streaming-reducible-query hooks.metabase.toucan.db-ns/lint-query-call
|
||||
metabase.app-db.core/select-or-insert! hooks.metabase.toucan.db-ns/lint-query-call
|
||||
metabase.app-db.core/update-or-insert! hooks.metabase.toucan.db-ns/lint-query-call
|
||||
metabase.app-db.core/current-timestamp-string hooks.metabase.toucan.db-ns/lint-query-call
|
||||
metabase.app-db.query/query hooks.metabase.toucan.db-ns/lint-query-call
|
||||
metabase.app-db.query/streaming-reducible-query hooks.metabase.toucan.db-ns/lint-query-call
|
||||
metabase.app-db.query/select-or-insert! hooks.metabase.toucan.db-ns/lint-query-call
|
||||
metabase.app-db.query/update-or-insert! hooks.metabase.toucan.db-ns/lint-query-call
|
||||
metabase.app-db.query/current-timestamp-string hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/count hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/delete! hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/exists? hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/insert! hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/insert-returning-instance! hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/insert-returning-instances! hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/insert-returning-pk! hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/insert-returning-pks! hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/query hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/query-one hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/reducible-query hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/reducible-select hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/reducible-update hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/reducible-update-returning-pks hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/save! hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/select hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/select-fn->fn hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/select-fn->pk hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/select-fn-reducible hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/select-fn-set hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/select-fn-vec hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/select-one hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/select-one-fn hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/select-one-pk hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/select-pk->fn hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/select-pks-set hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/select-pks-vec hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/update! hooks.metabase.toucan.db-ns/lint-query-call
|
||||
toucan2.core/update-returning-pks! hooks.metabase.toucan.db-ns/lint-query-call
|
||||
metabase.lib.test-util.macros/$ids hooks.metabase.test.data/$ids
|
||||
metabase.lib.test-util.macros/mbql-query hooks.metabase.test.data/mbql-query
|
||||
metabase.lib.test-util.macros/mbql-5-query hooks.metabase.test.data/mbql-query
|
||||
@@ -840,7 +881,13 @@
|
||||
{:pattern "^metabase(?:((?:-enterprise\\.sandbox)?\\.query-processor)|(?:\\.driver\\.sql(?!server)(?!ite))).*-test$"
|
||||
:name general-qp-and-driver-test-namespaces}
|
||||
{:pattern "(?:^mage.*)"
|
||||
:name mage-namespaces}]
|
||||
:name mage-namespaces}
|
||||
;;
|
||||
;; the app-db module is the application database layer itself (connections, migrations, encryption), so it gets to
|
||||
;; run Toucan 2 queries anywhere.
|
||||
;;
|
||||
{:pattern "^metabase\\.app-db\\..*"
|
||||
:name app-db-namespaces}]
|
||||
|
||||
:config-in-ns
|
||||
{test-namespaces
|
||||
@@ -850,6 +897,7 @@
|
||||
:private-call {:level :off}
|
||||
:metabase/defsetting-must-specify-export {:level :off}
|
||||
:metabase/defsetting-namespace {:level :off}
|
||||
:metabase/t2-query-namespace {:level :off}
|
||||
:metabase/test-helpers-use-non-thread-safe-functions {:level :warning} ; more config in `.clj-kondo/config/test-helpers/config.edn`
|
||||
|
||||
:discouraged-var
|
||||
@@ -857,6 +905,9 @@
|
||||
metabase.lib.convert/->legacy-MBQL {:level :off}
|
||||
metabase.lib.core/->legacy-MBQL {:level :off}}}}
|
||||
|
||||
app-db-namespaces
|
||||
{:linters {:metabase/t2-query-namespace {:level :off}}}
|
||||
|
||||
;; .cljc tests run in both Clojure and ClojureScript — `with-dynamic-fn-redefs` is JVM-only,
|
||||
;; so leave these alone.
|
||||
metabase.lib.aggregation-test
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
query-processor
|
||||
search
|
||||
settings
|
||||
users
|
||||
util
|
||||
warehouses}
|
||||
:model-exports #{:model/Action}
|
||||
@@ -146,6 +147,7 @@
|
||||
collections
|
||||
config
|
||||
events
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
util}
|
||||
@@ -158,7 +160,10 @@
|
||||
|
||||
agent-api
|
||||
{:team "Metabot"
|
||||
:api #{metabase.agent-api.api metabase.agent-api.init metabase.agent-api.usage}
|
||||
:api #{metabase.agent-api.api
|
||||
metabase.agent-api.init
|
||||
metabase.agent-api.schema
|
||||
metabase.agent-api.usage}
|
||||
:uses #{ai-tracing
|
||||
api
|
||||
auth-identity
|
||||
@@ -222,6 +227,7 @@
|
||||
driver
|
||||
eid-translation
|
||||
internal-stats
|
||||
lib
|
||||
lib-be
|
||||
models
|
||||
permissions
|
||||
@@ -291,6 +297,7 @@
|
||||
:uses #{api-scope
|
||||
config
|
||||
events
|
||||
lib
|
||||
models
|
||||
settings
|
||||
util}
|
||||
@@ -325,6 +332,7 @@
|
||||
:uses #{api
|
||||
app-db
|
||||
events
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
users
|
||||
@@ -363,17 +371,17 @@
|
||||
{:team "UX West"
|
||||
:api #{metabase.appearance.core
|
||||
metabase.appearance.init}
|
||||
:uses #{settings util}
|
||||
:uses #{lib settings util}
|
||||
:model-imports #{:model/Dashboard}}
|
||||
|
||||
audit-app
|
||||
{:team "UX West"
|
||||
:api #{metabase.audit-app.core
|
||||
metabase.audit-app.init}
|
||||
:api #{metabase.audit-app.core metabase.audit-app.init}
|
||||
:friends #{enterprise/memoize-monitor}
|
||||
:uses #{api
|
||||
app-db
|
||||
events
|
||||
lib
|
||||
models
|
||||
premium-features
|
||||
settings
|
||||
@@ -407,14 +415,19 @@
|
||||
|
||||
auth-identity
|
||||
{:team "UX West"
|
||||
:api #{metabase.auth-identity.core metabase.auth-identity.db metabase.auth-identity.init}
|
||||
:api #{metabase.auth-identity.core
|
||||
metabase.auth-identity.db
|
||||
metabase.auth-identity.init
|
||||
metabase.auth-identity.schema}
|
||||
:uses #{channel
|
||||
events
|
||||
lib
|
||||
login-history
|
||||
models
|
||||
notification
|
||||
premium-features
|
||||
request
|
||||
users
|
||||
util}
|
||||
:model-exports #{:model/AuthIdentity}
|
||||
:model-imports #{:model/Session :model/User}}
|
||||
@@ -438,6 +451,7 @@
|
||||
:uses #{api
|
||||
app-db
|
||||
collections
|
||||
lib
|
||||
permissions
|
||||
queries
|
||||
util}
|
||||
@@ -464,9 +478,7 @@
|
||||
|
||||
cache
|
||||
{:team "UX West"
|
||||
:api #{metabase.cache.api
|
||||
metabase.cache.core
|
||||
metabase.cache.init}
|
||||
:api #{metabase.cache.api metabase.cache.core metabase.cache.init}
|
||||
:uses #{api
|
||||
app-db
|
||||
config
|
||||
@@ -601,6 +613,7 @@
|
||||
audit-app
|
||||
config
|
||||
events
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
@@ -633,6 +646,7 @@
|
||||
documents
|
||||
eid-translation
|
||||
events
|
||||
lib
|
||||
lib-be
|
||||
models
|
||||
notification
|
||||
@@ -665,6 +679,7 @@
|
||||
api
|
||||
channel
|
||||
events
|
||||
lib
|
||||
models
|
||||
request
|
||||
users
|
||||
@@ -687,16 +702,16 @@
|
||||
|
||||
content-translation
|
||||
{:team "UX West"
|
||||
:api #{metabase.content-translation.models}
|
||||
:api #{metabase.content-translation.models metabase.content-translation.schema}
|
||||
:uses #{premium-features
|
||||
util}
|
||||
:model-exports #{:model/ContentTranslation}}
|
||||
|
||||
content-verification
|
||||
{:team "UX West"
|
||||
:api #{metabase.content-verification.core
|
||||
metabase.content-verification.init}
|
||||
:api #{metabase.content-verification.core metabase.content-verification.init}
|
||||
:uses #{app-db
|
||||
lib
|
||||
models
|
||||
util}
|
||||
:model-exports #{:model/ModerationReview}
|
||||
@@ -809,6 +824,7 @@
|
||||
database-routing
|
||||
driver
|
||||
events
|
||||
lib
|
||||
request
|
||||
sync
|
||||
util
|
||||
@@ -837,6 +853,7 @@
|
||||
batch-processing
|
||||
collections
|
||||
events
|
||||
lib
|
||||
lib-be
|
||||
models
|
||||
parameters
|
||||
@@ -961,6 +978,7 @@
|
||||
{:team "Embedding"
|
||||
:api #{metabase.embedding.init
|
||||
metabase.embedding.jwt
|
||||
metabase.embedding.schema
|
||||
metabase.embedding.settings
|
||||
metabase.embedding.util
|
||||
metabase.embedding.validation}
|
||||
@@ -1004,7 +1022,7 @@
|
||||
{:team "Metabot"
|
||||
:api #{metabase.entity-retrieval.core
|
||||
metabase.entity-retrieval.mirror}
|
||||
:uses #{premium-features}
|
||||
:uses #{premium-features util}
|
||||
:model-imports #{:model/OsiAiContext}}
|
||||
|
||||
events
|
||||
@@ -1028,9 +1046,7 @@
|
||||
|
||||
explorations
|
||||
{:team "UX West"
|
||||
:api #{metabase.explorations.api
|
||||
metabase.explorations.core
|
||||
metabase.explorations.init}
|
||||
:api #{metabase.explorations.api metabase.explorations.core metabase.explorations.init}
|
||||
:uses #{analytics
|
||||
analytics-interface
|
||||
api
|
||||
@@ -1103,6 +1119,7 @@
|
||||
:api #{metabase.glossary.api}
|
||||
:uses #{api
|
||||
events
|
||||
lib
|
||||
models
|
||||
util}
|
||||
:model-exports #{:model/Glossary}
|
||||
@@ -1127,8 +1144,7 @@
|
||||
|
||||
indexed-entities
|
||||
{:team "UX West"
|
||||
:api #{metabase.indexed-entities.api
|
||||
metabase.indexed-entities.init}
|
||||
:api #{metabase.indexed-entities.api metabase.indexed-entities.init}
|
||||
:uses #{analytics
|
||||
api
|
||||
driver
|
||||
@@ -1150,6 +1166,7 @@
|
||||
metabase.indexes.reconcile
|
||||
metabase.indexes.schema}
|
||||
:uses #{driver
|
||||
lib
|
||||
models
|
||||
util}
|
||||
:model-exports #{:model/TableIndex}}
|
||||
@@ -1159,6 +1176,7 @@
|
||||
:api #{metabase.indexes-rest.api}
|
||||
:uses #{api
|
||||
indexes
|
||||
lib
|
||||
transforms-base
|
||||
util}
|
||||
:model-imports #{:model/Database
|
||||
@@ -1178,8 +1196,7 @@
|
||||
internal-stats
|
||||
{:team "UX West"
|
||||
:api #{metabase.internal-stats.core}
|
||||
:uses #{app-db
|
||||
models}
|
||||
:uses #{app-db models util}
|
||||
:model-imports #{:model/AiUsageLog
|
||||
:model/Card
|
||||
:model/Dashboard
|
||||
@@ -1316,13 +1333,12 @@
|
||||
|
||||
login-history
|
||||
{:team "UX West"
|
||||
:api #{metabase.login-history.api
|
||||
metabase.login-history.core
|
||||
metabase.login-history.init}
|
||||
:api #{metabase.login-history.api metabase.login-history.core metabase.login-history.init}
|
||||
:uses #{analytics
|
||||
api
|
||||
app-db
|
||||
channel
|
||||
lib
|
||||
request
|
||||
settings
|
||||
util}
|
||||
@@ -1334,6 +1350,7 @@
|
||||
metabase.mcp.callback-api
|
||||
metabase.mcp.core
|
||||
metabase.mcp.init
|
||||
metabase.mcp.schema
|
||||
metabase.mcp.usage}
|
||||
:model-exports #{:model/McpSessionLog
|
||||
:model/McpToolCallLog}
|
||||
@@ -1342,6 +1359,7 @@
|
||||
api
|
||||
app-db
|
||||
config
|
||||
lib
|
||||
llm
|
||||
metabot
|
||||
oauth-server
|
||||
@@ -1355,7 +1373,7 @@
|
||||
|
||||
measures
|
||||
{:team "Graphy"
|
||||
:api #{metabase.measures.api}
|
||||
:api #{metabase.measures.api metabase.measures.schema}
|
||||
:uses #{api
|
||||
events
|
||||
lib
|
||||
@@ -1376,6 +1394,7 @@
|
||||
metabase.metabot.core
|
||||
metabase.metabot.init
|
||||
metabase.metabot.metadata-perms
|
||||
metabase.metabot.schema
|
||||
metabase.metabot.self
|
||||
metabase.metabot.settings
|
||||
metabase.metabot.tools.entity-details}
|
||||
@@ -1428,6 +1447,7 @@
|
||||
transforms
|
||||
transforms-base
|
||||
util
|
||||
warehouse-schema
|
||||
warehouses}
|
||||
:model-exports #{:model/AiUsageLog
|
||||
:model/Metabot
|
||||
@@ -1457,6 +1477,7 @@
|
||||
metabase.metrics.core}
|
||||
:uses #{api
|
||||
app-db
|
||||
collections
|
||||
events
|
||||
lib
|
||||
lib-be
|
||||
@@ -1464,7 +1485,6 @@
|
||||
models
|
||||
parameters
|
||||
permissions
|
||||
queries
|
||||
query-processor
|
||||
request
|
||||
server
|
||||
@@ -1473,9 +1493,7 @@
|
||||
|
||||
model-persistence
|
||||
{:team "Graphy"
|
||||
:api #{metabase.model-persistence.api
|
||||
metabase.model-persistence.core
|
||||
metabase.model-persistence.init}
|
||||
:api #{metabase.model-persistence.api metabase.model-persistence.core metabase.model-persistence.init}
|
||||
:uses #{api
|
||||
app-db
|
||||
driver
|
||||
@@ -1492,7 +1510,8 @@
|
||||
task
|
||||
task-history
|
||||
tracing
|
||||
util}
|
||||
util
|
||||
warehouses}
|
||||
:model-exports #{:model/PersistedInfo}
|
||||
:model-imports #{:model/Card :model/Database}}
|
||||
|
||||
@@ -1542,8 +1561,7 @@
|
||||
|
||||
native-query-snippets
|
||||
{:team "Graphy"
|
||||
:api #{metabase.native-query-snippets.api
|
||||
metabase.native-query-snippets.core}
|
||||
:api #{metabase.native-query-snippets.api metabase.native-query-snippets.core}
|
||||
:uses #{api
|
||||
collections
|
||||
events
|
||||
@@ -1580,6 +1598,7 @@
|
||||
parameters
|
||||
permissions
|
||||
premium-features
|
||||
queries
|
||||
query-processor
|
||||
request
|
||||
session
|
||||
@@ -1612,6 +1631,7 @@
|
||||
api-scope
|
||||
appearance
|
||||
events
|
||||
lib
|
||||
mcp
|
||||
models
|
||||
request
|
||||
@@ -1682,6 +1702,7 @@
|
||||
remote-sync
|
||||
settings
|
||||
tracing
|
||||
users
|
||||
util
|
||||
enterprise/advanced-permissions}
|
||||
:model-exports #{:model/ApplicationPermissionsRevision
|
||||
@@ -1776,9 +1797,7 @@
|
||||
:api #{metabase.public-sharing.core
|
||||
metabase.public-sharing.init
|
||||
metabase.public-sharing.validation}
|
||||
:uses #{api
|
||||
settings
|
||||
util}
|
||||
:uses #{api settings util}
|
||||
:model-imports #{:model/Action
|
||||
:model/Card
|
||||
:model/Dashboard
|
||||
@@ -1811,9 +1830,7 @@
|
||||
|
||||
pulse
|
||||
{:team "Gadget"
|
||||
:api #{metabase.pulse.api
|
||||
metabase.pulse.core
|
||||
metabase.pulse.init}
|
||||
:api #{metabase.pulse.api metabase.pulse.core metabase.pulse.init}
|
||||
:uses #{api
|
||||
app-db
|
||||
channel
|
||||
@@ -1873,6 +1890,7 @@
|
||||
graph
|
||||
lib
|
||||
lib-be
|
||||
lib-metric
|
||||
metrics
|
||||
models
|
||||
parameters
|
||||
@@ -2040,8 +2058,10 @@
|
||||
metabase.remote-sync.init}
|
||||
:uses #{api
|
||||
events
|
||||
lib
|
||||
models
|
||||
premium-features}
|
||||
premium-features
|
||||
util}
|
||||
:model-imports #{:model/Collection}}
|
||||
|
||||
request
|
||||
@@ -2055,7 +2075,8 @@
|
||||
api
|
||||
config
|
||||
embedding
|
||||
permissions ; FIXME
|
||||
lib
|
||||
permissions
|
||||
premium-features
|
||||
session
|
||||
settings
|
||||
@@ -2107,10 +2128,12 @@
|
||||
:api #{metabase.sample-data.core
|
||||
metabase.sample-data.init}
|
||||
:uses #{driver
|
||||
lib
|
||||
plugins
|
||||
settings
|
||||
sync
|
||||
util}
|
||||
util
|
||||
warehouses}
|
||||
:model-imports #{:model/DataPermissions :model/Database :model/Table}}
|
||||
|
||||
search
|
||||
@@ -2158,10 +2181,21 @@
|
||||
:model/Table
|
||||
:model/User}}
|
||||
|
||||
|
||||
secrets
|
||||
{:team "Graphy"
|
||||
:api #{metabase.secrets.core}
|
||||
:uses #{api driver models premium-features util}}
|
||||
:uses #{api
|
||||
driver
|
||||
lib
|
||||
models
|
||||
premium-features
|
||||
util}}
|
||||
|
||||
security-center
|
||||
{:team "Gadget"
|
||||
:api #{metabase.security-center.schema}
|
||||
:uses #{util}}
|
||||
|
||||
segments
|
||||
{:team "Graphy"
|
||||
@@ -2199,6 +2233,7 @@
|
||||
driver
|
||||
embedding
|
||||
initialization-status
|
||||
lib
|
||||
lib-be
|
||||
mcp
|
||||
oauth-server
|
||||
@@ -2228,6 +2263,7 @@
|
||||
channel
|
||||
config
|
||||
events
|
||||
lib
|
||||
premium-features
|
||||
request
|
||||
settings
|
||||
@@ -2246,6 +2282,7 @@
|
||||
app-db
|
||||
config
|
||||
events
|
||||
lib
|
||||
models
|
||||
premium-features
|
||||
util
|
||||
@@ -2267,6 +2304,7 @@
|
||||
metabase.setup.init}
|
||||
:uses #{app-db
|
||||
config
|
||||
lib
|
||||
settings
|
||||
util}
|
||||
:model-imports #{:model/User}}
|
||||
@@ -2279,6 +2317,7 @@
|
||||
appearance
|
||||
auth-identity
|
||||
events
|
||||
lib
|
||||
request
|
||||
settings
|
||||
setup
|
||||
@@ -2353,6 +2392,7 @@
|
||||
:uses #{api
|
||||
auth-identity
|
||||
config
|
||||
lib
|
||||
permissions
|
||||
premium-features
|
||||
request
|
||||
@@ -2433,11 +2473,10 @@
|
||||
|
||||
task-history
|
||||
{:team "UX West"
|
||||
:api #{metabase.task-history.api
|
||||
metabase.task-history.core
|
||||
metabase.task-history.init}
|
||||
:api #{metabase.task-history.api metabase.task-history.core metabase.task-history.init}
|
||||
:uses #{api
|
||||
config
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
@@ -2467,10 +2506,13 @@
|
||||
config
|
||||
lib
|
||||
lib-be
|
||||
llm
|
||||
mcp
|
||||
metabot
|
||||
permissions
|
||||
premium-features
|
||||
search
|
||||
security-center
|
||||
session
|
||||
util}
|
||||
:model-imports #{:model/AiUsageLog
|
||||
@@ -2501,12 +2543,12 @@
|
||||
|
||||
timeline
|
||||
{:team "UX West"
|
||||
:api #{metabase.timeline.api
|
||||
metabase.timeline.core}
|
||||
:api #{metabase.timeline.api metabase.timeline.core}
|
||||
:uses #{analytics
|
||||
api
|
||||
collections
|
||||
events
|
||||
lib
|
||||
models
|
||||
util}
|
||||
:model-exports #{:model/Timeline}
|
||||
@@ -2609,6 +2651,7 @@
|
||||
metabase.transforms-rest.api.transform-job
|
||||
metabase.transforms-rest.api.transform-tag}
|
||||
:uses #{api
|
||||
lib
|
||||
models
|
||||
request
|
||||
transforms
|
||||
@@ -2670,6 +2713,7 @@
|
||||
driver
|
||||
events
|
||||
lib
|
||||
lib-be
|
||||
model-persistence
|
||||
models
|
||||
permissions
|
||||
@@ -2724,6 +2768,7 @@
|
||||
channel
|
||||
config
|
||||
events
|
||||
lib
|
||||
models
|
||||
notification
|
||||
permissions
|
||||
@@ -2750,6 +2795,7 @@
|
||||
collections
|
||||
config
|
||||
events
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
@@ -2785,14 +2831,14 @@
|
||||
|
||||
view-log
|
||||
{:team "UX West"
|
||||
:api #{metabase.view-log.core
|
||||
metabase.view-log.init}
|
||||
:api #{metabase.view-log.core metabase.view-log.init}
|
||||
:uses #{analytics
|
||||
api
|
||||
app-db
|
||||
audit-app
|
||||
batch-processing
|
||||
events
|
||||
lib
|
||||
models
|
||||
premium-features
|
||||
query-permissions
|
||||
@@ -2813,6 +2859,7 @@
|
||||
metabase.warehouse-schema.models.field-user-settings
|
||||
metabase.warehouse-schema.models.field-values
|
||||
metabase.warehouse-schema.models.table
|
||||
metabase.warehouse-schema.schema
|
||||
metabase.warehouse-schema.table}
|
||||
:uses #{analyze
|
||||
api
|
||||
@@ -2878,7 +2925,8 @@
|
||||
{:team "UX West"
|
||||
:api #{metabase.warehouses.core
|
||||
metabase.warehouses.init
|
||||
metabase.warehouses.models.database}
|
||||
metabase.warehouses.models.database
|
||||
metabase.warehouses.schema}
|
||||
:uses #{analytics-interface
|
||||
api
|
||||
app-db
|
||||
@@ -2954,6 +3002,7 @@
|
||||
query-processor
|
||||
segments
|
||||
settings
|
||||
types
|
||||
util
|
||||
warehouse-schema}
|
||||
:model-imports #{:model/Card
|
||||
@@ -3007,7 +3056,8 @@
|
||||
setup
|
||||
sync
|
||||
users
|
||||
util}
|
||||
util
|
||||
warehouses}
|
||||
:model-imports #{:model/ApiKey :model/Database :model/User}}
|
||||
|
||||
enterprise/advanced-permissions
|
||||
@@ -3050,6 +3100,7 @@
|
||||
:api #{}
|
||||
:uses #{driver
|
||||
premium-features
|
||||
util
|
||||
enterprise/advanced-config
|
||||
enterprise/mfa
|
||||
enterprise/scim
|
||||
@@ -3084,6 +3135,7 @@
|
||||
permissions
|
||||
plugins
|
||||
premium-features
|
||||
queries
|
||||
query-permissions
|
||||
query-processor
|
||||
settings
|
||||
@@ -3092,6 +3144,8 @@
|
||||
sync
|
||||
users
|
||||
util
|
||||
warehouse-schema
|
||||
warehouses
|
||||
enterprise/serialization}
|
||||
:model-imports #{:model/AuditLog
|
||||
:model/Card
|
||||
@@ -3124,6 +3178,7 @@
|
||||
{:team "UX West"
|
||||
:api #{metabase-enterprise.billing.api.routes}
|
||||
:uses #{api
|
||||
lib
|
||||
premium-features
|
||||
store-api
|
||||
util}
|
||||
@@ -3133,6 +3188,7 @@
|
||||
{:team "UX West"
|
||||
:api #{metabase-enterprise.cache.init}
|
||||
:uses #{cache
|
||||
lib
|
||||
premium-features
|
||||
query-processor
|
||||
task
|
||||
@@ -3185,6 +3241,7 @@
|
||||
:api #{metabase-enterprise.content-verification.api.routes}
|
||||
:uses #{api
|
||||
content-verification
|
||||
lib
|
||||
util
|
||||
enterprise/api}
|
||||
:model-imports #{:model/Card :model/Dashboard}}
|
||||
@@ -3239,6 +3296,7 @@
|
||||
config
|
||||
driver
|
||||
embeddings
|
||||
lib
|
||||
metabot
|
||||
models
|
||||
premium-features
|
||||
@@ -3262,14 +3320,17 @@
|
||||
:uses #{api
|
||||
collections
|
||||
events
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
util}
|
||||
:model-imports #{:model/Collection
|
||||
:model-imports #{:model/AuditLog
|
||||
:model/Collection
|
||||
:model/Dimension
|
||||
:model/Field
|
||||
:model/Table}}
|
||||
:model/Table
|
||||
:model/User}}
|
||||
|
||||
enterprise/database-replication
|
||||
{:team "Cloud"
|
||||
@@ -3277,6 +3338,7 @@
|
||||
metabase-enterprise.database-replication.init}
|
||||
:uses #{api
|
||||
driver
|
||||
lib
|
||||
premium-features
|
||||
settings
|
||||
util
|
||||
@@ -3364,8 +3426,10 @@
|
||||
appearance
|
||||
audit-app
|
||||
embedding
|
||||
lib
|
||||
permissions
|
||||
premium-features
|
||||
util
|
||||
enterprise/sso}
|
||||
:model-imports #{:model/Card
|
||||
:model/Collection
|
||||
@@ -3393,6 +3457,7 @@
|
||||
collections
|
||||
entity-retrieval
|
||||
health-inspector
|
||||
lib
|
||||
premium-features
|
||||
search
|
||||
task
|
||||
@@ -3408,6 +3473,7 @@
|
||||
{:team "Gadget"
|
||||
:api #{metabase-enterprise.erd.api}
|
||||
:uses #{api
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
util
|
||||
@@ -3423,9 +3489,11 @@
|
||||
:uses #{analytics
|
||||
analytics-interface
|
||||
api
|
||||
lib
|
||||
premium-features
|
||||
settings
|
||||
util
|
||||
warehouses
|
||||
enterprise/harbormaster}
|
||||
:model-imports #{:model/Database :model/Setting}}
|
||||
|
||||
@@ -3463,13 +3531,16 @@
|
||||
:api #{metabase-enterprise.library.api}
|
||||
:uses #{api
|
||||
collections
|
||||
premium-features}
|
||||
lib
|
||||
premium-features
|
||||
util}
|
||||
:model-imports #{:model/Card :model/Collection :model/Table}}
|
||||
|
||||
enterprise/mcp
|
||||
{:team "Metabot"
|
||||
:api #{metabase-enterprise.mcp.init}
|
||||
:uses #{analytics
|
||||
lib
|
||||
mcp
|
||||
metabot
|
||||
premium-features
|
||||
@@ -3501,6 +3572,7 @@
|
||||
mcp
|
||||
metabot}
|
||||
:uses #{api
|
||||
lib
|
||||
lib-be
|
||||
llm
|
||||
metabot
|
||||
@@ -3524,6 +3596,7 @@
|
||||
:api #{metabase-enterprise.metabot-analytics.api}
|
||||
:uses #{api
|
||||
driver
|
||||
lib
|
||||
metabot
|
||||
permissions
|
||||
query-processor
|
||||
@@ -3540,9 +3613,11 @@
|
||||
metabase-enterprise.mfa.routes}
|
||||
:uses #{api
|
||||
appearance
|
||||
auth-identity
|
||||
channel
|
||||
config
|
||||
events
|
||||
lib
|
||||
premium-features
|
||||
request
|
||||
settings
|
||||
@@ -3577,6 +3652,7 @@
|
||||
app-db
|
||||
collections
|
||||
events
|
||||
lib
|
||||
models
|
||||
premium-features
|
||||
search
|
||||
@@ -3611,12 +3687,15 @@
|
||||
:uses #{analytics
|
||||
api
|
||||
app-db
|
||||
dashboards
|
||||
events
|
||||
lib
|
||||
lib-be
|
||||
measures
|
||||
model-persistence
|
||||
models
|
||||
queries
|
||||
segments
|
||||
source-swap
|
||||
task
|
||||
transforms
|
||||
@@ -3636,8 +3715,7 @@
|
||||
|
||||
enterprise/sandbox
|
||||
{:team "UX West"
|
||||
:api #{metabase-enterprise.sandbox.api.routes
|
||||
metabase-enterprise.sandbox.api.util}
|
||||
:api #{metabase-enterprise.sandbox.api.routes metabase-enterprise.sandbox.api.util}
|
||||
:uses #{api
|
||||
app-db
|
||||
audit-app
|
||||
@@ -3649,6 +3727,7 @@
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
queries
|
||||
query-processor
|
||||
request
|
||||
tenants
|
||||
@@ -3674,6 +3753,7 @@
|
||||
:uses #{analytics-interface
|
||||
api
|
||||
api-keys
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
server
|
||||
@@ -3694,8 +3774,7 @@
|
||||
|
||||
enterprise/security-center
|
||||
{:team "Gadget"
|
||||
:api #{metabase-enterprise.security-center.api
|
||||
metabase-enterprise.security-center.init}
|
||||
:api #{metabase-enterprise.security-center.api metabase-enterprise.security-center.init}
|
||||
:uses #{analytics
|
||||
analytics-interface
|
||||
api
|
||||
@@ -3704,10 +3783,12 @@
|
||||
config
|
||||
driver
|
||||
events
|
||||
lib
|
||||
models
|
||||
notification
|
||||
permissions
|
||||
premium-features
|
||||
security-center
|
||||
settings
|
||||
system
|
||||
task
|
||||
@@ -3735,12 +3816,14 @@
|
||||
embeddings
|
||||
events
|
||||
health-inspector
|
||||
lib
|
||||
lib-be
|
||||
llm
|
||||
metabot
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
request
|
||||
search
|
||||
settings
|
||||
task
|
||||
@@ -3778,7 +3861,8 @@
|
||||
enterprise/snippet-collections
|
||||
{:team "Graphy"
|
||||
:api #{}
|
||||
:uses #{models
|
||||
:uses #{lib
|
||||
models
|
||||
native-query-snippets
|
||||
permissions
|
||||
premium-features
|
||||
@@ -3814,6 +3898,7 @@
|
||||
:uses #{analytics
|
||||
api
|
||||
collections
|
||||
lib
|
||||
premium-features
|
||||
queries
|
||||
request
|
||||
@@ -3830,11 +3915,13 @@
|
||||
app-db
|
||||
auth-identity
|
||||
events
|
||||
lib
|
||||
models
|
||||
request
|
||||
settings
|
||||
system
|
||||
task
|
||||
users
|
||||
util}
|
||||
:model-imports #{:model/AuthIdentity :model/Session :model/User}}
|
||||
|
||||
@@ -3846,6 +3933,7 @@
|
||||
auth-identity
|
||||
collections
|
||||
events
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
@@ -3858,7 +3946,8 @@
|
||||
enterprise/transforms
|
||||
{:team "Graphy"
|
||||
:api #{metabase-enterprise.transforms.api}
|
||||
:uses #{premium-features
|
||||
:uses #{lib
|
||||
premium-features
|
||||
util
|
||||
enterprise/transforms-inspector}
|
||||
:model-imports #{:model/Transform}}
|
||||
@@ -3915,6 +4004,7 @@
|
||||
{:team "UX West"
|
||||
:api #{metabase-enterprise.upload-management.api}
|
||||
:uses #{api
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
:metabase/namespace-name 4
|
||||
:metabase/prefer-with-dynamic-fn-redefs 28
|
||||
:metabase/test-helpers-use-non-thread-safe-functions 18
|
||||
:metabase/validate-defendpoint-has-response-schema 439
|
||||
:metabase/validate-defendpoint-query-params-use-kebab-case 50
|
||||
:metabase/validate-defendpoint-has-response-schema 427
|
||||
:metabase/validate-defendpoint-query-params-use-kebab-case 48
|
||||
:metabase/validate-defendpoint-route-uses-kebab-case 44
|
||||
:metabase/validate-deftest 10
|
||||
:missing-docstring 12
|
||||
@@ -61,6 +61,7 @@
|
||||
:metabase/defsetting-must-specify-export 1
|
||||
:metabase/defsetting-namespace 1
|
||||
:metabase/prefer-with-dynamic-fn-redefs 3
|
||||
:metabase/t2-query-namespace 2
|
||||
:missing-docstring 1
|
||||
:private-call 1
|
||||
:redundant-ignore 1
|
||||
|
||||
35
.clj-kondo/src/hooks/metabase/toucan/db_ns.clj
Normal file
@@ -0,0 +1,35 @@
|
||||
(ns hooks.metabase.toucan.db-ns
|
||||
"Lint that application database query calls -- Toucan 2's `t2/select`, `t2/query`, `t2/insert!`, `t2/update!`,
|
||||
`t2/delete!` and friends, plus the `metabase.app-db.core` wrappers around them such as `mdb/query` and
|
||||
`mdb/update-or-insert!` -- live in a module's `db` namespace, i.e. `metabase[-enterprise].<module>.db` (or `metabase.driver.<driver>.db` for
|
||||
driver modules). Every other namespace in a module goes through those functions instead of talking to the
|
||||
application database directly.
|
||||
|
||||
Registered as an `:analyze-call` hook on each of those functions in `.clj-kondo/config.edn`. The hook
|
||||
returns its input unchanged so Kondo's normal analysis of the call (arity, var usage) still runs."
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]))
|
||||
|
||||
(defn- db-namespace? [ns-sym]
|
||||
(boolean (re-matches #"^metabase(?:-enterprise)?\.(?:driver\.)?[^.]+\.db$" (name ns-sym))))
|
||||
|
||||
(defn- test-file?
|
||||
"Whether `filename` is in a test source tree. Test namespaces are exempt through the `test-namespaces` group in
|
||||
`config.edn`, but helper namespaces like `metabase.sso.test-helpers` don't match that group's pattern."
|
||||
[filename]
|
||||
(boolean (and filename (re-find #"(?:^|/)test/" filename))))
|
||||
|
||||
(defn lint-query-call
|
||||
"Register a `:metabase/t2-query-namespace` finding when a Toucan 2 query call appears outside a `<module>.db`
|
||||
namespace."
|
||||
[{:keys [node ns filename] :as input}]
|
||||
(when (and ns
|
||||
(not (db-namespace? ns))
|
||||
(not (test-file? filename)))
|
||||
(let [fn-node (first (:children node))]
|
||||
(hooks/reg-finding!
|
||||
(assoc (meta fn-node)
|
||||
:message (format "Application database query calls like `%s` must live in metabase[-enterprise].<module>.db (or metabase.driver.<driver>.db) namespaces"
|
||||
(hooks/sexpr fn-node))
|
||||
:type :metabase/t2-query-namespace))))
|
||||
input)
|
||||
50
.clj-kondo/test/hooks/metabase/toucan/db_ns_test.clj
Normal file
@@ -0,0 +1,50 @@
|
||||
(ns hooks.metabase.toucan.db-ns-test
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[clj-kondo.impl.utils]
|
||||
[clojure.test :refer :all]
|
||||
[hooks.metabase.toucan.db-ns :as toucan.db-ns]))
|
||||
|
||||
(defn- lint-query-call [form ns-sym & [filename]]
|
||||
(binding [clj-kondo.impl.utils/*ctx* {:config {:linters {:metabase/t2-query-namespace {:level :warning}}}
|
||||
:ignores (atom nil)
|
||||
:findings (atom [])
|
||||
:namespaces (atom {})}]
|
||||
(let [input {:node (hooks/parse-string (pr-str form))
|
||||
:ns ns-sym
|
||||
:filename (or filename "src/metabase/foo/bar.clj")}
|
||||
output (toucan.db-ns/lint-query-call input)]
|
||||
(is (identical? (:node input) (:node output))
|
||||
"the hook must return the node unchanged so Kondo's normal analysis still runs")
|
||||
@(:findings clj-kondo.impl.utils/*ctx*))))
|
||||
|
||||
(deftest ^:parallel t2-query-must-live-in-db-namespace-test
|
||||
(testing "a query call outside a db namespace is flagged"
|
||||
(is (=? [{:type :metabase/t2-query-namespace
|
||||
:message #".*`t2/select-one`.*"}]
|
||||
(lint-query-call '(t2/select-one :model/Card :id 1) 'metabase.queries.models.card))))
|
||||
(testing "metabase.<module>.db is allowed"
|
||||
(is (empty? (lint-query-call '(t2/select-one :model/Card :id 1) 'metabase.queries.db))))
|
||||
(testing "metabase-enterprise.<module>.db is allowed"
|
||||
(is (empty? (lint-query-call '(t2/select-one :model/Card :id 1) 'metabase-enterprise.sandbox.db))))
|
||||
(testing "metabase.driver.<driver>.db is allowed for driver modules"
|
||||
(is (empty? (lint-query-call '(t2/select-one :model/Database :id 1) 'metabase.driver.bigquery-cloud-sdk.db))))
|
||||
(testing "a nested db namespace is not a module db namespace"
|
||||
(is (=? [{:type :metabase/t2-query-namespace}]
|
||||
(lint-query-call '(t2/query {:select [:*]}) 'metabase.queries.models.db))))
|
||||
(testing "a namespace merely ending in db is not a module db namespace"
|
||||
(is (=? [{:type :metabase/t2-query-namespace}]
|
||||
(lint-query-call '(t2/delete! :model/Card :id 1) 'metabase.queries.dashboards-db))))
|
||||
(testing "files in a test source tree are exempt even when the namespace name doesn't look like a test"
|
||||
(is (empty? (lint-query-call '(t2/update! :model/User 1 {:sso_source nil}) 'metabase.sso.test-helpers
|
||||
"test/metabase/sso/test_helpers.clj")))
|
||||
(is (empty? (lint-query-call '(t2/select :model/Card) 'metabase-enterprise.remote-sync.test-helpers
|
||||
"/repo/enterprise/backend/test/metabase_enterprise/remote_sync/test_helpers.clj"))))
|
||||
(testing "app-db wrappers around Toucan 2 are reported the same way"
|
||||
(is (=? [{:type :metabase/t2-query-namespace
|
||||
:message #".*`mdb/update-or-insert!`.*"}]
|
||||
(lint-query-call '(mdb/update-or-insert! :model/Foo {:id 1}) 'metabase.foo.models.foo))))
|
||||
(testing "fully-qualified calls are reported by their written name"
|
||||
(is (=? [{:type :metabase/t2-query-namespace
|
||||
:message #".*`toucan2.core/insert!`.*"}]
|
||||
(lint-query-call '(toucan2.core/insert! :model/Card {}) 'metabase.queries.models.card)))))
|
||||
31
.github/actions/prepare-shoppy-e2e/action.yml
vendored
@@ -14,21 +14,6 @@ inputs:
|
||||
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
|
||||
@@ -93,20 +78,6 @@ runs:
|
||||
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@d979d5b3a71173a29b74b5b88418bfda9437d885
|
||||
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
|
||||
@@ -115,8 +86,6 @@ runs:
|
||||
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"
|
||||
|
||||
4
.github/actions/test-driver/action.yml
vendored
@@ -20,8 +20,6 @@ inputs:
|
||||
required: false
|
||||
default: "ee"
|
||||
description: Edition to test (oss or ee)
|
||||
quarantine-engine:
|
||||
required: false
|
||||
granular-rerun:
|
||||
required: false
|
||||
default: "true"
|
||||
@@ -199,4 +197,4 @@ runs:
|
||||
with:
|
||||
adapter: backend
|
||||
test-suite: ${{ inputs.junit-name }}
|
||||
enforce-quarantine: ${{ inputs.quarantine-engine == 'ci-conductor' }}
|
||||
enforce-quarantine: "true"
|
||||
|
||||
2
.github/file-paths.yaml
vendored
@@ -159,6 +159,8 @@ project_backend_checks:
|
||||
- "modules/**/*.cljc"
|
||||
- "modules/**/*.cljs"
|
||||
- "**deps.edn"
|
||||
- ".clj-kondo/{src,test}/**/*.{clj,cljc,cljs}"
|
||||
- "{resources,test_resources,test_config}/**/*.{clj,cljc,cljs}"
|
||||
- ".clj-kondo/config.edn"
|
||||
- ".clj-kondo/config/modules/config.edn"
|
||||
- ".github/team.json"
|
||||
|
||||
4
.github/scripts/bundle-size-stats-prepare.ts
vendored
@@ -200,7 +200,9 @@ export function main() {
|
||||
// The last plotted point, as slim rows: [{ bundle, kind, rawBytes, gzipBytes, brotliBytes }].
|
||||
previous: readJson<CacheRow[]>(env.LAST) || [],
|
||||
threshold: Number(env.MIN_DELTA_PERCENT ?? 1),
|
||||
date: new Date().toISOString().slice(0, 10), // YYYY-MM-DD
|
||||
// The commit's own time rather than the day it was measured, so the merges
|
||||
// of one day keep their order on the chart.
|
||||
date: new Date(env.COMMIT_TIMESTAMP || Date.now()).toISOString(),
|
||||
commit: (env.HEAD_SHA || "").slice(0, 12),
|
||||
commitMessage: (env.COMMIT_MESSAGE || "").split("\n")[0], // subject line only
|
||||
version: readVersion(env.VERSION_PROPS),
|
||||
|
||||
6
.github/scripts/create-test-plan.ts
vendored
@@ -24,11 +24,17 @@ const STORY_GLOBS = [
|
||||
"enterprise/frontend/**/*.stories.{js,jsx,ts,tsx}",
|
||||
];
|
||||
|
||||
// `git ls-files -- frontend enterprise/frontend` already prints just over a
|
||||
// megabyte of paths, and node's default maxBuffer is exactly 1 MiB: past that
|
||||
// the spawn dies with ENOBUFS. Give the listings room to grow.
|
||||
const LS_FILES_MAX_BUFFER = 64 * 1024 * 1024;
|
||||
|
||||
// Returns the tracked files under `roots` that match `globs`. The `dot: true`
|
||||
// option means files inside dot-directories such as `.storybook` are included.
|
||||
function listFiles(roots: string[], globs: string[]): string[] {
|
||||
const tracked = execFileSync("git", ["ls-files", "--", ...roots], {
|
||||
encoding: "utf8",
|
||||
maxBuffer: LS_FILES_MAX_BUFFER,
|
||||
})
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
|
||||
102
.github/scripts/upload-bundle-load-stats.ts
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
// Appends the measured load times to the "Bundle Load Times" table in
|
||||
// eng-stats-importer. Reads ROWS (the JSON matrix.js prints) and API_KEY from
|
||||
// env, and stamps each row with the commit it came from.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
import { importStats } from "./stats-import";
|
||||
|
||||
/** One condition, as `frontend/test/bench/matrix.ts` reports it. */
|
||||
interface Condition {
|
||||
network: string;
|
||||
networkMbps: number;
|
||||
latencyMs: number;
|
||||
cpu: string;
|
||||
cpuThrottle: number;
|
||||
coldMs: number;
|
||||
warmMs: number;
|
||||
steadyMs: number;
|
||||
coldSpreadPercent: number;
|
||||
coldTtfbMs: number;
|
||||
coldFirstPaintMs: number;
|
||||
coldAppMountedMs: number;
|
||||
coldLargestPaintMs: number;
|
||||
coldPageReadyMs: number;
|
||||
warmPageReadyMs: number;
|
||||
scripts: number;
|
||||
scriptKb: number;
|
||||
runs: number;
|
||||
}
|
||||
|
||||
interface CommitStamp {
|
||||
sha: string;
|
||||
subject: string;
|
||||
/** ISO 8601, in UTC. */
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
type LoadTimeRow = Record<string, string | number>;
|
||||
|
||||
function buildRows(
|
||||
conditions: Condition[],
|
||||
{ sha, subject, timestamp }: CommitStamp,
|
||||
): LoadTimeRow[] {
|
||||
return conditions.map((condition) => ({
|
||||
// The commit's own time rather than the day it was measured, so the merges
|
||||
// of one day keep their order and a backfilled row lands on its commit.
|
||||
Date: timestamp,
|
||||
// Truncated the same way the bundle-size table does, so the two join.
|
||||
Commit: sha.slice(0, 12),
|
||||
// The stats table carries a free-text Description column. Populate it with
|
||||
// the commit subject so the chart's points are self-describing.
|
||||
Description: subject.split("\n")[0],
|
||||
Network: condition.network,
|
||||
"Network mbps": condition.networkMbps,
|
||||
"Latency ms": condition.latencyMs,
|
||||
CPU: condition.cpu,
|
||||
"CPU throttle": condition.cpuThrottle,
|
||||
"Cold ms": condition.coldMs,
|
||||
"Warm ms": condition.warmMs,
|
||||
"Steady ms": condition.steadyMs,
|
||||
"Cold spread %": condition.coldSpreadPercent,
|
||||
// The cold load broken up. A zero means the browser or the route never
|
||||
// reported that one, rather than that it happened at time zero.
|
||||
"Cold ttfb ms": condition.coldTtfbMs,
|
||||
"Cold first paint ms": condition.coldFirstPaintMs,
|
||||
"Cold app mounted ms": condition.coldAppMountedMs,
|
||||
"Cold largest paint ms": condition.coldLargestPaintMs,
|
||||
"Cold page ready ms": condition.coldPageReadyMs,
|
||||
"Warm page ready ms": condition.warmPageReadyMs,
|
||||
Scripts: condition.scripts,
|
||||
"Script kb": condition.scriptKb,
|
||||
Runs: condition.runs,
|
||||
}));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const path = process.env.ROWS || "artifacts/load-times.json";
|
||||
// matrix.js wrote this file, and Condition is the shape it prints.
|
||||
const conditions = JSON.parse(readFileSync(path, "utf8")) as Condition[];
|
||||
|
||||
const rows = buildRows(conditions, {
|
||||
sha: process.env.HEAD_SHA || "",
|
||||
subject: process.env.COMMIT_MESSAGE || "",
|
||||
timestamp: new Date(process.env.COMMIT_TIMESTAMP || Date.now()).toISOString(),
|
||||
});
|
||||
|
||||
console.table(rows);
|
||||
|
||||
try {
|
||||
await importStats({ table: "bundle_load_times", rows });
|
||||
console.log("Load times uploaded successfully");
|
||||
} catch (error) {
|
||||
// Stats logging is best-effort. A point lost to an unreachable importer is
|
||||
// worth less than a red build on master, and the next commit plots another.
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.log(
|
||||
`::warning::Load-time upload failed after retries; leaving run green: ${message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
6
.github/team.json
vendored
@@ -63,7 +63,8 @@
|
||||
"crisptrutski",
|
||||
"undrfined",
|
||||
"andreis",
|
||||
"prascuna"
|
||||
"prascuna",
|
||||
"krever"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -112,7 +113,8 @@
|
||||
"metatyb",
|
||||
"olehk-mb",
|
||||
"annatomka",
|
||||
"brybacki"
|
||||
"brybacki",
|
||||
"BillyBobFry"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
71
.github/workflows/ai-benchmark.yml
vendored
@@ -1,5 +1,5 @@
|
||||
name: AI Benchmark
|
||||
run-name: "Evals · ${{ github.event_name == 'pull_request' && 'label' || 'manual' }} · ${{ github.event.pull_request.number || inputs.pr || inputs.ref || github.sha }}"
|
||||
run-name: "Evals · ${{ github.event_name == 'pull_request' && github.event.label.name || format('manual · {0}', inputs.scope) }} · ${{ github.event.pull_request.number || inputs.pr || inputs.ref || github.sha }}"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -18,7 +18,16 @@ on:
|
||||
description: "Scope"
|
||||
required: true
|
||||
type: choice
|
||||
options: [smoke, full, "all (EXPENSIVE)"]
|
||||
options:
|
||||
- smoke
|
||||
- full
|
||||
- embarrassing-plain
|
||||
- embarrassing-human
|
||||
- embarrassing-all
|
||||
- goldens-plain
|
||||
- goldens-human
|
||||
- goldens-all
|
||||
- "all (EXPENSIVE)"
|
||||
default: smoke
|
||||
sample:
|
||||
description: "Sample (0 = full suite)"
|
||||
@@ -29,15 +38,15 @@ on:
|
||||
default: main
|
||||
|
||||
concurrency:
|
||||
group: evals-dispatch-${{ (github.event.label.name == 'evals/smoke' && github.event.pull_request.number) || github.run_id }}
|
||||
group: evals-dispatch-${{ github.event_name == 'pull_request' && format('{0}-{1}', github.event.pull_request.number, github.event.label.name) || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
if: >-
|
||||
github.event.pull_request.head.repo.full_name == github.repository
|
||||
&& github.event_name == 'pull_request'
|
||||
&& github.event.label.name == 'evals/smoke'
|
||||
(github.event_name == 'pull_request'
|
||||
&& github.event.pull_request.head.repo.full_name == github.repository
|
||||
&& startsWith(github.event.label.name, 'evals:'))
|
||||
|| github.event_name == 'workflow_dispatch'
|
||||
runs-on: ${{ vars.SLIM_RUNNER_KEY }}
|
||||
timeout-minutes: 5
|
||||
@@ -46,6 +55,7 @@ jobs:
|
||||
ref: ${{ steps.prepare.outputs.ref }}
|
||||
scope: ${{ steps.prepare.outputs.scope }}
|
||||
sample: ${{ steps.prepare.outputs.sample }}
|
||||
label: ${{ steps.prepare.outputs.label }}
|
||||
evals_branch: ${{ steps.prepare.outputs.evals_branch }}
|
||||
steps:
|
||||
- name: Prepare
|
||||
@@ -53,6 +63,7 @@ jobs:
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
LABEL_PR: ${{ github.event.pull_request.number }}
|
||||
LABEL: ${{ github.event.label.name }}
|
||||
INPUT_PR: ${{ inputs.pr }}
|
||||
INPUT_REF: ${{ inputs.ref }}
|
||||
INPUT_SCOPE: ${{ inputs.scope }}
|
||||
@@ -62,14 +73,24 @@ jobs:
|
||||
set -euo pipefail
|
||||
# The two paths are mutually exclusive, and `on:` admits no other trigger.
|
||||
if [[ "$EVENT_NAME" == 'pull_request' ]]; then
|
||||
PR="$LABEL_PR"; REF=''; SCOPE='smoke'
|
||||
SAMPLE='5'; EVALS_BRANCH='main'
|
||||
SCOPE="${LABEL#evals:}"
|
||||
case "$SCOPE" in
|
||||
embarrassing-plain|embarrassing-human|embarrassing-all|goldens-plain|goldens-human|goldens-all)
|
||||
SAMPLE='0' ;;
|
||||
smoke|full)
|
||||
SAMPLE='5' ;;
|
||||
*)
|
||||
echo "::error::'${LABEL}' is not a known evals trigger label; add its scope here and to the workflow's \`scope\` options, or remove the label"
|
||||
exit 1 ;;
|
||||
esac
|
||||
PR="$LABEL_PR"; REF=''; EVALS_BRANCH='main'
|
||||
else
|
||||
PR="$INPUT_PR"; REF="$INPUT_REF"; SCOPE="$INPUT_SCOPE"
|
||||
SAMPLE="${INPUT_SAMPLE:-5}"; EVALS_BRANCH="$INPUT_EVALS_BRANCH"
|
||||
LABEL='' # nothing to consume: a manual dispatch was not triggered by a label
|
||||
fi
|
||||
# None of these is ever multi-line; a newline would forge extra output entries.
|
||||
for v in PR REF SCOPE SAMPLE EVALS_BRANCH; do
|
||||
for v in PR REF SCOPE SAMPLE LABEL EVALS_BRANCH; do
|
||||
if [[ "${!v}" == *$'\n'* ]]; then
|
||||
echo "::error::${v} must be a single line"
|
||||
exit 1
|
||||
@@ -80,6 +101,7 @@ jobs:
|
||||
echo "ref=$REF"
|
||||
echo "scope=$SCOPE"
|
||||
echo "sample=$SAMPLE"
|
||||
echo "label=$LABEL"
|
||||
echo "evals_branch=$EVALS_BRANCH"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -93,18 +115,26 @@ jobs:
|
||||
steps:
|
||||
- name: Comment (building)
|
||||
id: post
|
||||
env:
|
||||
SCOPE: ${{ needs.prepare.outputs.scope }}
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
script: |
|
||||
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
||||
const body = `🤖 **Evals smoke** — ⏳ building the image… ([logs](${runUrl}))`;
|
||||
const prefix = `🤖 **Evals (${process.env.SCOPE})**`;
|
||||
const body = `${prefix} — ⏳ building the image… ([logs](${runUrl}))`;
|
||||
const { owner, repo } = context.repo;
|
||||
const issue_number = context.payload.pull_request.number;
|
||||
// Drop earlier verdicts first: they describe shas that are no longer head, and a stale
|
||||
// green one reads as this PR passing. The prefix survives eval-run's rewrites.
|
||||
// Drop this scope's earlier verdicts first: they describe shas that are no longer head,
|
||||
// and a stale green one reads as this PR passing. Only this scope's, though — a sibling
|
||||
// scope's comment is a live report on a run still going.
|
||||
const existing = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number });
|
||||
for (const c of existing.filter(c => c.body.startsWith('🤖 **Evals'))) {
|
||||
// The second form is what the single `evals/smoke` trigger wrote before the labels were
|
||||
// split per scope; nothing writes it any more, so this clause is only here to tidy PRs
|
||||
// that predate the split and can go once none are open.
|
||||
const stale = c => c.body.startsWith(prefix) || c.body.startsWith('🤖 **Evals smoke**');
|
||||
for (const c of existing.filter(stale)) {
|
||||
await github.rest.issues.deleteComment({ owner, repo, comment_id: c.id }).catch(() => {});
|
||||
}
|
||||
const { data } = await github.rest.issues.createComment({ owner, repo, issue_number, body });
|
||||
@@ -178,8 +208,11 @@ jobs:
|
||||
SHA: ${{ needs.resolve.outputs.sha }}
|
||||
SCOPE: ${{ needs.prepare.outputs.scope }}
|
||||
SAMPLE: ${{ needs.prepare.outputs.sample }}
|
||||
LABEL: ${{ needs.prepare.outputs.label }}
|
||||
COMMENT_ID: ${{ needs.announce.outputs.comment_id }}
|
||||
run: |
|
||||
# `label` is the one eval-run consumes once it has taken the run over; empty on a manual
|
||||
# dispatch, where there is nothing to consume.
|
||||
gh api repos/metabase/evals/actions/workflows/eval-run.yml/dispatches \
|
||||
-f ref="$EVALS_BRANCH" \
|
||||
-f "inputs[pr]=$PR" \
|
||||
@@ -187,16 +220,19 @@ jobs:
|
||||
-f "inputs[scope]=$SCOPE" \
|
||||
-f "inputs[surfaces]=all" \
|
||||
-f "inputs[sample]=$SAMPLE" \
|
||||
-f "inputs[label]=$LABEL" \
|
||||
-f "inputs[evals_branch]=$EVALS_BRANCH" \
|
||||
-f "inputs[comment_id]=$COMMENT_ID"
|
||||
|
||||
finalize:
|
||||
needs: [announce, resolve, image, dispatch]
|
||||
needs: [prepare, announce, resolve, image, dispatch]
|
||||
if: always() && needs.announce.result == 'success' && needs.dispatch.result != 'success'
|
||||
runs-on: ${{ vars.SLIM_RUNNER_KEY }}
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
COMMENT_ID: ${{ needs.announce.outputs.comment_id }}
|
||||
SCOPE: ${{ needs.prepare.outputs.scope }}
|
||||
LABEL: ${{ needs.prepare.outputs.label }}
|
||||
NEEDS_JSON: ${{ toJSON(needs) }}
|
||||
steps:
|
||||
- name: Comment (build/dispatch failed)
|
||||
@@ -207,9 +243,10 @@ jobs:
|
||||
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
||||
const cancelled = Object.values(JSON.parse(process.env.NEEDS_JSON))
|
||||
.some(j => j.result === 'cancelled');
|
||||
const prefix = `🤖 **Evals (${process.env.SCOPE})**`;
|
||||
const body = cancelled
|
||||
? `🤖 **Evals** — ⚪ cancelled before evals started ([logs](${runUrl}))`
|
||||
: `🤖 **Evals** — ❌ build or dispatch failed before evals started ([logs](${runUrl}))`;
|
||||
? `${prefix} — ⚪ cancelled before evals started ([logs](${runUrl}))`
|
||||
: `${prefix} — ❌ build or dispatch failed before evals started ([logs](${runUrl}))`;
|
||||
// The replacement run deletes this comment on its way in, so a concurrency cancel can
|
||||
// land here with nothing to patch. Losing the stamp is fine; failing the job isn't.
|
||||
await github.rest.issues.updateComment({
|
||||
@@ -222,5 +259,5 @@ jobs:
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
name: 'evals/smoke',
|
||||
name: process.env.LABEL,
|
||||
}).catch(() => {});
|
||||
|
||||
3
.github/workflows/app-db.yml
vendored
@@ -217,7 +217,6 @@ jobs:
|
||||
test-args: >-
|
||||
${{ matrix.job.test-args }}
|
||||
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
|
||||
be-tests-mysql:
|
||||
needs: app-db-matrix
|
||||
@@ -311,7 +310,6 @@ jobs:
|
||||
test-args: >-
|
||||
${{ matrix.job.test-args }}
|
||||
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
|
||||
be-tests-postgres:
|
||||
needs: app-db-matrix
|
||||
@@ -388,7 +386,6 @@ jobs:
|
||||
test-args: >-
|
||||
${{ matrix.job.test-args }}
|
||||
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
|
||||
app-db-tests-result:
|
||||
needs:
|
||||
|
||||
44
.github/workflows/bundle-size-stats.yml
vendored
@@ -4,25 +4,54 @@ name: Bundle Size Stats
|
||||
# emitted JS) plus the bundle-stats and embedding-sdk artifacts, measures
|
||||
# initial/total bundle sizes, and appends them to eng-stats-importer.
|
||||
|
||||
# Triggered by "Run tests", which is the workflow that builds the uberjar on a
|
||||
# master merge.
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build Uberjar"]
|
||||
workflows: ["Run tests"]
|
||||
types: [completed]
|
||||
# Master only: releases form non-linear lines whose deltas are meaningless,
|
||||
# so their sizes are backfilled separately rather than logged here.
|
||||
branches: [master]
|
||||
workflow_call:
|
||||
inputs:
|
||||
commit:
|
||||
description: "Commit SHA to measure."
|
||||
type: string
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
concurrency:
|
||||
group: bundle-size-stats-${{ github.event.workflow_run.head_sha }}
|
||||
group: bundle-size-stats-${{ inputs.commit || github.event.workflow_run.head_sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# "Run tests" concludes `failure` on master more often than `success`, from
|
||||
# jobs that have nothing to do with the jar, so its conclusion is not a usable
|
||||
# gate. The artifact is one: the uberjar job publishes it, and a docs-only
|
||||
# merge that skipped that job publishes none.
|
||||
uberjar-built:
|
||||
if: ${{ github.repository == 'metabase/metabase' }}
|
||||
runs-on: ${{ vars.SLIM_RUNNER_KEY }}
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
found: ${{ steps.probe.outputs.fetch-hit }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- id: probe
|
||||
uses: ./.github/actions/fetch-artifact
|
||||
with:
|
||||
commit: ${{ inputs.commit || github.event.workflow_run.head_sha }}
|
||||
edition: ee
|
||||
download: false
|
||||
|
||||
bundle-size-stats:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' && github.repository == 'metabase/metabase' }}
|
||||
needs: [uberjar-built]
|
||||
if: ${{ needs.uberjar-built.outputs.found == 'true' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
HEAD_SHA: ${{ inputs.commit || github.event.workflow_run.head_sha }}
|
||||
# Only record a data point when a bundle's as-served size (brotli where the
|
||||
# build ships .br, else gzip) moved by at least this percent vs the last
|
||||
# plotted point, keeping the chart to real changes.
|
||||
@@ -31,12 +60,12 @@ jobs:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
ref: ${{ env.HEAD_SHA }}
|
||||
- uses: ./.github/actions/prepare-node-bun
|
||||
- name: Download EE uberjar
|
||||
uses: ./.github/actions/fetch-artifact
|
||||
with:
|
||||
commit: ${{ github.event.workflow_run.head_sha }}
|
||||
commit: ${{ env.HEAD_SHA }}
|
||||
edition: ee
|
||||
extract-path: temp/uberjar
|
||||
- name: Extract front-end bundles from uberjar
|
||||
@@ -105,10 +134,11 @@ jobs:
|
||||
# Passed via env (not interpolated into the shell) so the commit
|
||||
# message can't inject commands; the script keeps only the subject.
|
||||
COMMIT_MESSAGE: ${{ github.event.workflow_run.head_commit.message }}
|
||||
COMMIT_TIMESTAMP: ${{ github.event.workflow_run.head_commit.timestamp }}
|
||||
run: bun ./.github/scripts/bundle-size-stats-prepare.ts
|
||||
- name: Import sizes (with deltas) into Stats
|
||||
id: upload
|
||||
if: steps.prepare.outputs.significant == 'true'
|
||||
if: ${{ steps.prepare.outputs.significant == 'true' && inputs.commit == '' }}
|
||||
env:
|
||||
ROWS: artifacts/upload-rows.json
|
||||
ENG_STATS_URL: ${{ secrets.ENG_STATS_URL }}
|
||||
|
||||
6
.github/workflows/docs-links.yml
vendored
@@ -32,3 +32,9 @@ jobs:
|
||||
fail: true
|
||||
args: docs --config ./.lychee/config.toml --offline
|
||||
format: markdown
|
||||
|
||||
# lychee can't read the YAML nav, so this script renders it as markdown first. lychee is on PATH
|
||||
# from the step above.
|
||||
- name: Check nav.yml links
|
||||
if: ${{ !cancelled() }}
|
||||
run: docs/util/check-links.sh nav
|
||||
|
||||
21
.github/workflows/drivers.yml
vendored
@@ -74,6 +74,7 @@ jobs:
|
||||
# All decision logic is consolidated in mage driver-decisions.
|
||||
determine-driver-skips:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
# All driver run decisions (from mage driver-decisions)
|
||||
h2-should-run: ${{ steps.driver-decisions.outputs.h2-should-run }}
|
||||
@@ -162,7 +163,6 @@ jobs:
|
||||
test-args: >-
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
|
||||
be-tests-bigquery-cloud-sdk-ee:
|
||||
needs: determine-driver-skips
|
||||
@@ -216,7 +216,6 @@ jobs:
|
||||
junit-name: 'be-tests-bigquery-cloud-sdk-ee'
|
||||
test-args: ${{ matrix.job.test-args }}
|
||||
artifact-suffix: ${{ matrix.job.name }}
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
- name: Cleanup python-runner
|
||||
if: ${{ always() && matrix.job.needs-python-runner == true }}
|
||||
uses: ./.github/actions/cleanup-python-runner
|
||||
@@ -279,7 +278,6 @@ jobs:
|
||||
junit-name: 'be-tests-clickhouse-ee'
|
||||
test-args: ${{ matrix.job.test-args }}
|
||||
artifact-suffix: ${{ matrix.job.name }}
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
- name: Cleanup python-runner
|
||||
if: ${{ always() && matrix.job.needs-python-runner == true }}
|
||||
uses: ./.github/actions/cleanup-python-runner
|
||||
@@ -313,7 +311,6 @@ jobs:
|
||||
test-args: >-
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
|
||||
be-tests-databricks-ee:
|
||||
needs: determine-driver-skips
|
||||
@@ -376,7 +373,6 @@ jobs:
|
||||
junit-name: 'be-tests-databricks-ee'
|
||||
artifact-suffix: ${{ matrix.job.name }}
|
||||
test-args: ${{ matrix.job.test-args }}
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
|
||||
be-tests-h2:
|
||||
needs: determine-driver-skips
|
||||
@@ -396,7 +392,6 @@ jobs:
|
||||
test-args: >-
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
|
||||
be-tests-h2-oss:
|
||||
needs: determine-driver-skips
|
||||
@@ -417,7 +412,6 @@ jobs:
|
||||
test-args: >-
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
|
||||
be-tests-mysql-mariadb:
|
||||
needs: determine-driver-skips
|
||||
@@ -536,7 +530,6 @@ jobs:
|
||||
test-args: >-
|
||||
${{ matrix.job.test-args }}
|
||||
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
- name: Cleanup python-runner
|
||||
if: always()
|
||||
uses: ./.github/actions/cleanup-python-runner
|
||||
@@ -596,7 +589,6 @@ jobs:
|
||||
junit-name: ${{ matrix.version.junit-name }}
|
||||
test-args: ${{ matrix.job.test-args }}
|
||||
artifact-suffix: ${{ matrix.version.name }}-${{ matrix.job.name }}
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
- name: Cleanup python-runner
|
||||
if: ${{ always() && matrix.job.needs-python-runner == true }}
|
||||
uses: ./.github/actions/cleanup-python-runner
|
||||
@@ -658,7 +650,6 @@ jobs:
|
||||
test-args: >-
|
||||
:exclude-tags '[:mongo-sharded-cluster-tests :mb/transforms-python-test]'
|
||||
:only-tags [:mb/driver-tests]
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
|
||||
be-tests-mongo-sharded-cluster-ee:
|
||||
needs: determine-driver-skips
|
||||
@@ -683,7 +674,6 @@ jobs:
|
||||
junit-name: 'be-tests-mongo-sharded-cluster-ee'
|
||||
test-args: >-
|
||||
:only metabase.driver.mongo.sharded-cluster-test
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
|
||||
be-tests-oracle:
|
||||
needs: determine-driver-skips
|
||||
@@ -748,7 +738,6 @@ jobs:
|
||||
test-args: >-
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
|
||||
be-tests-postgres:
|
||||
needs: determine-driver-skips
|
||||
@@ -841,7 +830,6 @@ jobs:
|
||||
test-args: >-
|
||||
${{ matrix.job.test-args }}
|
||||
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
- name: Cleanup python-runner
|
||||
if: always()
|
||||
uses: ./.github/actions/cleanup-python-runner
|
||||
@@ -908,7 +896,6 @@ jobs:
|
||||
test-args: >-
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
|
||||
be-tests-redshift-ee:
|
||||
needs: determine-driver-skips
|
||||
@@ -999,7 +986,6 @@ jobs:
|
||||
test-args: >-
|
||||
${{ matrix.job.test-args }}
|
||||
artifact-suffix: ${{ matrix.job.name }}
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
- name: Cleanup python-runner
|
||||
if: ${{ always() && matrix.job.needs-python-runner == true }}
|
||||
uses: ./.github/actions/cleanup-python-runner
|
||||
@@ -1062,7 +1048,6 @@ jobs:
|
||||
junit-name: 'be-tests-snowflake-ee'
|
||||
test-args: ${{ matrix.job.test-args }}
|
||||
artifact-suffix: ${{ matrix.job.name }}
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
- name: Cleanup python-runner
|
||||
if: ${{ always() && matrix.job.needs-python-runner == true }}
|
||||
uses: ./.github/actions/cleanup-python-runner
|
||||
@@ -1094,7 +1079,6 @@ jobs:
|
||||
test-args: >-
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
|
||||
be-tests-sqlite-ee:
|
||||
needs: determine-driver-skips
|
||||
@@ -1115,7 +1099,6 @@ jobs:
|
||||
test-args: >-
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
|
||||
be-tests-sqlserver:
|
||||
needs: determine-driver-skips
|
||||
@@ -1171,7 +1154,6 @@ jobs:
|
||||
junit-name: ${{ matrix.version.junit-name }}
|
||||
test-args: ${{ matrix.job.test-args }}
|
||||
artifact-suffix: ${{ matrix.version.name }}-${{ matrix.job.name }}
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
- name: Cleanup python-runner
|
||||
if: ${{ always() && matrix.job.needs-python-runner == true }}
|
||||
uses: ./.github/actions/cleanup-python-runner
|
||||
@@ -1268,7 +1250,6 @@ jobs:
|
||||
test-args: >-
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
|
||||
drivers-tests-result:
|
||||
needs:
|
||||
|
||||
31
.github/workflows/e2e-test.yml
vendored
@@ -64,6 +64,7 @@ jobs:
|
||||
# workspace where the gate step runs; an absolute path anchored to the
|
||||
# workspace makes both agree regardless of cwd.
|
||||
QUARANTINE_FAILURES_FILE: ${{ github.workspace }}/target/quarantine-failures.jsonl
|
||||
RERUN_ARTIFACT_BASE: failed-tests-${{ inputs.name }}-${{ inputs.edition }}
|
||||
# Cypress records video for every spec but the passing specs' videos are
|
||||
# deleted in after:spec so recording is ~5-10% slowdown per shard and almost
|
||||
# never yields an artifact. Failure screenshots are unaffected, and the
|
||||
@@ -119,17 +120,28 @@ jobs:
|
||||
id: cypress-prep
|
||||
uses: ./.github/actions/prepare-cypress
|
||||
|
||||
- name: Download failed test artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
if: github.run_attempt != '1'
|
||||
- name: Resolve previous attempt
|
||||
if: ${{ github.run_attempt != '1' }}
|
||||
run: echo "RERUN_PREV_ATTEMPT=$((GITHUB_RUN_ATTEMPT - 1))" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Restore previously-failed specs
|
||||
if: ${{ github.run_attempt != '1' }}
|
||||
continue-on-error: true
|
||||
id: failed-specs
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: failed-tests-${{ inputs.name }}-${{ inputs.edition }}
|
||||
pattern: ${{ env.RERUN_ARTIFACT_BASE }}-attempt-${{ env.RERUN_PREV_ATTEMPT }}
|
||||
path: target/rerun
|
||||
|
||||
- name: Get failed specs
|
||||
if: github.run_attempt != '1'
|
||||
run: echo "specs=$(cat failed-specs)" >> "$GITHUB_ENV"
|
||||
if: ${{ github.run_attempt != '1' }}
|
||||
run: |
|
||||
if [ -s target/rerun/failed-specs ]; then
|
||||
echo "Re-running only the specs that failed on attempt $RERUN_PREV_ATTEMPT:"
|
||||
cat target/rerun/failed-specs
|
||||
echo "specs=$(cat target/rerun/failed-specs)" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "No failed-spec list from attempt $RERUN_PREV_ATTEMPT; running the full suite."
|
||||
fi
|
||||
|
||||
# run-tests.yml compiles this in parallel with the uberjar build. Downloading it is ~10x faster than compiling it here.
|
||||
# Callers without that job (stress tests, coverage shards, cross-run re-runs) fall through to compiling as before.
|
||||
@@ -232,7 +244,7 @@ jobs:
|
||||
if: |
|
||||
!cancelled() && (steps.split-e2e.outcome == 'failure' || steps.tagged-e2e.outcome == 'failure')
|
||||
with:
|
||||
name: cypress-recording-${{ inputs.name }}-${{ inputs.edition }}
|
||||
name: cypress-recording-${{ inputs.name }}-${{ inputs.edition }}-attempt-${{ github.run_attempt }}
|
||||
path: |
|
||||
./cypress
|
||||
./logs/test.log
|
||||
@@ -242,8 +254,9 @@ jobs:
|
||||
uses: actions/upload-artifact@v7
|
||||
if: |
|
||||
!cancelled() && (steps.split-e2e.outcome == 'failure' || steps.tagged-e2e.outcome == 'failure')
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: failed-tests-${{ inputs.name }}-${{ inputs.edition }}
|
||||
name: ${{ env.RERUN_ARTIFACT_BASE }}-attempt-${{ github.run_attempt }}
|
||||
path: ./cypress/test-results
|
||||
if-no-files-found: ignore
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@ jobs:
|
||||
fail-fast: false
|
||||
env:
|
||||
CI_CONDUCTOR_TEST_SUITE: embedding-sdk-component-react-${{ matrix.react-version }}
|
||||
# Base name of this leg's granular-rerun artifact. Each matrix leg needs its own, or a
|
||||
# leg would restore the other leg's failed set. `-attempt-N` is appended below.
|
||||
RERUN_ARTIFACT_BASE: failed-specs-embedding-sdk-component-react-${{ matrix.react-version }}
|
||||
# Sensitive env vars (tokens, secrets) starting with `CYPRESS_` are accessed via async `cy.env()`
|
||||
# Example: `cy.env(["FOO"]).then(({ FOO }) => ...)`
|
||||
# For non-sensitive config, use `Cypress.expose()` in cypress config instead (synchronous)
|
||||
@@ -73,6 +76,30 @@ jobs:
|
||||
id: cypress-prep
|
||||
uses: ./.github/actions/prepare-cypress
|
||||
|
||||
- name: Resolve previous attempt
|
||||
if: ${{ github.run_attempt != '1' }}
|
||||
run: echo "RERUN_PREV_ATTEMPT=$((GITHUB_RUN_ATTEMPT - 1))" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Restore previously-failed specs
|
||||
if: ${{ github.run_attempt != '1' }}
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
# use pattern instead of name since this should still pass if the artifact doesn't exist
|
||||
pattern: ${{ env.RERUN_ARTIFACT_BASE }}-attempt-${{ env.RERUN_PREV_ATTEMPT }}
|
||||
path: target/rerun
|
||||
|
||||
- name: Get failed specs
|
||||
if: ${{ github.run_attempt != '1' }}
|
||||
run: |
|
||||
if [ -s target/rerun/failed-specs ]; then
|
||||
echo "Re-running failed specs from attempt $RERUN_PREV_ATTEMPT:"
|
||||
cat target/rerun/failed-specs
|
||||
echo "specs=$(cat target/rerun/failed-specs)" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "No failed-spec list from attempt $RERUN_PREV_ATTEMPT; running the full suite."
|
||||
fi
|
||||
|
||||
# esbuild resolves every import in the spec bundle's graph, even the ones tree shaking then drops.
|
||||
# The cljs imports resolve into target/cljs_dev, so the compiled CLJS has to exist before Cypress runs.
|
||||
# run-tests.yml compiles it in parallel with the uberjar build, so downloading is ~10x faster than compiling here.
|
||||
@@ -107,9 +134,18 @@ jobs:
|
||||
|
||||
- name: Run component tests for Embedding SDK
|
||||
id: run-tests
|
||||
shell: bash
|
||||
env:
|
||||
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
run: node e2e/runner/run_cypress_ci.js component
|
||||
SPECS: ${{ env.specs }}
|
||||
# record video for re-runs of previously failed specs
|
||||
CYPRESS_VIDEO: ${{ env.specs && 'true' || 'false' }}
|
||||
run: |
|
||||
SPEC_ARGS=()
|
||||
if [ -n "$SPECS" ]; then
|
||||
SPEC_ARGS=(--spec "$SPECS")
|
||||
fi
|
||||
node e2e/runner/run_cypress_ci.js component "${SPEC_ARGS[@]}"
|
||||
# Keep this step continue-on-error because the quarantine logic controls whether it ultimately fails.
|
||||
continue-on-error: true
|
||||
|
||||
@@ -140,12 +176,21 @@ jobs:
|
||||
uses: actions/upload-artifact@v7
|
||||
if: ${{ !cancelled() && steps.run-tests.outcome == 'failure' }}
|
||||
with:
|
||||
name: cypress-recording-embedding-sdk-react-${{ matrix.react-version }}
|
||||
name: cypress-recording-embedding-sdk-react-${{ matrix.react-version }}-attempt-${{ github.run_attempt }}
|
||||
path: |
|
||||
./cypress
|
||||
./logs/test.log
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: "SDK component tests > Upload failed specs for rerun"
|
||||
uses: actions/upload-artifact@v7
|
||||
if: ${{ !cancelled() && steps.run-tests.outcome == 'failure' }}
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: ${{ env.RERUN_ARTIFACT_BASE }}-attempt-${{ github.run_attempt }}
|
||||
path: ./cypress/test-results
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: "SDK component tests > Publish Summary"
|
||||
if: ${{ !cancelled() && steps.run-tests.outcome == 'failure' }}
|
||||
uses: actions/github-script@v9
|
||||
@@ -195,6 +240,7 @@ jobs:
|
||||
MB_WAREHOUSE_ALLOWED_NETWORKS: "allow-all"
|
||||
METASTORE_DEV_SERVER_URL: ${{ secrets.METASTORE_DEV_SERVER_URL }}
|
||||
CI_CONDUCTOR_TEST_SUITE: embedding-sdk-component-backward-compatibility
|
||||
RERUN_ARTIFACT_BASE: failed-specs-embedding-sdk-component-backward-compatibility
|
||||
# Sensitive env vars (tokens, secrets) starting with `CYPRESS_` are accessed via async `cy.env()`
|
||||
# Example: `cy.env(["FOO"]).then(({ FOO }) => ...)`
|
||||
# For non-sensitive config, use `Cypress.expose()` in cypress config instead (synchronous)
|
||||
@@ -261,6 +307,30 @@ jobs:
|
||||
id: cypress-prep
|
||||
uses: ./.github/actions/prepare-cypress
|
||||
|
||||
- name: Resolve previous attempt
|
||||
if: ${{ github.run_attempt != '1' }}
|
||||
run: echo "RERUN_PREV_ATTEMPT=$((GITHUB_RUN_ATTEMPT - 1))" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Restore previously-failed specs
|
||||
if: ${{ github.run_attempt != '1' }}
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
# use pattern instead of name since this should still pass if the artifact doesn't exist
|
||||
pattern: ${{ env.RERUN_ARTIFACT_BASE }}-attempt-${{ env.RERUN_PREV_ATTEMPT }}
|
||||
path: target/rerun
|
||||
|
||||
- name: Get failed specs
|
||||
if: ${{ github.run_attempt != '1' }}
|
||||
run: |
|
||||
if [ -s target/rerun/failed-specs ]; then
|
||||
echo "Re-running only the specs that failed on attempt $RERUN_PREV_ATTEMPT:"
|
||||
cat target/rerun/failed-specs
|
||||
echo "specs=$(cat target/rerun/failed-specs)" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "No failed-spec list from attempt $RERUN_PREV_ATTEMPT; running the full suite."
|
||||
fi
|
||||
|
||||
# esbuild resolves every import in the spec bundle's graph, even the ones tree shaking then drops.
|
||||
# The cljs imports resolve into target/cljs_dev, so the compiled CLJS has to exist before Cypress runs.
|
||||
# The checked-out tree is the previous commit, which the current run's CLJS artifact was not built from,
|
||||
@@ -283,11 +353,20 @@ jobs:
|
||||
|
||||
- name: Run component tests for Embedding SDK
|
||||
id: run-tests
|
||||
shell: bash
|
||||
env:
|
||||
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
SPECS: ${{ env.specs }}
|
||||
# record video for re-runs of previously failed specs
|
||||
CYPRESS_VIDEO: ${{ env.specs && 'true' || 'false' }}
|
||||
run: |
|
||||
SPEC_ARGS=()
|
||||
if [ -n "$SPECS" ]; then
|
||||
SPEC_ARGS=(--spec "$SPECS")
|
||||
fi
|
||||
node e2e/runner/run_cypress_ci.js component \
|
||||
--expose grepTags="-@skip-backward-compatibility"
|
||||
--expose grepTags="-@skip-backward-compatibility" \
|
||||
"${SPEC_ARGS[@]}"
|
||||
# Keep this step continue-on-error because the quarantine logic controls whether it ultimately fails.
|
||||
continue-on-error: true
|
||||
|
||||
@@ -318,12 +397,21 @@ jobs:
|
||||
uses: actions/upload-artifact@v7
|
||||
if: ${{ !cancelled() && steps.run-tests.outcome == 'failure' }}
|
||||
with:
|
||||
name: cypress-recording-embedding-sdk-cross-version
|
||||
name: cypress-recording-embedding-sdk-cross-version-attempt-${{ github.run_attempt }}
|
||||
path: |
|
||||
./cypress
|
||||
./logs/test.log
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Upload failed specs for rerun
|
||||
uses: actions/upload-artifact@v7
|
||||
if: ${{ !cancelled() && steps.run-tests.outcome == 'failure' }}
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: ${{ env.RERUN_ARTIFACT_BASE }}-attempt-${{ github.run_attempt }}
|
||||
path: ./cypress/test-results
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Publish Summary
|
||||
if: ${{ !cancelled() && steps.run-tests.outcome == 'failure' }}
|
||||
uses: actions/github-script@v9
|
||||
|
||||
@@ -203,9 +203,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
environment: ["production", "development"]
|
||||
# TEMPORARILY DISABLED: re-enable by removing the `false &&` below.
|
||||
if: |
|
||||
false &&
|
||||
!cancelled() && needs.get-sample-apps-data.result == 'success' &&
|
||||
(github.ref == 'refs/heads/master' || github.base_ref == 'master')
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
@@ -235,11 +233,6 @@ jobs:
|
||||
sample-app-branch-name: ${{ needs.get-sample-apps-data.outputs.branch_name }}
|
||||
sample-app-environment: ${{ matrix.environment }}
|
||||
staging-mb-all-features-token: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
|
||||
shoppy-iam-role: ${{ secrets.SHOPPY_IAM_ROLE }}
|
||||
shoppy-metabase-app-db-s3-bucket: ${{ secrets.SHOPPY_METABASE_APP_DB_S3_BUCKET }}
|
||||
shoppy-metabase-app-db-s3-key: ${{ secrets.SHOPPY_METABASE_APP_DB_S3_KEY }}
|
||||
shoppy-metabase-app-db-user: ${{ secrets.SHOPPY_METABASE_APP_DB_USER }}
|
||||
shoppy-metabase-app-db-database: ${{ secrets.SHOPPY_METABASE_APP_DB_DATABASE }}
|
||||
metastore-dev-server-url: ${{ secrets.METASTORE_DEV_SERVER_URL }}
|
||||
shoppy-datadog-application-id: ${{ secrets.SHOPPY_METABASE_APP_DATADOG_APPLICATION_ID }}
|
||||
shoppy-datadog-client-token: ${{ secrets.SHOPPY_METABASE_APP_DATADOG_CLIENT_TOKEN }}
|
||||
@@ -272,9 +265,7 @@ jobs:
|
||||
# They use the shoppy production environment and include performance measurement tests.
|
||||
e2e-shoppy-synthetic-tests:
|
||||
needs: [get-sample-apps-data]
|
||||
# TEMPORARILY DISABLED: re-enable by removing the `false &&` below.
|
||||
if: |
|
||||
false &&
|
||||
!cancelled() && needs.get-sample-apps-data.result == 'success' &&
|
||||
github.event_name == 'push' &&
|
||||
github.ref == 'refs/heads/master'
|
||||
@@ -305,11 +296,6 @@ jobs:
|
||||
sample-app-branch-name: ${{ needs.get-sample-apps-data.outputs.branch_name }}
|
||||
sample-app-environment: production
|
||||
staging-mb-all-features-token: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
|
||||
shoppy-iam-role: ${{ secrets.SHOPPY_IAM_ROLE }}
|
||||
shoppy-metabase-app-db-s3-bucket: ${{ secrets.SHOPPY_METABASE_APP_DB_S3_BUCKET }}
|
||||
shoppy-metabase-app-db-s3-key: ${{ secrets.SHOPPY_METABASE_APP_DB_S3_KEY }}
|
||||
shoppy-metabase-app-db-user: ${{ secrets.SHOPPY_METABASE_APP_DB_USER }}
|
||||
shoppy-metabase-app-db-database: ${{ secrets.SHOPPY_METABASE_APP_DB_DATABASE }}
|
||||
metastore-dev-server-url: ${{ secrets.METASTORE_DEV_SERVER_URL }}
|
||||
shoppy-datadog-application-id: ${{ secrets.SHOPPY_METABASE_APP_DATADOG_APPLICATION_ID }}
|
||||
shoppy-datadog-client-token: ${{ secrets.SHOPPY_METABASE_APP_DATADOG_CLIENT_TOKEN }}
|
||||
|
||||
@@ -1,29 +1,68 @@
|
||||
name: CI for PR Review ENV Destroy
|
||||
run-name: Destroying Dynamic PR Environment for ${{ github.ref_name }} by @${{ github.actor }}
|
||||
name: Cleanup PR Environment
|
||||
run-name: Destroying Dynamic PR Environment for PR ${{ inputs.pr_number || github.event.number }} by @${{ github.actor }}
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
pull_request:
|
||||
types: [ closed, unlabeled ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to destroy environment for'
|
||||
required: true
|
||||
type: string
|
||||
self_hosting:
|
||||
description: 'Self-hosting stack'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
pr_number:
|
||||
description: 'PR number (required for manual dispatch)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
env:
|
||||
PR_NUMBER: ${{ inputs.pr_number || github.event.number }}
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ inputs.pr_number || github.event.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
destroy_pr:
|
||||
# Manual runs go through the pr-env-admin environment for approval.
|
||||
# Skipped for pull_request events, which are already scoped to the PR's own labels.
|
||||
approve:
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
runs-on: ${{ vars.SLIM_RUNNER_KEY }}
|
||||
timeout-minutes: 5
|
||||
environment: pr-env-admin
|
||||
steps:
|
||||
- name: Approved
|
||||
env:
|
||||
PR_NUMBER: ${{ inputs.pr_number }}
|
||||
run: |
|
||||
echo "Destroying PR environment for PR #${PR_NUMBER}"
|
||||
|
||||
cleanup:
|
||||
needs: [ approve ]
|
||||
# Runs when dispatched manually (after approval), when a PR carrying a PR-Env
|
||||
# label is closed, or when one of those labels is removed.
|
||||
if: >-
|
||||
${{ !cancelled()
|
||||
&& (needs.approve.result == 'success' || needs.approve.result == 'skipped')
|
||||
&& (github.event_name == 'workflow_dispatch'
|
||||
|| (github.event.action == 'closed'
|
||||
&& (contains(github.event.pull_request.labels.*.name, 'PR-Env')
|
||||
|| contains(github.event.pull_request.labels.*.name, 'PR-Env-self-hosting')
|
||||
|| contains(github.event.pull_request.labels.*.name, 'PR-Env-analytics-dev')
|
||||
|| contains(github.event.pull_request.labels.*.name, 'PR-Env-Stats-Dump')))
|
||||
|| (github.event.action == 'unlabeled'
|
||||
&& (github.event.label.name == 'PR-Env'
|
||||
|| github.event.label.name == 'PR-Env-self-hosting'
|
||||
|| github.event.label.name == 'PR-Env-analytics-dev'
|
||||
|| github.event.label.name == 'PR-Env-Stats-Dump'))) }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
name: PR Review ENV Destroy
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
env:
|
||||
PR_NUMBER: ${{ inputs.pr_number || github.event.number }}
|
||||
SELF_HOSTING: >-
|
||||
${{ (github.event_name == 'workflow_dispatch' && inputs.self_hosting)
|
||||
|| (github.event.action == 'unlabeled' && github.event.label.name == 'PR-Env-self-hosting')
|
||||
|| (github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'PR-Env-self-hosting')) }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
@@ -69,17 +108,17 @@ jobs:
|
||||
namespace: default
|
||||
user: oidc-token
|
||||
- name: Destroy PR Review ENV
|
||||
if: ${{ !inputs.self_hosting }}
|
||||
if: ${{ env.SELF_HOSTING != 'true' }}
|
||||
run: |
|
||||
kubectl delete metabase -n hosting-pr${{ env.PR_NUMBER }} hosting-pr${{ env.PR_NUMBER }}
|
||||
kubectl delete ns hosting-pr${{ env.PR_NUMBER }}
|
||||
- name: Setup Helm
|
||||
if: ${{ inputs.self_hosting }}
|
||||
if: ${{ env.SELF_HOSTING == 'true' }}
|
||||
uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0
|
||||
with:
|
||||
version: 'latest'
|
||||
- name: Destroy PR Review ENV (self-hosting)
|
||||
if: ${{ inputs.self_hosting }}
|
||||
if: ${{ env.SELF_HOSTING == 'true' }}
|
||||
run: |
|
||||
helm uninstall hosting-pr${{ env.PR_NUMBER }} --namespace hosting-pr${{ env.PR_NUMBER }} || true
|
||||
kubectl delete ns hosting-pr${{ env.PR_NUMBER }} || true
|
||||
154
.github/workflows/lint.security.yml
vendored
@@ -36,6 +36,7 @@ jobs:
|
||||
uses: github/codeql-action/analyze@v4
|
||||
|
||||
trivy:
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -68,8 +69,157 @@ jobs:
|
||||
sarif_file: trivy-results-${{ matrix.edition }}.sarif
|
||||
category: trivy-${{ matrix.edition }}
|
||||
|
||||
# Generate pom.xml files for all Maven projects
|
||||
snyk-generate-poms:
|
||||
name: Generate pom.xml files
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
- name: Generate all pom.xml
|
||||
run: .github/scripts/write-poms.sh
|
||||
- name: Upload pom files
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: pom-files
|
||||
path: |
|
||||
pom.xml
|
||||
modules/drivers/*/pom.xml
|
||||
|
||||
# Yarn/NPM projects that don't need pom files
|
||||
snyk-frontend-projects:
|
||||
name: ${{ matrix.project.name }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
matrix:
|
||||
project:
|
||||
- name: Main Frontend (bun w/ yarn.lock)
|
||||
file: yarn.lock
|
||||
package-manager: yarn
|
||||
category: main-frontend-bun
|
||||
- name: Release Helper
|
||||
file: release/yarn.lock
|
||||
package-manager: yarn
|
||||
category: release-helper
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
bun-version-file: 'package.json'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Snyk doesn't support bun.lock yet, so we generate yarn.lock and scan that.
|
||||
# Bun has a bug where it normalizes "< 3.0.0" to "< 3" in alias entries
|
||||
# but not in dependency specs, causing Snyk to fail matching dependencies.
|
||||
# The sed commands fix this inconsistency. Bun also names aliased entries
|
||||
# after the aliased package instead of the alias, which the script fixes.
|
||||
# Both continue on error to let Snyk report parsing issues directly rather
|
||||
# than failing on the workaround.
|
||||
- name: Generate yarn.lock
|
||||
if: matrix.project.file == 'yarn.lock'
|
||||
run: |
|
||||
bun install --yarn
|
||||
sed -i -E 's/< ([0-9]+)\.0\.0"/< \1"/g; s/< ([0-9]+)\.0"/< \1"/g' yarn.lock || true
|
||||
bun ${{ github.workspace }}/.github/scripts/fix-yarn-lock-aliases.ts || true
|
||||
- name: Generate release/yarn.lock
|
||||
if: matrix.project.file == 'release/yarn.lock'
|
||||
run: |
|
||||
bun install --yarn
|
||||
sed -i -E 's/< ([0-9]+)\.0\.0"/< \1"/g; s/< ([0-9]+)\.0"/< \1"/g' yarn.lock || true
|
||||
bun ${{ github.workspace }}/.github/scripts/fix-yarn-lock-aliases.ts || true
|
||||
working-directory: release
|
||||
- uses: snyk/actions/setup@9adf32b1121593767fc3c057af55b55db032dc04 # v1.0.0
|
||||
- name: Run snyk test
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
continue-on-error: true
|
||||
run: snyk test --org="metabase-AnD9iS4Wfu4eBYMsG2DZ5N" --remote-repo-url="https://github.com/metabase/metabase" --file=${{ matrix.project.file }} --package-manager=${{ matrix.project.package-manager }} --sarif-file-output=snyk-${{ matrix.project.category }}.sarif
|
||||
- name: Upload results
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
with:
|
||||
sarif_file: snyk-${{ matrix.project.category }}.sarif
|
||||
category: ${{ matrix.project.category }}
|
||||
|
||||
# Maven projects that need pom files
|
||||
snyk-maven-projects:
|
||||
needs: snyk-generate-poms
|
||||
name: ${{ matrix.project.name }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
matrix:
|
||||
project:
|
||||
- name: Main Backend (maven)
|
||||
file: pom.xml
|
||||
category: main-backend-maven
|
||||
- name: Driver - Athena
|
||||
file: modules/drivers/athena/pom.xml
|
||||
category: driver-athena
|
||||
- name: Driver - BigQuery Cloud SDK
|
||||
file: modules/drivers/bigquery-cloud-sdk/pom.xml
|
||||
category: driver-bigquery-cloud-sdk
|
||||
- name: Driver - ClickHouse
|
||||
file: modules/drivers/clickhouse/pom.xml
|
||||
category: driver-clickhouse
|
||||
- name: Driver - Databricks
|
||||
file: modules/drivers/databricks/pom.xml
|
||||
category: driver-databricks
|
||||
- name: Driver - Druid JDBC
|
||||
file: modules/drivers/druid-jdbc/pom.xml
|
||||
category: driver-druid-jdbc
|
||||
- name: Driver - Hive-like
|
||||
file: modules/drivers/hive-like/pom.xml
|
||||
category: driver-hive-like
|
||||
- name: Driver - MongoDB
|
||||
file: modules/drivers/mongo/pom.xml
|
||||
category: driver-mongo
|
||||
- name: Driver - Oracle
|
||||
file: modules/drivers/oracle/pom.xml
|
||||
category: driver-oracle
|
||||
- name: Driver - Presto JDBC
|
||||
file: modules/drivers/presto-jdbc/pom.xml
|
||||
category: driver-presto-jdbc
|
||||
- name: Driver - Redshift
|
||||
file: modules/drivers/redshift/pom.xml
|
||||
category: driver-redshift
|
||||
- name: Driver - Snowflake
|
||||
file: modules/drivers/snowflake/pom.xml
|
||||
category: driver-snowflake
|
||||
- name: Driver - SparkSQL
|
||||
file: modules/drivers/sparksql/pom.xml
|
||||
category: driver-sparksql
|
||||
- name: Driver - SQL Server
|
||||
file: modules/drivers/sqlserver/pom.xml
|
||||
category: driver-sqlserver
|
||||
- name: Driver - Starburst
|
||||
file: modules/drivers/starburst/pom.xml
|
||||
category: driver-starburst
|
||||
- name: Driver - Vertica
|
||||
file: modules/drivers/vertica/pom.xml
|
||||
category: driver-vertica
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: snyk/actions/setup@9adf32b1121593767fc3c057af55b55db032dc04 # v1.0.0
|
||||
- name: Download pom files
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: pom-files
|
||||
- name: Run snyk test
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
continue-on-error: true
|
||||
run: snyk test --org="metabase-AnD9iS4Wfu4eBYMsG2DZ5N" --remote-repo-url="https://github.com/metabase/metabase" --file=${{ matrix.project.file }} --package-manager=maven --sarif-file-output=snyk-${{ matrix.project.category }}.sarif
|
||||
|
||||
- name: Upload results
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
with:
|
||||
sarif_file: snyk-${{ matrix.project.category }}.sarif
|
||||
category: ${{ matrix.project.category }}
|
||||
|
||||
notify:
|
||||
needs: [codeql, trivy]
|
||||
needs: [codeql, trivy, snyk-frontend-projects, snyk-maven-projects]
|
||||
if: ${{ failure() && github.repository == 'metabase/metabase' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 5
|
||||
@@ -94,6 +244,8 @@ jobs:
|
||||
const scans = [
|
||||
['CodeQL', '${{ needs.codeql.result }}'],
|
||||
['Trivy', '${{ needs.trivy.result }}'],
|
||||
['Snyk (frontend)', '${{ needs.snyk-frontend-projects.result }}'],
|
||||
['Snyk (maven)', '${{ needs.snyk-maven-projects.result }}'],
|
||||
];
|
||||
const failed = scans.filter(([, result]) => result === 'failure').map(([name]) => name);
|
||||
|
||||
|
||||
38
.github/workflows/pr-env-close-unlabel.yml
vendored
@@ -1,38 +0,0 @@
|
||||
name: PR Closed or Unlabeled
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [ closed, unlabeled ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
pr-env:
|
||||
if: ${{ (github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'PR-Env') && !contains(github.event.pull_request.labels.*.name, 'PR-Env-self-hosting') && !contains(github.event.pull_request.labels.*.name, 'PR-Env-analytics-dev') && !contains(github.event.pull_request.labels.*.name, 'PR-Env-Stats-Dump')) || github.event.label.name == 'PR-Env' }}
|
||||
uses: ./.github/workflows/pr-env-destroy.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
self_hosting: false
|
||||
|
||||
pr-env-self-hosting:
|
||||
if: ${{ (github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'PR-Env-self-hosting')) || github.event.label.name == 'PR-Env-self-hosting' }}
|
||||
uses: ./.github/workflows/pr-env-destroy.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
self_hosting: true
|
||||
|
||||
pr-env-analytics-dev:
|
||||
if: ${{ (github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'PR-Env-analytics-dev')) || github.event.label.name == 'PR-Env-analytics-dev' }}
|
||||
uses: ./.github/workflows/pr-env-destroy.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
self_hosting: false
|
||||
|
||||
pr-env-stats-dump:
|
||||
if: ${{ (github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'PR-Env-Stats-Dump')) || github.event.label.name == 'PR-Env-Stats-Dump' }}
|
||||
uses: ./.github/workflows/pr-env-destroy.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
self_hosting: false
|
||||
33
.github/workflows/pr-env-destroy-manual.yml
vendored
@@ -1,33 +0,0 @@
|
||||
name: PR Env Destroy (Manual)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to destroy environment for'
|
||||
required: true
|
||||
type: string
|
||||
self_hosting:
|
||||
description: 'Self-hosting stack'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
approve:
|
||||
runs-on: ${{ vars.SLIM_RUNNER_KEY }}
|
||||
timeout-minutes: 5
|
||||
environment: pr-env-admin
|
||||
steps:
|
||||
- name: Approved
|
||||
env:
|
||||
PR_NUMBER: ${{ inputs.pr_number }}
|
||||
run: |
|
||||
echo "Destroying PR environment for PR #${PR_NUMBER}"
|
||||
|
||||
destroy:
|
||||
needs: approve
|
||||
uses: ./.github/workflows/pr-env-destroy.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
self_hosting: ${{ inputs.self_hosting }}
|
||||
pr_number: ${{ inputs.pr_number }}
|
||||
128
.github/workflows/report.update-e2e-timings.yml
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
name: Update E2E Timings
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
days_back:
|
||||
description: "Number of days to look back for timing artifacts"
|
||||
type: number
|
||||
default: 7
|
||||
required: false
|
||||
schedule:
|
||||
# Run weekly on Sundays at 6 AM UTC
|
||||
- cron: "0 6 * * 0"
|
||||
|
||||
jobs:
|
||||
collect-and-average-timings:
|
||||
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
DAYS_BACK: ${{ inputs.days_back || 7 }}
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.METABASE_BOT_APP_ID }}
|
||||
private-key: ${{ secrets.METABASE_BOT_APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Collect and average timing data
|
||||
id: collect-timings
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const { collectAndAverageTimings } = require('${{ github.workspace }}/.github/scripts/average-e2e-timings.js');
|
||||
const result = await collectAndAverageTimings({
|
||||
github,
|
||||
context,
|
||||
token: '${{ steps.app-token.outputs.token }}',
|
||||
daysBack: ${{ env.DAYS_BACK }},
|
||||
existingTimingsPath: 'e2e/support/timings.json'
|
||||
});
|
||||
|
||||
core.setOutput('artifact-count', result.artifactCount);
|
||||
|
||||
- name: Configure git and commit changes
|
||||
id: commit
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
git add e2e/support/timings.json
|
||||
# Check if anything changed before commit/push
|
||||
if git diff --cached --quiet; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Timing data is unchanged; skipping the pull request"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
DATE=$(date +%Y%m%d-%H%M%S)
|
||||
BRANCH_NAME="update-e2e-timings-$DATE"
|
||||
echo "BRANCH_NAME=$BRANCH_NAME" >> "$GITHUB_ENV"
|
||||
echo "Branch name will be: $BRANCH_NAME"
|
||||
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
git commit -m "update e2e timings $DATE" --no-verify
|
||||
git push -u origin "$BRANCH_NAME"
|
||||
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create Pull Request
|
||||
id: create_pr
|
||||
if: steps.commit.outputs.changed == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
run: |
|
||||
BODY=$(cat <<'EOF'
|
||||
## Summary
|
||||
This PR updates the E2E test timing data with averaged values from recent test runs.
|
||||
|
||||
## Details
|
||||
- Collected data from the last **${{ env.DAYS_BACK }} days** of test runs
|
||||
- Averaged timing data from **${{ steps.collect-timings.outputs.artifact-count }}** test runs
|
||||
|
||||
## Generated automatically
|
||||
🤖 This PR was created automatically by the `update-e2e-timings` workflow.
|
||||
The timing data helps cypress-split optimize test distribution across parallel jobs.
|
||||
|
||||
Co-Authored-By: GitHub Actions <actions@github.com>
|
||||
EOF
|
||||
)
|
||||
|
||||
URL=$(gh pr create \
|
||||
--base master \
|
||||
--head "$BRANCH_NAME" \
|
||||
--title "[chore] Update E2E test timings with recent data" \
|
||||
--body "$BODY" \
|
||||
--label ".CI & Tests" \
|
||||
--label "no-backport" \
|
||||
--reviewer metabase/devexp)
|
||||
|
||||
PR_NUMBER=${URL##*/}
|
||||
|
||||
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Approve and Merge the PR
|
||||
if: steps.commit.outputs.changed == 'true'
|
||||
env:
|
||||
# uses a different token than the token that created the PR so we can approve and merge it
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ steps.create_pr.outputs.pr_number }}
|
||||
shell: bash
|
||||
run: |
|
||||
gh pr review --approve "$PR_NUMBER"
|
||||
gh pr merge --squash --auto "$PR_NUMBER"
|
||||
5
.github/workflows/semantic-search.yml
vendored
@@ -96,7 +96,6 @@ jobs:
|
||||
test-args: >-
|
||||
${{ matrix.job.test-args }}
|
||||
:exclude-tags '[${{ matrix.job.exclude-tag }}]'
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
|
||||
semantic-search-tests-mariadb:
|
||||
if: ${{ !inputs.skip && (inputs.suite == '' || inputs.suite == 'mariadb') }}
|
||||
@@ -162,7 +161,6 @@ jobs:
|
||||
test-args: >-
|
||||
${{ matrix.job.test-args }}
|
||||
:exclude-tags '[${{ matrix.job.exclude-tag }}]'
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
|
||||
semantic-search-tests-mysql:
|
||||
if: ${{ !inputs.skip && (inputs.suite == '' || inputs.suite == 'mysql') }}
|
||||
@@ -235,7 +233,6 @@ jobs:
|
||||
test-args: >-
|
||||
${{ matrix.job.test-args }}
|
||||
:exclude-tags '[${{ matrix.job.exclude-tag }}]'
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
|
||||
semantic-search-tests-postgres:
|
||||
if: ${{ !inputs.skip && (inputs.suite == '' || inputs.suite == 'postgres') }}
|
||||
@@ -300,7 +297,6 @@ jobs:
|
||||
test-args: >-
|
||||
${{ matrix.job.test-args }}
|
||||
:exclude-tags '[${{ matrix.job.exclude-tag }}]'
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
|
||||
# pgvector-on-the-app-db mode: the app db itself is a pgvector-capable Postgres and
|
||||
# MB_PGVECTOR_DB_URL is deliberately unset. The dedicated-harness tests gate themselves off; the
|
||||
@@ -355,7 +351,6 @@ jobs:
|
||||
test-args: >-
|
||||
${{ matrix.job.test-args }}
|
||||
:exclude-tags '[${{ matrix.job.exclude-tag }}]'
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
|
||||
semantic-search-tests-result:
|
||||
needs:
|
||||
|
||||
165
.github/workflows/snyk.yml
vendored
@@ -1,165 +0,0 @@
|
||||
name: "Snyk"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "release-**"
|
||||
paths:
|
||||
- "**/deps.edn"
|
||||
- "**/package.json"
|
||||
- ".github/workflows/snyk.yml"
|
||||
- ".github/scripts/write-poms.sh"
|
||||
- ".github/scripts/fix-yarn-lock-aliases.ts"
|
||||
schedule:
|
||||
- cron: "0 5 * * *"
|
||||
|
||||
jobs:
|
||||
# Generate pom.xml files for all Maven projects
|
||||
generate-poms:
|
||||
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
|
||||
name: Generate pom.xml files
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
- name: Generate all pom.xml
|
||||
run: .github/scripts/write-poms.sh
|
||||
- name: Upload pom files
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: pom-files
|
||||
path: |
|
||||
pom.xml
|
||||
modules/drivers/*/pom.xml
|
||||
|
||||
# Yarn/NPM projects that don't need pom files
|
||||
frontend-projects:
|
||||
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
|
||||
name: ${{ matrix.project.name }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
strategy:
|
||||
matrix:
|
||||
project:
|
||||
- name: Main Frontend (bun w/ yarn.lock)
|
||||
file: yarn.lock
|
||||
package-manager: yarn
|
||||
category: main-frontend-bun
|
||||
- name: Release Helper
|
||||
file: release/yarn.lock
|
||||
package-manager: yarn
|
||||
category: release-helper
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
bun-version-file: 'package.json'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Snyk doesn't support bun.lock yet, so we generate yarn.lock and scan that.
|
||||
# Bun has a bug where it normalizes "< 3.0.0" to "< 3" in alias entries
|
||||
# but not in dependency specs, causing Snyk to fail matching dependencies.
|
||||
# The sed commands fix this inconsistency. Bun also names aliased entries
|
||||
# after the aliased package instead of the alias, which the script fixes.
|
||||
# Both continue on error to let Snyk report parsing issues directly rather
|
||||
# than failing on the workaround.
|
||||
- name: Generate yarn.lock
|
||||
if: matrix.project.file == 'yarn.lock'
|
||||
run: |
|
||||
bun install --yarn
|
||||
sed -i -E 's/< ([0-9]+)\.0\.0"/< \1"/g; s/< ([0-9]+)\.0"/< \1"/g' yarn.lock || true
|
||||
bun ${{ github.workspace }}/.github/scripts/fix-yarn-lock-aliases.ts || true
|
||||
- name: Generate release/yarn.lock
|
||||
if: matrix.project.file == 'release/yarn.lock'
|
||||
run: |
|
||||
bun install --yarn
|
||||
sed -i -E 's/< ([0-9]+)\.0\.0"/< \1"/g; s/< ([0-9]+)\.0"/< \1"/g' yarn.lock || true
|
||||
bun ${{ github.workspace }}/.github/scripts/fix-yarn-lock-aliases.ts || true
|
||||
working-directory: release
|
||||
- uses: snyk/actions/setup@9adf32b1121593767fc3c057af55b55db032dc04 # v1.0.0
|
||||
- name: Run snyk test
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
continue-on-error: true
|
||||
run: snyk test --org="metabase-AnD9iS4Wfu4eBYMsG2DZ5N" --remote-repo-url="https://github.com/metabase/metabase" --file=${{ matrix.project.file }} --package-manager=${{ matrix.project.package-manager }} --sarif-file-output=snyk-${{ matrix.project.category }}.sarif
|
||||
- name: Upload results
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: snyk-${{ matrix.project.category }}.sarif
|
||||
category: ${{ matrix.project.category }}
|
||||
|
||||
# Maven projects that need pom files
|
||||
maven-projects:
|
||||
needs: generate-poms
|
||||
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
|
||||
name: ${{ matrix.project.name }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
strategy:
|
||||
matrix:
|
||||
project:
|
||||
- name: Main Backend (maven)
|
||||
file: pom.xml
|
||||
category: main-backend-maven
|
||||
- name: Driver - Athena
|
||||
file: modules/drivers/athena/pom.xml
|
||||
category: driver-athena
|
||||
- name: Driver - BigQuery Cloud SDK
|
||||
file: modules/drivers/bigquery-cloud-sdk/pom.xml
|
||||
category: driver-bigquery-cloud-sdk
|
||||
- name: Driver - ClickHouse
|
||||
file: modules/drivers/clickhouse/pom.xml
|
||||
category: driver-clickhouse
|
||||
- name: Driver - Databricks
|
||||
file: modules/drivers/databricks/pom.xml
|
||||
category: driver-databricks
|
||||
- name: Driver - Druid JDBC
|
||||
file: modules/drivers/druid-jdbc/pom.xml
|
||||
category: driver-druid-jdbc
|
||||
- name: Driver - Hive-like
|
||||
file: modules/drivers/hive-like/pom.xml
|
||||
category: driver-hive-like
|
||||
- name: Driver - MongoDB
|
||||
file: modules/drivers/mongo/pom.xml
|
||||
category: driver-mongo
|
||||
- name: Driver - Oracle
|
||||
file: modules/drivers/oracle/pom.xml
|
||||
category: driver-oracle
|
||||
- name: Driver - Presto JDBC
|
||||
file: modules/drivers/presto-jdbc/pom.xml
|
||||
category: driver-presto-jdbc
|
||||
- name: Driver - Redshift
|
||||
file: modules/drivers/redshift/pom.xml
|
||||
category: driver-redshift
|
||||
- name: Driver - Snowflake
|
||||
file: modules/drivers/snowflake/pom.xml
|
||||
category: driver-snowflake
|
||||
- name: Driver - SparkSQL
|
||||
file: modules/drivers/sparksql/pom.xml
|
||||
category: driver-sparksql
|
||||
- name: Driver - SQL Server
|
||||
file: modules/drivers/sqlserver/pom.xml
|
||||
category: driver-sqlserver
|
||||
- name: Driver - Starburst
|
||||
file: modules/drivers/starburst/pom.xml
|
||||
category: driver-starburst
|
||||
- name: Driver - Vertica
|
||||
file: modules/drivers/vertica/pom.xml
|
||||
category: driver-vertica
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: snyk/actions/setup@9adf32b1121593767fc3c057af55b55db032dc04 # v1.0.0
|
||||
- name: Download pom files
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: pom-files
|
||||
- name: Run snyk test
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
continue-on-error: true
|
||||
run: snyk test --org="metabase-AnD9iS4Wfu4eBYMsG2DZ5N" --remote-repo-url="https://github.com/metabase/metabase" --file=${{ matrix.project.file }} --package-manager=maven --sarif-file-output=snyk-${{ matrix.project.category }}.sarif
|
||||
|
||||
- name: Upload results
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: snyk-${{ matrix.project.category }}.sarif
|
||||
category: ${{ matrix.project.category }}
|
||||
126
.github/workflows/test.bundle-load-stats.yml
vendored
Normal file
@@ -0,0 +1,126 @@
|
||||
name: Bundle Load Stats
|
||||
|
||||
# Reuses the uberjar built on every master merge: boots it, signs in, loads the
|
||||
# app in a throttled headless Chrome across four network and CPU conditions, and
|
||||
# appends the times to eng-stats-importer.
|
||||
#
|
||||
# The instance is blank on purpose. `index.html` is built from settings and the
|
||||
# signed-in user alone, so seeding content would not change a single byte of the
|
||||
# document this measures.
|
||||
|
||||
# Triggered by "Run tests", which is the workflow that builds the uberjar on a
|
||||
# master merge.
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Run tests"]
|
||||
types: [completed]
|
||||
# Master only: a release forms a non-linear line whose times cannot be
|
||||
# compared against the commit before it.
|
||||
branches: [master]
|
||||
workflow_call:
|
||||
inputs:
|
||||
commit:
|
||||
description: "Commit SHA to measure."
|
||||
type: string
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
concurrency:
|
||||
group: bundle-load-stats-${{ inputs.commit || github.event.workflow_run.head_sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# "Run tests" concludes `failure` on master more often than `success`, from
|
||||
# jobs that have nothing to do with the jar, so its conclusion is not a usable
|
||||
# gate. The artifact is one: the uberjar job publishes it, and a docs-only
|
||||
# merge that skipped that job publishes none.
|
||||
uberjar-built:
|
||||
if: ${{ github.repository == 'metabase/metabase' }}
|
||||
runs-on: ${{ vars.SLIM_RUNNER_KEY }}
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
found: ${{ steps.probe.outputs.fetch-hit }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- id: probe
|
||||
uses: ./.github/actions/fetch-artifact
|
||||
with:
|
||||
commit: ${{ inputs.commit || github.event.workflow_run.head_sha }}
|
||||
edition: ee
|
||||
download: false
|
||||
|
||||
bundle-load-stats:
|
||||
needs: [uberjar-built]
|
||||
if: ${{ needs.uberjar-built.outputs.found == 'true' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
HEAD_SHA: ${{ inputs.commit || github.event.workflow_run.head_sha }}
|
||||
MB_JETTY_PORT: "4000"
|
||||
SITE: http://localhost:4000
|
||||
# Loads per condition. The first is the cold reading and the rest give the
|
||||
# median, so this trades run time for a steadier number.
|
||||
RUNS: "8"
|
||||
steps:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ env.HEAD_SHA }}
|
||||
- uses: ./.github/actions/prepare-node-bun
|
||||
- name: Prepare JDK 25
|
||||
uses: actions/setup-java@v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
- name: Install Chrome v144
|
||||
uses: browser-actions/setup-chrome@2e1d749697dd1612b833dba4a722266286fbefcd # v2.1.2
|
||||
with:
|
||||
chrome-version: 144
|
||||
id: setup-chrome
|
||||
- name: Download EE uberjar
|
||||
uses: ./.github/actions/fetch-artifact
|
||||
with:
|
||||
commit: ${{ env.HEAD_SHA }}
|
||||
edition: ee
|
||||
extract-path: temp/uberjar
|
||||
- name: Start Metabase
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
# --add-opens matches how the jar is launched everywhere else
|
||||
# (uberjar.yml, pre-release.yml, bin/docker/run_metabase.sh), so the
|
||||
# benchmark runs the app under the JVM configuration we ship.
|
||||
MB_DB_FILE="${RUNNER_TEMP}/bench.db" \
|
||||
nohup java --add-opens java.base/java.nio=ALL-UNNAMED \
|
||||
-jar temp/uberjar/metabase.jar > artifacts/metabase.log 2>&1 &
|
||||
- name: Sign in
|
||||
run: |
|
||||
# Waits for the instance to migrate and come up, then creates the
|
||||
# first user. Masked because the token authenticates as that user.
|
||||
session=$(bun frontend/test/bench/sign-in.ts "$SITE")
|
||||
echo "::add-mask::$session"
|
||||
echo "SESSION_COOKIE=$session" >> "$GITHUB_ENV"
|
||||
- name: Warm the backend
|
||||
run: |
|
||||
# The first document a fresh JVM renders pays for its own JIT. Spend
|
||||
# that here rather than inside the first condition measured.
|
||||
for _ in 1 2 3 4 5; do curl -sS -o /dev/null "$SITE/"; done
|
||||
- name: Measure load times
|
||||
env:
|
||||
CHROME_PATH: ${{ steps.setup-chrome.outputs.chrome-path }}
|
||||
run: |
|
||||
bun frontend/test/bench/matrix.ts "$SITE/" "$RUNS" \
|
||||
| tee artifacts/load-times.json
|
||||
- name: Show the Metabase log if something went wrong
|
||||
if: failure()
|
||||
run: tail -100 artifacts/metabase.log || true
|
||||
- name: Import load times into Stats
|
||||
if: ${{ inputs.commit == '' }}
|
||||
env:
|
||||
ROWS: artifacts/load-times.json
|
||||
# Passed via env (not interpolated into the shell) so the commit
|
||||
# message can't inject commands; the script keeps only the subject.
|
||||
COMMIT_MESSAGE: ${{ github.event.workflow_run.head_commit.message }}
|
||||
COMMIT_TIMESTAMP: ${{ github.event.workflow_run.head_commit.timestamp }}
|
||||
ENG_STATS_URL: ${{ secrets.ENG_STATS_URL }}
|
||||
API_KEY: ${{ secrets.ENG_STATS_API_KEY }}
|
||||
run: bun .github/scripts/upload-bundle-load-stats.ts
|
||||
17
.github/workflows/test.cleanup-dwh-data.yml
vendored
@@ -10,21 +10,15 @@ on:
|
||||
required: false
|
||||
default: 'snowflake,bigquery-cloud-sdk,redshift'
|
||||
type: string
|
||||
temp-data-hours:
|
||||
description: 'TTL in hours for per-run garbage (minimum 2)'
|
||||
hours:
|
||||
description: 'TTL in hours for per-run garbage (minimum 4)'
|
||||
required: false
|
||||
default: '2'
|
||||
type: string
|
||||
fixture-hours:
|
||||
description: 'TTL in hours for datasets runs share (minimum 24)'
|
||||
required: false
|
||||
default: '96'
|
||||
default: '12'
|
||||
type: string
|
||||
|
||||
# `inputs.*` is empty on the scheduled run, so each falls back to the nightly value.
|
||||
env:
|
||||
GC_TEMP_HOURS: ${{ inputs.temp-data-hours || 2 }} # for temp data
|
||||
GC_FIXTURE_HOURS: ${{ inputs.fixture-hours || 96 }} # for reusable data
|
||||
GC_HOURS: ${{ inputs.hours || 12 }} # for temp data
|
||||
GC_DRIVERS: ${{ inputs.drivers || 'snowflake,bigquery-cloud-sdk,redshift' }}
|
||||
|
||||
concurrency:
|
||||
@@ -66,8 +60,7 @@ jobs:
|
||||
run: |
|
||||
clojure -X:dev:drivers:drivers-dev:test metabase.test.data.gc/gc-orphans! \
|
||||
:drivers "\"${{ env.GC_DRIVERS }}\"" \
|
||||
:temp-data-hours "${{ env.GC_TEMP_HOURS }}" \
|
||||
:fixture-hours "${{ env.GC_FIXTURE_HOURS }}" \
|
||||
:hours "${{ env.GC_HOURS }}" \
|
||||
:report-dir '"target/gc-report"'
|
||||
|
||||
# The report is most wanted on the runs that fail, so upload it regardless of the sweep's outcome.
|
||||
|
||||
103
.github/workflows/update-e2e-timings.yml
vendored
@@ -1,103 +0,0 @@
|
||||
name: Update E2E Timings
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
days_back:
|
||||
description: "Number of days to look back for timing artifacts"
|
||||
type: number
|
||||
default: 7
|
||||
required: false
|
||||
schedule:
|
||||
# Run weekly on Sundays at 6 AM UTC
|
||||
- cron: "0 6 * * 0"
|
||||
|
||||
jobs:
|
||||
collect-and-average-timings:
|
||||
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
DAYS_BACK: ${{ inputs.days_back || 7 }}
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.METABASE_BOT_APP_ID }}
|
||||
private-key: ${{ secrets.METABASE_BOT_APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Collect and average timing data
|
||||
id: collect-timings
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const { collectAndAverageTimings } = require('${{ github.workspace }}/.github/scripts/average-e2e-timings.js');
|
||||
const result = await collectAndAverageTimings({
|
||||
github,
|
||||
context,
|
||||
token: '${{ steps.app-token.outputs.token }}',
|
||||
daysBack: ${{ env.DAYS_BACK }},
|
||||
existingTimingsPath: 'e2e/support/timings.json'
|
||||
});
|
||||
|
||||
core.setOutput('artifact-count', result.artifactCount);
|
||||
|
||||
- name: Set up git configuration
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
BRANCH_NAME="update-e2e-timings-$(date +%Y%m%d-%H%M%S)"
|
||||
echo "BRANCH_NAME=$BRANCH_NAME" >> "$GITHUB_ENV"
|
||||
echo "Branch name will be: $BRANCH_NAME"
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
branch: ${{ env.BRANCH_NAME }}
|
||||
base: master
|
||||
commit-message: |
|
||||
Update E2E test timings with averaged data
|
||||
|
||||
Collected timing data from ${{ env.DAYS_BACK }} days of test runs.
|
||||
All specs with timing data were included (simple averages).
|
||||
|
||||
🤖 Generated automatically by update-e2e-timings workflow
|
||||
|
||||
Co-Authored-By: GitHub Actions <actions@github.com>
|
||||
committer: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
|
||||
author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
|
||||
title: "[chore] Update E2E test timings with recent data"
|
||||
body: |
|
||||
## Summary
|
||||
This PR updates the E2E test timing data with averaged values from recent test runs.
|
||||
|
||||
## Details
|
||||
- Collected data from the last **${{ env.DAYS_BACK }} days** of test runs
|
||||
- Averaged timing data from **${{ steps.collect-timings.outputs.artifact-count }}** test runs
|
||||
|
||||
## Generated automatically
|
||||
🤖 This PR was created automatically by the `update-e2e-timings` workflow.
|
||||
The timing data helps cypress-split optimize test distribution across parallel jobs.
|
||||
|
||||
Co-Authored-By: GitHub Actions <actions@github.com>
|
||||
labels: |
|
||||
.CI & Tests
|
||||
no-backport
|
||||
add-paths: e2e/support/timings.json
|
||||
team-reviewers: |
|
||||
devexp
|
||||
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 31 KiB |